tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

event-timing-test-utils.js (22810B)


      1 function mainThreadBusy(ms) {
      2  const target = performance.now() + ms;
      3  while (performance.now() < target);
      4 }
      5 
      6 async function wait() {
      7  return new Promise(resolve => step_timeout(resolve, 0));
      8 }
      9 
     10 async function raf() {
     11  return new Promise(resolve => requestAnimationFrame(resolve));
     12 }
     13 
     14 async function afterNextPaint() {
     15  await raf();
     16  await wait();
     17 }
     18 
     19 async function blockNextEventListener(target, eventType, duration = 120) {
     20  return new Promise(resolve => {
     21    target.addEventListener(eventType, () => {
     22      mainThreadBusy(duration);
     23      resolve();
     24    }, { once: true });
     25  });
     26 }
     27 
     28 async function clickAndBlockMain(id, options = {}) {
     29  options = {
     30    eventType: "pointerdown",
     31    duration: 120,
     32    ...options
     33  };
     34  const element = document.getElementById(id);
     35 
     36  await Promise.all([
     37    blockNextEventListener(element, options.eventType, options.duration),
     38    click(element),
     39  ]);
     40 }
     41 
     42 
     43 // This method should receive an entry of type 'event'. |isFirst| is true only when we want
     44 // to check that the event also happens to correspond to the first event. In this case, the
     45 // timings of the 'first-input' entry should be equal to those of this entry. |minDuration|
     46 // is used to compared against entry.duration.
     47 function verifyEvent(entry, eventType, targetId, isFirst=false, minDuration=104, notCancelable=false) {
     48  assert_equals(entry.cancelable, !notCancelable, 'cancelable property');
     49  assert_equals(entry.name, eventType);
     50  assert_equals(entry.entryType, 'event');
     51  assert_greater_than_equal(entry.duration, minDuration,
     52      "The entry's duration should be greater than or equal to " + minDuration + " ms.");
     53  assert_greater_than_equal(entry.processingStart, entry.startTime,
     54      "The entry's processingStart should be greater than or equal to startTime.");
     55  assert_greater_than_equal(entry.processingEnd, entry.processingStart,
     56      "The entry's processingEnd must be at least as large as processingStart.");
     57  // |duration| is a number rounded to the nearest 8 ms, so add 4 to get a lower bound
     58  // on the actual duration.
     59  assert_greater_than_equal(entry.duration + 4, entry.processingEnd - entry.startTime,
     60      "The entry's duration must be at least as large as processingEnd - startTime.");
     61  if (isFirst) {
     62    let firstInputs = performance.getEntriesByType('first-input');
     63    assert_equals(firstInputs.length, 1, 'There should be a single first-input entry');
     64    let firstInput = firstInputs[0];
     65    assert_equals(firstInput.name, entry.name);
     66    assert_equals(firstInput.entryType, 'first-input');
     67    assert_equals(firstInput.startTime, entry.startTime);
     68    assert_equals(firstInput.duration, entry.duration);
     69    assert_equals(firstInput.processingStart, entry.processingStart);
     70    assert_equals(firstInput.processingEnd, entry.processingEnd);
     71    assert_equals(firstInput.cancelable, entry.cancelable);
     72  }
     73  if (targetId) {
     74    const target = document.getElementById(targetId);
     75    assert_equals(entry.target, target);
     76  }
     77 }
     78 
     79 function verifyClickEvent(entry, targetId, isFirst=false, minDuration=104, event='pointerdown') {
     80  verifyEvent(entry, event, targetId, isFirst, minDuration);
     81 }
     82 
     83 
     84  // Add a PerformanceObserver and observe with a durationThreshold of |dur|. This test will
     85  // attempt to check that the duration is appropriately checked by:
     86  // * Asserting that entries received have a duration which is the smallest multiple of 8
     87  //   that is greater than or equal to |dur|.
     88  // * Issuing |numEntries| entries that has duration greater than |slowDur|.
     89  // * Asserting that exactly |numEntries| entries are received.
     90  // Parameters:
     91  // |t|          - the test harness.
     92  // |dur|        - the durationThreshold for the PerformanceObserver.
     93  // |id|         - the ID of the element to be clicked.
     94  // |numEntries| - the number of entries.
     95  // |slowDur|    - the min duration of a slow entry.
     96 async function testDuration(t, id, numEntries, dur, slowDur) {
     97  assert_implements(window.PerformanceEventTiming, 'Event Timing is not supported.');
     98  const observerPromise = new Promise(async resolve => {
     99    let minDuration = Math.ceil(dur / 8) * 8;
    100    // Exposed events must always have a minimum duration of 16.
    101    minDuration = Math.max(minDuration, 16);
    102    let numEntriesReceived = 0;
    103    new PerformanceObserver(list => {
    104      const pointerDowns = list.getEntriesByName('pointerdown');
    105      pointerDowns.forEach(e => {
    106        t.step(() => {
    107          verifyClickEvent(e, id, false /* isFirst */, minDuration);
    108        });
    109      });
    110      numEntriesReceived += pointerDowns.length;
    111      // All the entries should be received since the slowDur is higher
    112      // than the duration threshold.
    113      if (numEntriesReceived === numEntries)
    114        resolve();
    115    }).observe({type: "event", durationThreshold: dur});
    116  });
    117  const clicksPromise = new Promise(async resolve => {
    118    for (let index = 0; index < numEntries; index++) {
    119      // Add some click events that has at least slowDur for duration.
    120      await clickAndBlockMain(id, { duration: slowDur });
    121    }
    122    resolve();
    123  });
    124  return Promise.all([observerPromise, clicksPromise]);
    125 }
    126 
    127  // Add a PerformanceObserver and observe with a durationThreshold of |durThreshold|. This test will
    128  // attempt to check that the duration is appropriately checked by:
    129  // * Asserting that entries received have a duration which is the smallest multiple of 8
    130  //   that is greater than or equal to |durThreshold|.
    131  // * Issuing |numEntries| entries that have at least |processingDelay| as duration.
    132  // * Asserting that the entries we receive has duration greater than or equals to the
    133  //   duration threshold we setup
    134  // Parameters:
    135  // |t|                     - the test harness.
    136  // |id|                    - the ID of the element to be clicked.
    137  // |durThreshold|          - the durationThreshold for the PerformanceObserver.
    138  // |numEntries|            - the number of slow and number of fast entries.
    139  // |processingDelay|       - the event duration we add on each event.
    140  async function testDurationWithDurationThreshold(t, id, numEntries, durThreshold, processingDelay) {
    141    assert_implements(window.PerformanceEventTiming, 'Event Timing is not supported.');
    142    const observerPromise = new Promise(async resolve => {
    143      let minDuration = Math.ceil(durThreshold / 8) * 8;
    144      // Exposed events must always have a minimum duration of 16.
    145      minDuration = Math.max(minDuration, 16);
    146      new PerformanceObserver(t.step_func(list => {
    147        const pointerDowns = list.getEntriesByName('pointerdown');
    148        pointerDowns.forEach(p => {
    149        assert_greater_than_equal(p.duration, minDuration,
    150          "The entry's duration should be greater than or equal to " + minDuration + " ms.");
    151        });
    152        resolve();
    153      })).observe({type: "event", durationThreshold: durThreshold});
    154    });
    155    for (let index = 0; index < numEntries; index++) {
    156      // These clicks are expected to be ignored, unless the test has some extra delays.
    157      // In that case, the test will verify the event duration to ensure the event duration is
    158      // greater than the duration threshold
    159      await clickAndBlockMain(id, { duration: processingDelay });
    160    }
    161    // Send click with event duration equals to or greater than |durThreshold|, so the
    162    // observer promise can be resolved
    163    await clickAndBlockMain(id, { duration: durThreshold });
    164    return observerPromise;
    165  }
    166 
    167 // Apply events that trigger an event of the given |eventType| to be dispatched to the
    168 // |target|. Some of these assume that the target is not on the top left corner of the
    169 // screen, which means that (0, 0) of the viewport is outside of the |target|.
    170 function applyAction(eventType, target) {
    171  const actions = new test_driver.Actions();
    172  if (eventType === 'auxclick') {
    173    actions.pointerMove(0, 0, {origin: target})
    174    .pointerDown({button: actions.ButtonType.MIDDLE})
    175    .pointerUp({button: actions.ButtonType.MIDDLE});
    176  } else if (eventType === 'click' || eventType === 'mousedown' || eventType === 'mouseup'
    177      || eventType === 'pointerdown' || eventType === 'pointerup'
    178      || eventType === 'touchstart' || eventType === 'touchend') {
    179    actions.pointerMove(0, 0, {origin: target})
    180    .pointerDown()
    181    .pointerUp();
    182  } else if (eventType === 'contextmenu') {
    183    actions.pointerMove(0, 0, {origin: target})
    184    .pointerDown({button: actions.ButtonType.RIGHT})
    185    .pointerUp({button: actions.ButtonType.RIGHT});
    186  } else if (eventType === 'dblclick') {
    187    actions.pointerMove(0, 0, {origin: target})
    188    .pointerDown()
    189    .pointerUp()
    190    .pointerDown()
    191    .pointerUp()
    192    // Reset by clicking outside of the target.
    193    .pointerMove(0, 0)
    194    .pointerDown()
    195  } else if (eventType === 'mouseenter' || eventType === 'mouseover'
    196      || eventType === 'pointerenter' || eventType === 'pointerover') {
    197    // Move outside of the target and then back inside.
    198    // Moving it to 0, 1 because 0, 0 doesn't cause the pointer to
    199    // move in Firefox. See https://github.com/w3c/webdriver/issues/1545
    200    actions.pointerMove(0, 1)
    201    .pointerMove(0, 0, {origin: target});
    202  } else if (eventType === 'mouseleave' || eventType === 'mouseout'
    203      || eventType === 'pointerleave' || eventType === 'pointerout') {
    204    actions.pointerMove(0, 0, {origin: target})
    205    .pointerMove(0, 0);
    206  } else if (eventType === 'keyup' || eventType === 'keydown') {
    207    // Any key here as an input should work.
    208    // TODO: Switch this to use test_driver.Actions.key{up,down}
    209    // when test driver supports it.
    210    // Please check crbug.com/893480.
    211    const key = 'k';
    212    return test_driver.send_keys(target, key);
    213  } else {
    214    assert_unreached('The event type ' + eventType + ' is not supported.');
    215  }
    216  return actions.send();
    217 }
    218 
    219 function requiresListener(eventType) {
    220  return ['mouseenter',
    221          'mouseleave',
    222          'pointerdown',
    223          'pointerenter',
    224          'pointerleave',
    225          'pointerout',
    226          'pointerover',
    227          'pointerup',
    228          'keyup',
    229          'keydown'
    230        ].includes(eventType);
    231 }
    232 
    233 function notCancelable(eventType) {
    234  return ['mouseenter', 'mouseleave', 'pointerenter', 'pointerleave'].includes(eventType);
    235 }
    236 
    237 // Tests the given |eventType|'s performance.eventCounts value. Since this is populated only when
    238 // the event is processed, we check every 10 ms until we've found the |expectedCount|.
    239 function testCounts(t, resolve, looseCount, eventType, expectedCount) {
    240  const counts = performance.eventCounts.get(eventType);
    241  if (counts < expectedCount) {
    242    t.step_timeout(() => {
    243      testCounts(t, resolve, looseCount, eventType, expectedCount);
    244    }, 10);
    245    return;
    246  }
    247  if (looseCount) {
    248    assert_greater_than_equal(performance.eventCounts.get(eventType), expectedCount,
    249        `Should have at least ${expectedCount} ${eventType} events`)
    250  } else {
    251    assert_equals(performance.eventCounts.get(eventType), expectedCount,
    252        `Should have ${expectedCount} ${eventType} events`);
    253  }
    254  resolve();
    255 }
    256 
    257 // Tests the given |eventType| by creating events whose target are the element with id
    258 // 'target'. The test assumes that such element already exists. |looseCount| is set for
    259 // eventTypes for which events would occur for other interactions other than the ones being
    260 // specified for the target, so the counts could be larger.
    261 async function testEventType(t, eventType, looseCount=false, targetId='target') {
    262  assert_implements(window.EventCounts, "Event Counts isn't supported");
    263  const target = document.getElementById(targetId);
    264  if (requiresListener(eventType)) {
    265    target.addEventListener(eventType, () =>{});
    266  }
    267  const initialCount = performance.eventCounts.get(eventType);
    268  if (!looseCount) {
    269    assert_equals(initialCount, 0, 'No events yet.');
    270  }
    271  // Trigger two 'fast' events of the type.
    272  await applyAction(eventType, target);
    273  await applyAction(eventType, target);
    274  await afterNextPaint();
    275  await new Promise(t.step_func(resolve => {
    276    testCounts(t, resolve, looseCount, eventType, initialCount + 2);
    277  }));
    278  // The durationThreshold used by the observer. A slow events needs to be slower than that.
    279  const durationThreshold = 16;
    280  // Now add an event handler to cause a slow event.
    281  target.addEventListener(eventType, () => {
    282    mainThreadBusy(durationThreshold + 4);
    283  });
    284  const observerPromise = new Promise(async resolve => {
    285    new PerformanceObserver(t.step_func(entryList => {
    286      let eventTypeEntries = entryList.getEntriesByName(eventType);
    287      if (eventTypeEntries.length === 0)
    288        return;
    289 
    290      let entry = null;
    291      if (!looseCount) {
    292        entry = eventTypeEntries[0];
    293        assert_equals(eventTypeEntries.length, 1);
    294      } else {
    295        // The other events could also be considered slow. Find the one with the correct
    296        // target.
    297        eventTypeEntries.forEach(e => {
    298          if (e.target === document.getElementById(targetId))
    299            entry = e;
    300        });
    301        if (!entry)
    302          return;
    303      }
    304      verifyEvent(entry,
    305                  eventType,
    306                  targetId,
    307                  false /* isFirst */,
    308                  durationThreshold,
    309                  notCancelable(eventType));
    310      // Shouldn't need async testing here since we already got the observer entry, but might as
    311      // well reuse the method.
    312      testCounts(t, resolve, looseCount, eventType, initialCount + 3);
    313    })).observe({type: 'event', durationThreshold: durationThreshold});
    314  });
    315  // Cause a slow event.
    316  await applyAction(eventType, target);
    317 
    318  await afterNextPaint();
    319 
    320  await observerPromise;
    321 }
    322 
    323 function addListeners(target, events) {
    324  const eventListener = (e) => {
    325    mainThreadBusy(200);
    326  };
    327  events.forEach(e => { target.addEventListener(e, eventListener); });
    328 }
    329 
    330 // The testdriver.js, testdriver-vendor.js and testdriver-actions.js need to be
    331 // included to use this function.
    332 async function tap(target) {
    333  return new test_driver.Actions()
    334    .addPointer("touchPointer", "touch")
    335    .pointerMove(0, 0, { origin: target })
    336    .pointerDown()
    337    .pointerUp()
    338    .send();
    339 }
    340 
    341 async function click(target) {
    342  return test_driver.click(target);
    343 }
    344 
    345 async function auxClick(target) {
    346  const actions = new test_driver.Actions();
    347  return actions.addPointer("mousePointer", "mouse")
    348    .pointerMove(0, 0, { origin: target })
    349    .pointerDown({ button: actions.ButtonType.RIGHT })
    350    .pointerUp({ button: actions.ButtonType.RIGHT })
    351    .send();
    352 }
    353 
    354 async function pointerdown(target) {
    355  const actions = new test_driver.Actions();
    356  return actions.addPointer("mousePointer", "mouse")
    357    .pointerMove(0, 0, { origin: target })
    358    .pointerDown()
    359    .send();
    360 }
    361 
    362 async function orphanPointerup(target) {
    363  const actions = new test_driver.Actions();
    364  await actions.addPointer("mousePointer", "mouse")
    365    .pointerMove(0, 0, { origin: target })
    366    .pointerUp()
    367    .send();
    368 
    369  // Orphan pointerup doesn't get triggered in some browsers. Sending a
    370  // non-pointer related event to make sure that at least an event gets handled.
    371  // If a browsers sends an orphan pointerup, it will always be before the
    372  // keydown, so the test will correctly handle it.
    373  await pressKey(target, 'a');
    374 }
    375 
    376 async function auxPointerdown(target) {
    377  const actions = new test_driver.Actions();
    378  return actions.addPointer("mousePointer", "mouse")
    379    .pointerMove(0, 0, { origin: target })
    380    .pointerDown({ button: actions.ButtonType.RIGHT })
    381    .send();
    382 }
    383 
    384 async function orphanAuxPointerup(target) {
    385  const actions = new test_driver.Actions();
    386  await actions.addPointer("mousePointer", "mouse")
    387    .pointerMove(0, 0, { origin: target })
    388    .pointerUp({ button: actions.ButtonType.RIGHT })
    389    .send();
    390 
    391  // Orphan pointerup doesn't get triggered in some browsers. Sending a
    392  // non-pointer related event to make sure that at least an event gets handled.
    393  // If a browsers sends an orphan pointerup, it will always be before the
    394  // keydown, so the test will correctly handle it.
    395  await pressKey(target, 'a');
    396 }
    397 
    398 // The testdriver.js, testdriver-vendor.js need to be included to use this
    399 // function.
    400 async function pressKey(target, key) {
    401  await test_driver.send_keys(target, key);
    402 }
    403 
    404 async function flingAndTapInTarget(target) {
    405  const actions = new test_driver.Actions();
    406  return actions.addPointer("pointer1", "touch")
    407        .pointerMove(0, 0, {origin: target})
    408        .pointerDown()
    409        .pointerMove(0, -50, {origin: target})
    410        .pointerMove(0, -50, {origin: target})
    411        .pointerUp()
    412        .pause(60)
    413        .pointerMove(0, 0, {origin: target})
    414        .pointerDown()
    415        .pointerUp()
    416        .send();
    417 }
    418 
    419 async function textSelectionInTarget(target) {
    420  const actions = new test_driver.Actions();
    421  return actions.addPointer("pointer1", "mouse")
    422        .pointerMove(0, 0, {origin: target})
    423        .pointerDown({button: actions.ButtonType.LEFT})
    424        .pointerMove(20, 60, {origin: target})
    425        .pointerMove(20, 120, {origin: target})
    426        .pointerUp()
    427        .send();
    428 }
    429 
    430 // The testdriver.js, testdriver-vendor.js need to be included to use this
    431 // function.
    432 async function addListenersAndPress(target, key, events) {
    433  addListeners(target, events);
    434  return pressKey(target, key);
    435 }
    436 
    437 // The testdriver.js, testdriver-vendor.js need to be included to use this
    438 // function.
    439 async function addListenersAndClick(target) {
    440  addListeners(target,
    441    ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']);
    442  return click(target);
    443 }
    444 
    445 function filterAndAddToMap(events, map) {
    446  return function (entry) {
    447    if (events.includes(entry.name)) {
    448      map.set(entry.name, entry.interactionId);
    449      return true;
    450    }
    451    return false;
    452  }
    453 }
    454 
    455 async function createPerformanceObserverPromise(observeTypes, callback, readyToResolve
    456 ) {
    457  return new Promise(resolve => {
    458    new PerformanceObserver(entryList => {
    459      callback(entryList);
    460 
    461      if (readyToResolve()) {
    462        resolve();
    463      }
    464    }).observe({ entryTypes: observeTypes });
    465  });
    466 }
    467 
    468 const ENTER_KEY = '\uE007';
    469 const SPACE_KEY = '\uE00D';
    470 
    471 async function blockPointerDownEventListener(target, duration, count) {
    472  return new Promise(resolve => {
    473    target.addEventListener("pointerdown", () => {
    474      event_count++;
    475      mainThreadBusy(duration);
    476      if (event_count == count)
    477        resolve();
    478    });
    479  });
    480 }
    481 
    482 async function blockCapturePointerDownEventListener(target, duration) {
    483  return new Promise(resolve => {
    484    target.addEventListener("pointerdown", (e) => {
    485      mainThreadBusy(duration);
    486      target.setPointerCapture(e.pointerId);
    487      resolve();
    488    });
    489  });
    490 }
    491 
    492 async function flingTapAndBlockMain(target, duration) {
    493  return Promise.all([
    494    blockPointerDownEventListener(target, duration, 2),
    495    blockNextEventListener(target, "pointercancel", duration),
    496    blockNextEventListener(target, "scroll", duration),
    497    flingAndTapInTarget(target),
    498  ]);
    499 }
    500 
    501 async function textSelectionAndBlockMain(target, duration) {
    502  return Promise.all([
    503    blockCapturePointerDownEventListener(target, "pointerdown", duration),
    504    blockNextEventListener(target, "pointermove", duration),
    505    blockNextEventListener(target, "scroll", duration),
    506    blockNextEventListener(target, "pointerup", 10),
    507    textSelectionInTarget(target),
    508    // afterNextPaint(),
    509  ]);
    510 }
    511 
    512 // The testdriver.js, testdriver-vendor.js need to be included to use this
    513 // function.
    514 async function interactAndObserve(interactionType, target, observerPromise, key = '') {
    515  let interactionPromise;
    516  switch (interactionType) {
    517    case 'key': {
    518      addListeners(target, ['keydown', 'keyup']);
    519      interactionPromise = pressKey(target, key);
    520    }
    521    case 'tap': {
    522      addListeners(target, ['pointerdown', 'pointerup']);
    523      interactionPromise = tap(target);
    524      break;
    525    }
    526    case 'click': {
    527      addListeners(target,
    528        ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click']);
    529      interactionPromise = click(target);
    530      break;
    531    }
    532    case 'auxclick': {
    533      addListeners(target,
    534        ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu', 'auxclick']);
    535      interactionPromise = auxClick(target);
    536      break;
    537    }
    538    case 'aux-pointerdown': {
    539      addListeners(target,
    540        ['mousedown', 'pointerdown', 'contextmenu']);
    541      interactionPromise = auxPointerdown(target);
    542      break;
    543    }
    544    case 'aux-pointerdown-and-pointerdown': {
    545      addListeners(target,
    546        ['mousedown', 'pointerdown', 'contextmenu']);
    547      interactionPromise = Promise.all([auxPointerdown(target), pointerdown(target)]);
    548      break;
    549    }
    550    case 'orphan-pointerup': {
    551      addListeners(target, ['pointerup', 'keydown']);
    552      interactionPromise = orphanPointerup(target);
    553      break;
    554    }
    555    case 'space-key-simulated-click': {
    556      addListeners(target, ['keydown', 'click']);
    557      interactionPromise = interact('key', target, SPACE_KEY);
    558      break;
    559    }
    560    case 'enter-key-simulated-click': {
    561      addListeners(target, ['keydown', 'click']);
    562      interactionPromise = interact('key', target, ENTER_KEY);
    563      break;
    564    }
    565    case 'fling-tap': {
    566      interactionPromise = flingTapAndBlockMain(target, 30);
    567      break;
    568    }
    569    case 'selection-scroll': {
    570      interactionPromise = textSelectionAndBlockMain(target, 30);
    571      break;
    572    }
    573    case 'orphan-keydown': {
    574      addListeners(target, ['keydown']);
    575      interactionPromise = new test_driver.Actions()
    576        .pointerMove(0, 0, {origin: target})
    577        .pointerDown()
    578        .pointerUp()
    579        .addTick()
    580        .keyDown('a')
    581        .send();
    582      break;
    583    }
    584  }
    585  return Promise.all([interactionPromise, observerPromise]);
    586 }
    587 
    588 async function interact(interactionType, element, key = '') {
    589  switch (interactionType) {
    590    case 'click': {
    591      return click(element);
    592    }
    593    case 'tap': {
    594      return tap(element);
    595    }
    596    case 'key': {
    597      return test_driver.send_keys(element, key);
    598    }
    599  }
    600 }
    601 
    602 async function verifyInteractionCount(t, expectedCount) {
    603  await t.step_wait(() => {
    604    return performance.interactionCount >= expectedCount;
    605  }, 'interactionCount did not increase enough', 10000, 5);
    606  assert_equals(performance.interactionCount, expectedCount,
    607    'interactionCount increased more than expected');
    608 }
    609 
    610 function interactionCount_test(interactionType, elements, key = '') {
    611  return promise_test(async t => {
    612    assert_implements(window.PerformanceEventTiming,
    613      'Event Timing is not supported');
    614    assert_equals(performance.interactionCount, 0, 'Initial count is not 0');
    615 
    616    let expectedCount = 1;
    617    for (let element of elements) {
    618      await interact(interactionType, element, key);
    619      await verifyInteractionCount(t, expectedCount++);
    620    }
    621  }, `EventTiming: verify interactionCount for ${interactionType} interaction`);
    622 }