Bug 1453010 - Part 4: Add test for locking highlighting. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Sat, 28 Apr 2018 10:48:23 +0900
changeset 789494 e3077a8ed374fee8953d7598d78cd284548451de
parent 788943 7f9cdf730364e77e53741e0b5abf2b5a4b6a7bc4
child 789495 06cea98a7190691d9b10d012b144814b0fe3eb23
push id108269
push userbmo:dakatsuka@mozilla.com
push dateSun, 29 Apr 2018 00:37:24 +0000
reviewersgl
bugs1453010
milestone61.0a1
Bug 1453010 - Part 4: Add test for locking highlighting. r?gl This patch depends on following PR. https://github.com/devtools-html/devtools-core/pull/1028 MozReview-Commit-ID: 5IAWzZ3YTyg
devtools/client/inspector/animation/components/AnimationTarget.js
devtools/client/inspector/animation/test/browser.ini
devtools/client/inspector/animation/test/browser_animation_animation-target.js
devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js
devtools/client/inspector/animation/test/head.js
--- a/devtools/client/inspector/animation/components/AnimationTarget.js
+++ b/devtools/client/inspector/animation/components/AnimationTarget.js
@@ -49,17 +49,17 @@ class AnimationTarget extends Component 
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.state.nodeFront !== nextState.nodeFront ||
            this.props.highlightedNode !== nextState.highlightedNode;
   }
 
   async updateNodeFront(animation) {
-    const { emitEventForTest, getNodeFromActor } = this.props;
+    const { getNodeFromActor } = this.props;
 
     // Try and get it from the playerFront directly.
     let nodeFront = animation.animationTargetNodeFront;
 
     // Next, get it from the walkerActor if it wasn't found.
     if (!nodeFront) {
       try {
         nodeFront = await getNodeFromActor(animation.actorID);
@@ -68,21 +68,21 @@ class AnimationTarget extends Component 
         // attributed to the panel having been destroyed in the meantime, this
         // error needs to be logged and render needs to stop.
         console.error(e);
         return;
       }
     }
 
     this.setState({ nodeFront });
-    emitEventForTest("animation-target-rendered");
   }
 
   render() {
     const {
+      emitEventForTest,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       highlightedNode,
       setHighlightedNode,
       setSelectedNode,
     } = this.props;
 
     const { nodeFront } = this.state;
@@ -90,16 +90,18 @@ class AnimationTarget extends Component 
     if (!nodeFront) {
       return dom.div(
         {
           className: "animation-target"
         }
       );
     }
 
+    emitEventForTest("animation-target-rendered");
+
     const isHighlighted = nodeFront.actorID === highlightedNode;
 
     return dom.div(
       {
         className: "animation-target" +
                    (isHighlighted ? " highlighting" : ""),
       },
       Rep(
--- a/devtools/client/inspector/animation/test/browser.ini
+++ b/devtools/client/inspector/animation/test/browser.ini
@@ -19,16 +19,17 @@ support-files =
 [browser_animation_animated-property-list.js]
 [browser_animation_animated-property-list_unchanged-items.js]
 [browser_animation_animated-property-name.js]
 [browser_animation_animation-detail_close-button.js]
 [browser_animation_animation-detail_title.js]
 [browser_animation_animation-detail_visibility.js]
 [browser_animation_animation-list.js]
 [browser_animation_animation-target.js]
+[browser_animation_animation-target_highlight.js]
 [browser_animation_animation-timeline-tick.js]
 [browser_animation_current-time-label.js]
 [browser_animation_current-time-scrubber.js]
 [browser_animation_empty_on_invalid_nodes.js]
 [browser_animation_inspector_exists.js]
 [browser_animation_keyframes-graph_computed-value-path.js]
 [browser_animation_keyframes-graph_computed-value-path_easing-hint.js]
 [browser_animation_keyframes-graph_keyframe-marker.js]
--- a/devtools/client/inspector/animation/test/browser_animation_animation-target.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target.js
@@ -2,34 +2,42 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test for following AnimationTarget component works.
 // * element existance
 // * number of elements
 // * content of element
+// * select an animated node by clicking on inspect node
+// * title of inspect icon
 
 add_task(async function() {
   await addTab(URL_ROOT + "doc_simple_animation.html");
   await removeAnimatedElementsExcept([".animated", ".long"]);
-  const { animationInspector, inspector, panel } = await openAnimationInspector();
+  const { animationInspector, panel } = await openAnimationInspector();
 
   info("Checking the animation target elements existance");
   const animationItemEls = panel.querySelectorAll(".animation-list .animation-item");
   is(animationItemEls.length, animationInspector.state.animations.length,
      "Number of animation target element should be same to number of animations " +
      "that displays");
 
   for (const animationItemEl of animationItemEls) {
     const animationTargetEl = animationItemEl.querySelector(".animation-target");
     ok(animationTargetEl,
       "The animation target element should be in each animation item element");
   }
 
+  info("Checking the selecting an animated node by clicking the target node");
+  await clickOnTargetNode(animationInspector, panel, 0);
+  is(panel.querySelectorAll(".animation-target").length, 1,
+    "The length of animations should be 1");
+
   info("Checking the content of animation target");
-  await selectNodeAndWaitForAnimations(".animated", inspector);
   const animationTargetEl =
     panel.querySelector(".animation-list .animation-item .animation-target");
   is(animationTargetEl.textContent, "div.ball.animated",
     "The target element's content is correct");
   ok(animationTargetEl.querySelector(".objectBox"), "objectBox is in the page exists");
+  ok(animationTargetEl.querySelector(".open-inspector").title,
+     INSPECTOR_L10N.getStr("inspector.nodePreview.highlightNodeLabel"));
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following highlighting related.
+// * highlight when mouse over on a target node
+// * unhighlight when mouse out from the above element
+// * lock highlighting when click on the inspect icon in animation target component
+// * add 'highlighting' class to animation target component during locking
+// * unlock highlighting when click on the above icon
+// * lock highlighting when click on the other inspect icon
+// * if the locked node has multi animations,
+//   the class will add to those animation target as well
+
+add_task(async function() {
+  await addTab(URL_ROOT + "doc_simple_animation.html");
+  await removeAnimatedElementsExcept([".animated", ".multi"]);
+  const { animationInspector, panel, toolbox } = await openAnimationInspector();
+
+  info("Check highlighting when mouse over on a target node");
+  let onHighlight = toolbox.once("node-highlight");
+  mouseOverOnTargetNode(animationInspector, panel, 0);
+  let nodeFront = await onHighlight;
+  assertNodeFront(nodeFront, "DIV", "ball animated");
+
+  info("Check unhighlighting when mouse out on a target node");
+  let onUnhighlight = toolbox.once("node-unhighlight");
+  mouseOutOnTargetNode(animationInspector, panel, 0);
+  await onUnhighlight;
+  ok(true, "Unhighlighted the targe node");
+
+  info("Check node is highlighted when the inspect icon is clicked");
+  onHighlight = toolbox.once("node-highlight");
+  await clickOnInspectIcon(animationInspector, panel, 0);
+  nodeFront = await onHighlight;
+  assertNodeFront(nodeFront, "DIV", "ball animated");
+  ok(panel.querySelectorAll(".animation-target")[0].classList.contains("highlighting"),
+    "The highlighted animation target element should have 'highlighting' class");
+
+  info("Check if the animation target is still highlighted on mouse out");
+  mouseOutOnTargetNode(animationInspector, panel, 0);
+  await wait(500);
+  ok(panel.querySelectorAll(".animation-target")[0].classList.contains("highlighting"),
+    "The highlighted element still should have 'highlighting' class");
+
+  info("Highlighting another animation target");
+  onHighlight = toolbox.once("node-highlight");
+  await clickOnInspectIcon(animationInspector, panel, 1);
+  nodeFront = await onHighlight;
+  assertNodeFront(nodeFront, "DIV", "ball multi");
+
+  info("Check the highlighted state of the animation targets");
+  const animationTargetEls = panel.querySelectorAll(".animation-target");
+  ok(!animationTargetEls[0].classList.contains("highlighting"),
+    "The animation target[0] should not have 'highlighting' class");
+  ok(animationTargetEls[1].classList.contains("highlighting"),
+    "The animation target[1] should have 'highlighting' class");
+  ok(animationTargetEls[2].classList.contains("highlighting"),
+    "The animation target[2] should have 'highlighting' class");
+});
+
+function assertNodeFront(nodeFront, tagName, classValue) {
+  is(nodeFront.tagName, "DIV",
+     "The highlighted node has the correct tagName");
+  is(nodeFront.attributes[0].name, "class",
+     "The highlighted node has the correct attributes");
+  is(nodeFront.attributes[0].value, classValue,
+     "The highlighted node has the correct class");
+}
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -93,97 +93,98 @@ addTab = async function(url) {
 const removeAnimatedElementsExcept = async function(selectors) {
   return executeInContent("Test:RemoveAnimatedElementsExcept", { selectors });
 };
 
 /**
  * Click on an animation in the timeline to select it.
  *
  * @param {AnimationInspector} animationInspector.
- * @param {AnimationsPanel} panel
- *        The panel instance.
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} index
  *        The index of the animation to click on.
  */
 const clickOnAnimation = async function(animationInspector, panel, index) {
   info("Click on animation " + index + " in the timeline");
   const summaryGraphEl = panel.querySelectorAll(".animation-summary-graph")[index];
   await clickOnSummaryGraph(animationInspector, panel, summaryGraphEl);
 };
 
 /**
  * Click on an animation by given selector of node which is target element of animation.
  *
  * @param {AnimationInspector} animationInspector.
- * @param {AnimationsPanel} panel
- *        The panel instance.
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {String} selector
  *        Selector of node which is target element of animation.
  */
 const clickOnAnimationByTargetSelector = async function(animationInspector,
                                                         panel, selector) {
   info(`Click on animation whose selector of target element is '${ selector }'`);
   const animationItemEl = findAnimationItemElementsByTargetSelector(panel, selector);
   const summaryGraphEl = animationItemEl.querySelector(".animation-summary-graph");
   await clickOnSummaryGraph(animationInspector, panel, summaryGraphEl);
 };
 
 /**
  * Click on close button for animation detail pane.
  *
- * @param {AnimationsPanel} panel
- *        The panel instance.
+ * @param {DOMElement} panel
+ *        #animation-container element.
  */
 const clickOnDetailCloseButton = function(panel) {
   info("Click on close button for animation detail pane");
   const buttonEl = panel.querySelector(".animation-detail-close-button");
   const bounds = buttonEl.getBoundingClientRect();
   const x = bounds.width / 2;
   const y = bounds.height / 2;
   EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
 };
 
 /**
  * Click on pause/resume button.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
- *        The panel instance.
+ * @param {DOMElement} panel
+ *        #animation-container element.
  */
 const clickOnPauseResumeButton = async function(animationInspector, panel) {
   info("Click on pause/resume button");
   const buttonEl = panel.querySelector(".pause-resume-button");
   const bounds = buttonEl.getBoundingClientRect();
   const x = bounds.width / 2;
   const y = bounds.height / 2;
   EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
  * Click on rewind button.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
- *        The panel instance.
+ * @param {DOMElement} panel
+ *        #animation-container element.
  */
 const clickOnRewindButton = async function(animationInspector, panel) {
   info("Click on rewind button");
   const buttonEl = panel.querySelector(".rewind-button");
   const bounds = buttonEl.getBoundingClientRect();
   const x = bounds.width / 2;
   const y = bounds.height / 2;
   EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
  * Click on the scrubber controller pane to update the animation current time.
  *
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} mouseDownPosition
  *        rate on scrubber controller pane.
  *        This method calculates
  *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
  *        as the clientX of MouseEvent.
  */
 const clickOnCurrentTimeScrubberController = async function(animationInspector,
                                                             panel,
@@ -194,20 +195,40 @@ const clickOnCurrentTimeScrubberControll
   const mousedonwX = bounds.width * mouseDownPosition;
 
   info(`Click ${ mousedonwX } on scrubber controller`);
   EventUtils.synthesizeMouse(controllerEl, mousedonwX, 0, {}, controllerEl.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
+ * Click on the inspect icon for the given AnimationTargetComponent.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ *        #animation-container element.
+ * @param {Number} index
+ *        The index of the AnimationTargetComponent to click on.
+ */
+const clickOnInspectIcon = async function(animationInspector, panel, index) {
+  info(`Click on an inspect icon in animation target component[${ index }]`);
+  const iconEl =
+    panel.querySelectorAll(".animation-target .objectBox .open-inspector")[index];
+  iconEl.scrollIntoView(false);
+  EventUtils.synthesizeMouseAtCenter(iconEl, {}, iconEl.ownerGlobal);
+  // We wait just one time, because the components are updated synchronously.
+  await animationInspector.once("animation-target-rendered");
+};
+
+/**
  * Click on playback rate selector to select given rate.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} rate
  */
 const clickOnPlaybackRateSelector = async function(animationInspector, panel, rate) {
   info(`Click on playback rate selector to select ${rate}`);
   const selectEl = panel.querySelector(".playback-rate-selector");
   const optionEl = [...selectEl.options].filter(o => Number(o.value) === rate)[0];
 
   if (!optionEl) {
@@ -221,35 +242,56 @@ const clickOnPlaybackRateSelector = asyn
   EventUtils.synthesizeMouseAtCenter(optionEl, { type: "mouseup" }, win);
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
  * Click on given summary graph element.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Element} summaryGraphEl
  */
 const clickOnSummaryGraph = async function(animationInspector, panel, summaryGraphEl) {
   // Disable pointer-events of the scrubber in order to avoid to click accidently.
   const scrubberEl = panel.querySelector(".current-time-scrubber");
   scrubberEl.style.pointerEvents = "none";
   // Scroll to show the timeBlock since the element may be out of displayed area.
   summaryGraphEl.scrollIntoView(false);
   EventUtils.synthesizeMouseAtCenter(summaryGraphEl, {}, summaryGraphEl.ownerGlobal);
   await waitForAnimationDetail(animationInspector);
   // Restore the scrubber style.
   scrubberEl.style.pointerEvents = "unset";
 };
 
 /**
+ * Click on the target node for the given AnimationTargetComponent index.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ *        #animation-container element.
+ * @param {Number} index
+ *        The index of the AnimationTargetComponent to click on.
+ */
+const clickOnTargetNode = async function(animationInspector, panel, index) {
+  info(`Click on a target node in animation target component[${ index }]`);
+  const targetEl = panel.querySelectorAll(".animation-target .objectBox")[index];
+  targetEl.scrollIntoView(false);
+  const onHighlight = animationInspector.inspector.toolbox.once("node-highlight");
+  EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal);
+  await waitForRendering(animationInspector);
+  await onHighlight;
+};
+
+/**
  * Drag on the scrubber to update the animation current time.
  *
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} mouseDownPosition
  *        rate on scrubber controller pane.
  *        This method calculates
  *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
  *        as the clientX of MouseEvent.
  * @param {Number} mouseMovePosition
  *        Dispatch mousemove event with mouseMovePosition after mousedown.
  *        Calculation for clinetX is same to above.
@@ -275,17 +317,18 @@ const dragOnCurrentTimeScrubber = async 
   EventUtils.synthesizeMouse(controllerEl, mousemoveX, mouseYPixel,
                              { type: "mouseup" }, controllerEl.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
  * Drag on the scrubber controller pane to update the animation current time.
  *
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} mouseDownPosition
  *        rate on scrubber controller pane.
  *        This method calculates
  *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
  *        as the clientX of MouseEvent.
  * @param {Number} mouseMovePosition
  *        Dispatch mousemove event with mouseMovePosition after mousedown.
  *        Calculation for clinetX is same to above.
@@ -310,17 +353,18 @@ const dragOnCurrentTimeScrubberControlle
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
  * Get current animation duration and rate of
  * clickOrDragOnCurrentTimeScrubberController in given pixels.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} pixels
  * @return {Object}
  *         {
  *           duration,
  *           rate,
  *         }
  */
 const getDurationAndRate = function(animationInspector, panel, pixels) {
@@ -328,16 +372,48 @@ const getDurationAndRate = function(anim
   const bounds = controllerEl.getBoundingClientRect();
   const duration =
     animationInspector.state.timeScale.getDuration() / bounds.width * pixels;
   const rate = 1 / bounds.width * pixels;
   return { duration, rate };
 };
 
 /**
+ * Mouse over the target node for the given AnimationTargetComponent index.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ *        #animation-container element.
+ * @param {Number} index
+ *        The index of the AnimationTargetComponent to click on.
+ */
+const mouseOverOnTargetNode = function(animationInspector, panel, index) {
+  info(`Mouse over on a target node in animation target component[${ index }]`);
+  const el = panel.querySelectorAll(".animation-target .objectBox")[index];
+  el.scrollIntoView(false);
+  EventUtils.synthesizeMouse(el, 10, 5, { type: "mouseover" }, el.ownerGlobal);
+};
+
+/**
+ * Mouse out of the target node for the given AnimationTargetComponent index.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ *        #animation-container element.
+ * @param {Number} index
+ *        The index of the AnimationTargetComponent to click on.
+ */
+const mouseOutOnTargetNode = function(animationInspector, panel, index) {
+  info(`Mouse out on a target node in animation target component[${ index }]`);
+  const el = panel.querySelectorAll(".animation-target .objectBox")[index];
+  el.scrollIntoView(false);
+  EventUtils.synthesizeMouse(el, -1, -1, { type: "mouseout" }, el.ownerGlobal);
+};
+
+/**
  * Select animation inspector in sidebar and toolbar.
  *
  * @param {InspectorPanel} inspector
  */
 const selectAnimationInspector = async function(inspector) {
   await inspector.toolbox.selectTool("inspector");
   const onUpdated = inspector.once("inspector-updated");
   inspector.sidebar.select("newanimationinspector");
@@ -367,17 +443,18 @@ const selectNodeAndWaitForAnimations = a
   await onUpdated;
   await waitForRendering(inspector.animationinspector);
 };
 
 /**
  * Send keyboard event of space to given panel.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  */
 const sendSpaceKeyEvent = async function(animationInspector, panel) {
   panel.focus();
   EventUtils.sendKey("SPACE", panel.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
 };
 
 /**
@@ -494,40 +571,43 @@ const waitForSummaryAndDetail = async fu
     waitForAnimationDetail(animationInspector),
   ]);
 };
 
 /**
  * Check whether current time of all animations and UI are given specified time.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {Number} time
  */
 function assertAnimationsCurrentTime(animationInspector, time) {
   const isTimeEqual =
     animationInspector.state.animations.every(({state}) => state.currentTime === time);
   ok(isTimeEqual, `Current time of animations should be ${ time }`);
 }
 
 /**
  * Check whether the animations are pausing.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  */
 function assertAnimationsPausing(animationInspector, panel) {
   assertAnimationsPausingOrRunning(animationInspector, panel, true);
 }
 
 /**
  * Check whether the animations are pausing/running.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  * @param {boolean} shouldPause
  */
 function assertAnimationsPausingOrRunning(animationInspector, panel, shouldPause) {
   const hasRunningAnimation =
     animationInspector.state.animations.some(({state}) => state.playState === "running");
 
   if (shouldPause) {
     is(hasRunningAnimation, false, "All animations should be paused");
@@ -535,17 +615,18 @@ function assertAnimationsPausingOrRunnin
     is(hasRunningAnimation, true, "Animations should be running at least one");
   }
 }
 
 /**
  * Check whether the animations are running.
  *
  * @param {AnimationInspector} animationInspector
- * @param {AnimationsPanel} panel
+ * @param {DOMElement} panel
+ *        #animation-container element.
  */
 function assertAnimationsRunning(animationInspector, panel) {
   assertAnimationsPausingOrRunning(animationInspector, panel, false);
 }
 
 /**
  * Check the <stop> element in the given linearGradientEl for the correct offset
  * and color attributes.
@@ -628,19 +709,22 @@ function isPassingThrough(pathSegList, x
   return false;
 }
 
 /**
  * Return animation item element by target node selector.
  * This function compares betweem animation-target textContent and given selector.
  * Then returns matched first item.
  *
- * @param {Element} panel - root element of animation inspector.
- * @param {String} selector - selector of tested element.
- * @return {Element} animation item element.
+ * @param {DOMElement} panel
+ *        #animation-container element.
+ * @param {String} selector
+ *        Selector of tested element.
+ * @return {DOMElement}
+ *        Animation item element.
  */
 function findAnimationItemElementsByTargetSelector(panel, selector) {
   const attrNameEls = panel.querySelectorAll(".animation-target .attrName");
   const regexp = new RegExp(`\\${ selector }(\\.|$)`, "gi");
 
   for (const attrNameEl of attrNameEls) {
     if (regexp.exec(attrNameEl.textContent)) {
       return attrNameEl.closest(".animation-item");