head.js (32161B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */ 4 5 "use strict"; 6 7 // Import the inspector's head.js first (which itself imports shared-head.js). 8 Services.scriptloader.loadSubScript( 9 "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", 10 this 11 ); 12 13 const TAB_NAME = "animationinspector"; 14 15 const ANIMATION_L10N = new LocalizationHelper( 16 "devtools/client/locales/animationinspector.properties" 17 ); 18 19 /** 20 * Open the toolbox, with the inspector tool visible and the animationinspector 21 * sidebar selected. 22 * 23 * @return {Promise} that resolves when the inspector is ready. 24 */ 25 const openAnimationInspector = async function () { 26 const { inspector, toolbox } = await openInspectorSidebarTab(TAB_NAME); 27 await inspector.once("inspector-updated"); 28 const animationInspector = inspector.getPanel("animationinspector"); 29 const panel = inspector.panelWin.document.getElementById( 30 "animation-container" 31 ); 32 33 info("Wait for loading first content"); 34 const count = getDisplayedGraphCount(animationInspector, panel); 35 await waitUntil( 36 () => 37 panel.querySelectorAll(".animation-summary-graph-path").length >= count && 38 panel.querySelectorAll(".animation-target .objectBox").length >= count 39 ); 40 41 if ( 42 animationInspector.state.selectedAnimation && 43 animationInspector.state.detailVisibility 44 ) { 45 await waitUntil(() => panel.querySelector(".animated-property-list")); 46 } 47 48 return { animationInspector, toolbox, inspector, panel }; 49 }; 50 51 /** 52 * Close the toolbox. 53 * 54 * @return {Promise} that resolves when the toolbox has closed. 55 */ 56 const closeAnimationInspector = async function () { 57 return gDevTools.closeToolboxForTab(gBrowser.selectedTab); 58 }; 59 60 /** 61 * Remove animated elements from document except given selectors. 62 * 63 * @param {Array} selectors 64 * @return {Promise} 65 */ 66 const removeAnimatedElementsExcept = function (selectors) { 67 return SpecialPowers.spawn( 68 gBrowser.selectedBrowser, 69 [selectors], 70 selectorsChild => { 71 function isRemovableElement(animation, selectorsInner) { 72 for (const selector of selectorsInner) { 73 if (animation.effect.target.matches(selector)) { 74 return false; 75 } 76 } 77 78 return true; 79 } 80 81 for (const animation of content.document.getAnimations()) { 82 if (isRemovableElement(animation, selectorsChild)) { 83 animation.effect.target.remove(); 84 } 85 } 86 } 87 ); 88 }; 89 90 /** 91 * Click on an animation in the timeline to select it. 92 * 93 * @param {AnimationInspector} animationInspector. 94 * @param {DOMElement} panel 95 * #animation-container element. 96 * @param {number} index 97 * The index of the animation to click on. 98 */ 99 const clickOnAnimation = async function (animationInspector, panel, index) { 100 info("Click on animation " + index + " in the timeline"); 101 const animationItemEl = await findAnimationItemByIndex(panel, index); 102 const summaryGraphEl = animationItemEl.querySelector( 103 ".animation-summary-graph" 104 ); 105 clickOnSummaryGraph(animationInspector, panel, summaryGraphEl); 106 }; 107 108 /** 109 * Click on an animation by given selector of node which is target element of animation. 110 * 111 * @param {AnimationInspector} animationInspector. 112 * @param {DOMElement} panel 113 * #animation-container element. 114 * @param {string} selector 115 * Selector of node which is target element of animation. 116 */ 117 const clickOnAnimationByTargetSelector = async function ( 118 animationInspector, 119 panel, 120 selector 121 ) { 122 info(`Click on animation whose selector of target element is '${selector}'`); 123 const animationItemEl = await findAnimationItemByTargetSelector( 124 panel, 125 selector 126 ); 127 const summaryGraphEl = animationItemEl.querySelector( 128 ".animation-summary-graph" 129 ); 130 clickOnSummaryGraph(animationInspector, panel, summaryGraphEl); 131 }; 132 133 /** 134 * Click on close button for animation detail pane. 135 * 136 * @param {DOMElement} panel 137 * #animation-container element. 138 */ 139 const clickOnDetailCloseButton = function (panel) { 140 info("Click on close button for animation detail pane"); 141 const buttonEl = panel.querySelector(".animation-detail-close-button"); 142 const bounds = buttonEl.getBoundingClientRect(); 143 const x = bounds.width / 2; 144 const y = bounds.height / 2; 145 EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal); 146 }; 147 148 /** 149 * Click on pause/resume button. 150 * 151 * @param {AnimationInspector} animationInspector 152 * @param {DOMElement} panel 153 * #animation-container element. 154 */ 155 const clickOnPauseResumeButton = function (animationInspector, panel) { 156 info("Click on pause/resume button"); 157 const buttonEl = panel.querySelector(".pause-resume-button"); 158 const bounds = buttonEl.getBoundingClientRect(); 159 const x = bounds.width / 2; 160 const y = bounds.height / 2; 161 EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal); 162 }; 163 164 /** 165 * Click on rewind button. 166 * 167 * @param {AnimationInspector} animationInspector 168 * @param {DOMElement} panel 169 * #animation-container element. 170 */ 171 const clickOnRewindButton = function (animationInspector, panel) { 172 info("Click on rewind button"); 173 const buttonEl = panel.querySelector(".rewind-button"); 174 const bounds = buttonEl.getBoundingClientRect(); 175 const x = bounds.width / 2; 176 const y = bounds.height / 2; 177 EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal); 178 }; 179 180 /** 181 * Click on the scrubber controller pane to update the animation current time. 182 * 183 * @param {DOMElement} panel 184 * #animation-container element. 185 * @param {number} mouseDownPosition 186 * rate on scrubber controller pane. 187 * This method calculates 188 * `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane` 189 * as the clientX of MouseEvent. 190 */ 191 const clickOnCurrentTimeScrubberController = function ( 192 animationInspector, 193 panel, 194 mouseDownPosition 195 ) { 196 const controllerEl = panel.querySelector(".current-time-scrubber-area"); 197 const bounds = controllerEl.getBoundingClientRect(); 198 const mousedonwX = bounds.width * mouseDownPosition; 199 200 info(`Click ${mousedonwX} on scrubber controller`); 201 EventUtils.synthesizeMouse( 202 controllerEl, 203 mousedonwX, 204 0, 205 {}, 206 controllerEl.ownerGlobal 207 ); 208 }; 209 210 /** 211 * Click on the inspect icon for the given AnimationTargetComponent. 212 * 213 * @param {AnimationInspector} animationInspector. 214 * @param {DOMElement} panel 215 * #animation-container element. 216 * @param {number} index 217 * The index of the AnimationTargetComponent to click on. 218 */ 219 const clickOnInspectIcon = async function (animationInspector, panel, index) { 220 info(`Click on an inspect icon in animation target component[${index}]`); 221 const animationItemEl = await findAnimationItemByIndex(panel, index); 222 const iconEl = animationItemEl.querySelector( 223 ".animation-target .objectBox .highlight-node" 224 ); 225 iconEl.scrollIntoView(false); 226 // Use click instead of EventUtils.synthesizeMouseAtCenter because the latter 227 // seems to trigger additional events (e.g. mouseover) that might interfere with tests 228 iconEl.click(); 229 }; 230 231 /** 232 * Change playback rate selector to select given rate. 233 * 234 * @param {AnimationInspector} animationInspector 235 * @param {DOMElement} panel 236 * #animation-container element. 237 * @param {number} rate 238 */ 239 const changePlaybackRateSelector = async function ( 240 animationInspector, 241 panel, 242 rate 243 ) { 244 info(`Click on playback rate selector to select ${rate}`); 245 const selectEl = panel.querySelector(".playback-rate-selector"); 246 const optionIndex = [...selectEl.options].findIndex(o => +o.value == rate); 247 248 if (optionIndex == -1) { 249 ok( 250 false, 251 `Could not find an option for rate ${rate} in the rate selector. ` + 252 `Values are: ${[...selectEl.options].map(o => o.value)}` 253 ); 254 return; 255 } 256 257 selectEl.focus(); 258 259 const win = selectEl.ownerGlobal; 260 while (selectEl.selectedIndex != optionIndex) { 261 const key = selectEl.selectedIndex > optionIndex ? "LEFT" : "RIGHT"; 262 EventUtils.sendKey(key, win); 263 } 264 }; 265 266 /** 267 * Click on given summary graph element. 268 * 269 * @param {AnimationInspector} animationInspector 270 * @param {DOMElement} panel 271 * #animation-container element. 272 * @param {Element} summaryGraphEl 273 */ 274 const clickOnSummaryGraph = function ( 275 animationInspector, 276 panel, 277 summaryGraphEl 278 ) { 279 // Disable pointer-events of the scrubber in order to avoid to click accidently. 280 const scrubberEl = panel.querySelector(".current-time-scrubber"); 281 scrubberEl.style.pointerEvents = "none"; 282 // Scroll to show the timeBlock since the element may be out of displayed area. 283 summaryGraphEl.scrollIntoView(false); 284 EventUtils.synthesizeMouseAtCenter( 285 summaryGraphEl, 286 {}, 287 summaryGraphEl.ownerGlobal 288 ); 289 // Restore the scrubber style. 290 scrubberEl.style.pointerEvents = "unset"; 291 }; 292 293 /** 294 * Click on the target node for the given AnimationTargetComponent index. 295 * 296 * @param {AnimationInspector} animationInspector. 297 * @param {DOMElement} panel 298 * #animation-container element. 299 * @param {number} index 300 * The index of the AnimationTargetComponent to click on. 301 */ 302 const clickOnTargetNode = async function (animationInspector, panel, index) { 303 const { inspector } = animationInspector; 304 const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); 305 info(`Click on a target node in animation target component[${index}]`); 306 307 const animationItemEl = await findAnimationItemByIndex(panel, index); 308 const targetEl = animationItemEl.querySelector( 309 ".animation-target .objectBox" 310 ); 311 const onHighlight = waitForHighlighterTypeShown( 312 inspector.highlighters.TYPES.BOXMODEL 313 ); 314 EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal); 315 await onHighlight; 316 }; 317 318 /** 319 * Click on the target node for the given AnimationTargetComponent target element text 320 * 321 * @param {AnimationInspector} animationInspector. 322 * @param {DOMElement} panel 323 * #animation-container element. 324 * @param {string} targetText 325 * text displayed to represent the animation target in the panel. 326 */ 327 const clickOnTargetNodeByTargetText = async function ( 328 animationInspector, 329 panel, 330 targetText 331 ) { 332 const { inspector } = animationInspector; 333 const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); 334 info(`Click on a target node in animation target "${targetText}"`); 335 336 const animationItemEl = await findAnimationItemByTargetText( 337 panel, 338 targetText 339 ); 340 if (!animationItemEl) { 341 throw new Error(`Couln't find target "${targetText}"`); 342 } 343 const targetEl = animationItemEl.querySelector( 344 ".animation-target .objectBox" 345 ); 346 const onHighlight = waitForHighlighterTypeShown( 347 inspector.highlighters.TYPES.BOXMODEL 348 ); 349 EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal); 350 await onHighlight; 351 }; 352 353 /** 354 * Drag on the scrubber to update the animation current time. 355 * 356 * @param {DOMElement} panel 357 * #animation-container element. 358 * @param {number} mouseMovePixel 359 * Dispatch mousemove event with mouseMovePosition after mousedown. 360 * @param {number} mouseYPixel 361 * Y of mouse in pixel. 362 */ 363 const dragOnCurrentTimeScrubber = async function ( 364 animationInspector, 365 panel, 366 mouseMovePixel, 367 mouseYPixel 368 ) { 369 const controllerEl = panel.querySelector(".current-time-scrubber"); 370 info(`Drag scrubber to X ${mouseMovePixel}`); 371 EventUtils.synthesizeMouse( 372 controllerEl, 373 0, 374 mouseYPixel, 375 { type: "mousedown" }, 376 controllerEl.ownerGlobal 377 ); 378 await waitUntilAnimationsPlayState(animationInspector, "paused"); 379 380 const animation = animationInspector.state.animations[0]; 381 let currentTime = animation.state.currentTime; 382 EventUtils.synthesizeMouse( 383 controllerEl, 384 mouseMovePixel, 385 mouseYPixel, 386 { type: "mousemove" }, 387 controllerEl.ownerGlobal 388 ); 389 await waitUntil(() => animation.state.currentTime !== currentTime); 390 391 currentTime = animation.state.currentTime; 392 EventUtils.synthesizeMouse( 393 controllerEl, 394 mouseMovePixel, 395 mouseYPixel, 396 { type: "mouseup" }, 397 controllerEl.ownerGlobal 398 ); 399 await waitUntil(() => animation.state.currentTime !== currentTime); 400 }; 401 402 /** 403 * Drag on the scrubber controller pane to update the animation current time. 404 * 405 * @param {DOMElement} panel 406 * #animation-container element. 407 * @param {number} mouseDownPosition 408 * rate on scrubber controller pane. 409 * This method calculates 410 * `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane` 411 * as the clientX of MouseEvent. 412 * @param {number} mouseMovePosition 413 * Dispatch mousemove event with mouseMovePosition after mousedown. 414 * Calculation for clinetX is same to above. 415 */ 416 const dragOnCurrentTimeScrubberController = async function ( 417 animationInspector, 418 panel, 419 mouseDownPosition, 420 mouseMovePosition 421 ) { 422 const controllerEl = panel.querySelector(".current-time-scrubber-area"); 423 const bounds = controllerEl.getBoundingClientRect(); 424 const mousedonwX = bounds.width * mouseDownPosition; 425 const mousemoveX = bounds.width * mouseMovePosition; 426 427 info(`Drag on scrubber controller from ${mousedonwX} to ${mousemoveX}`); 428 EventUtils.synthesizeMouse( 429 controllerEl, 430 mousedonwX, 431 0, 432 { type: "mousedown" }, 433 controllerEl.ownerGlobal 434 ); 435 await waitUntilAnimationsPlayState(animationInspector, "paused"); 436 437 const animation = animationInspector.state.animations[0]; 438 let currentTime = animation.state.currentTime; 439 EventUtils.synthesizeMouse( 440 controllerEl, 441 mousemoveX, 442 0, 443 { type: "mousemove" }, 444 controllerEl.ownerGlobal 445 ); 446 await waitUntil(() => animation.state.currentTime !== currentTime); 447 448 currentTime = animation.state.currentTime; 449 EventUtils.synthesizeMouse( 450 controllerEl, 451 mousemoveX, 452 0, 453 { type: "mouseup" }, 454 controllerEl.ownerGlobal 455 ); 456 await waitUntil(() => animation.state.currentTime !== currentTime); 457 }; 458 459 /** 460 * Get current animation duration and rate of 461 * clickOrDragOnCurrentTimeScrubberController in given pixels. 462 * 463 * @param {AnimationInspector} animationInspector 464 * @param {DOMElement} panel 465 * #animation-container element. 466 * @param {number} pixels 467 * @return {object} 468 * { 469 * duration, 470 * rate, 471 * } 472 */ 473 const getDurationAndRate = function (animationInspector, panel, pixels) { 474 const controllerEl = panel.querySelector(".current-time-scrubber-area"); 475 const bounds = controllerEl.getBoundingClientRect(); 476 const duration = 477 (animationInspector.state.timeScale.getDuration() / bounds.width) * pixels; 478 const rate = (1 / bounds.width) * pixels; 479 return { duration, rate }; 480 }; 481 482 /** 483 * Mouse over the target node for the given AnimationTargetComponent index. 484 * 485 * @param {AnimationInspector} animationInspector. 486 * @param {DOMElement} panel 487 * #animation-container element. 488 * @param {number} index 489 * The index of the AnimationTargetComponent to click on. 490 */ 491 const mouseOverOnTargetNode = function (animationInspector, panel, index) { 492 info(`Mouse over on a target node in animation target component[${index}]`); 493 const el = panel.querySelectorAll(".animation-target .objectBox")[index]; 494 el.scrollIntoView(false); 495 EventUtils.synthesizeMouse(el, 10, 5, { type: "mouseover" }, el.ownerGlobal); 496 }; 497 498 /** 499 * Mouse out of the target node for the given AnimationTargetComponent index. 500 * 501 * @param {AnimationInspector} animationInspector. 502 * @param {DOMElement} panel 503 * #animation-container element. 504 * @param {number} index 505 * The index of the AnimationTargetComponent to click on. 506 */ 507 const mouseOutOnTargetNode = function (animationInspector, panel, index) { 508 info(`Mouse out on a target node in animation target component[${index}]`); 509 const el = panel.querySelectorAll(".animation-target .objectBox")[index]; 510 el.scrollIntoView(false); 511 EventUtils.synthesizeMouse(el, -1, -1, { type: "mouseout" }, el.ownerGlobal); 512 }; 513 514 /** 515 * Select animation inspector in sidebar and toolbar. 516 * 517 * @param {InspectorPanel} inspector 518 */ 519 const selectAnimationInspector = async function (inspector) { 520 await inspector.toolbox.selectTool("inspector"); 521 const onDispatched = waitForDispatch(inspector.store, "UPDATE_ANIMATIONS"); 522 inspector.sidebar.select("animationinspector"); 523 await onDispatched; 524 }; 525 526 /** 527 * Send keyboard event of space to given panel. 528 * 529 * @param {AnimationInspector} animationInspector 530 * @param {DOMElement} target element. 531 */ 532 const sendSpaceKeyEvent = function (animationInspector, element) { 533 element.focus(); 534 EventUtils.sendKey("SPACE", element.ownerGlobal); 535 }; 536 537 /** 538 * Set a node class attribute to the given selector. 539 * 540 * @param {AnimationInspector} animationInspector 541 * @param {string} selector 542 * @param {string} cls 543 * e.g. ".ball.still" 544 */ 545 const setClassAttribute = async function (animationInspector, selector, cls) { 546 await SpecialPowers.spawn( 547 gBrowser.selectedBrowser, 548 [cls, selector], 549 (attributeValue, selectorChild) => { 550 const node = content.document.querySelector(selectorChild); 551 if (!node) { 552 return; 553 } 554 555 node.setAttribute("class", attributeValue); 556 } 557 ); 558 }; 559 560 /** 561 * Set a new style properties to the node for the given selector. 562 * 563 * @param {AnimationInspector} animationInspector 564 * @param {string} selector 565 * @param {object} properties 566 * e.g. { 567 * animationDuration: "1000ms", 568 * animationTimingFunction: "linear", 569 * } 570 */ 571 const setEffectTimingAndPlayback = async function ( 572 animationInspector, 573 selector, 574 effectTiming, 575 playbackRate 576 ) { 577 await SpecialPowers.spawn( 578 gBrowser.selectedBrowser, 579 [selector, playbackRate, effectTiming], 580 (selectorChild, playbackRateChild, effectTimingChild) => { 581 let selectedAnimation = null; 582 583 for (const animation of content.document.getAnimations()) { 584 if (animation.effect.target.matches(selectorChild)) { 585 selectedAnimation = animation; 586 break; 587 } 588 } 589 590 if (!selectedAnimation) { 591 return; 592 } 593 594 selectedAnimation.playbackRate = playbackRateChild; 595 selectedAnimation.effect.updateTiming(effectTimingChild); 596 } 597 ); 598 }; 599 600 /** 601 * Set the sidebar width by given parameter. 602 * 603 * @param {string} width 604 * Change sidebar width by given parameter. 605 * @param {InspectorPanel} inspector 606 * The instance of InspectorPanel currently loaded in the toolbox 607 * @return {Promise} Resolves when the sidebar size changed. 608 */ 609 const setSidebarWidth = async function (width, inspector) { 610 const onUpdated = inspector.toolbox.once("inspector-sidebar-resized"); 611 inspector.splitBox.setState({ width }); 612 await onUpdated; 613 }; 614 615 /** 616 * Set a new style property declaration to the node for the given selector. 617 * 618 * @param {AnimationInspector} animationInspector 619 * @param {string} selector 620 * @param {string} propertyName 621 * e.g. "animationDuration" 622 * @param {string} propertyValue 623 * e.g. "5.5s" 624 */ 625 const setStyle = async function ( 626 animationInspector, 627 selector, 628 propertyName, 629 propertyValue 630 ) { 631 await SpecialPowers.spawn( 632 gBrowser.selectedBrowser, 633 [selector, propertyName, propertyValue], 634 (selectorChild, propertyNameChild, propertyValueChild) => { 635 const node = content.document.querySelector(selectorChild); 636 if (!node) { 637 return; 638 } 639 640 node.style[propertyNameChild] = propertyValueChild; 641 } 642 ); 643 }; 644 645 /** 646 * Set a new style properties to the node for the given selector. 647 * 648 * @param {AnimationInspector} animationInspector 649 * @param {string} selector 650 * @param {object} properties 651 * e.g. { 652 * animationDuration: "1000ms", 653 * animationTimingFunction: "linear", 654 * } 655 */ 656 const setStyles = async function (animationInspector, selector, properties) { 657 await SpecialPowers.spawn( 658 gBrowser.selectedBrowser, 659 [properties, selector], 660 (propertiesChild, selectorChild) => { 661 const node = content.document.querySelector(selectorChild); 662 if (!node) { 663 return; 664 } 665 666 for (const propertyName in propertiesChild) { 667 const propertyValue = propertiesChild[propertyName]; 668 node.style[propertyName] = propertyValue; 669 } 670 } 671 ); 672 }; 673 674 /** 675 * Wait until current time of animations will be changed to given current time. 676 * 677 * @param {AnimationInspector} animationInspector 678 * @param {number} currentTime 679 */ 680 const waitUntilCurrentTimeChangedAt = async function ( 681 animationInspector, 682 currentTime 683 ) { 684 info(`Wait until current time will be change to ${currentTime}`); 685 await waitUntil(() => 686 animationInspector.state.animations.every(a => { 687 return a.state.currentTime === currentTime; 688 }) 689 ); 690 }; 691 692 /** 693 * Wait until animations' play state will be changed to given state. 694 * 695 * @param {Array} animationInspector 696 * @param {string} state 697 */ 698 const waitUntilAnimationsPlayState = async function ( 699 animationInspector, 700 state 701 ) { 702 info(`Wait until play state will be change to ${state}`); 703 await waitUntil(() => 704 animationInspector.state.animations.every(a => a.state.playState === state) 705 ); 706 }; 707 708 /** 709 * Return count of graph that animation inspector is displaying. 710 * 711 * @param {AnimationInspector} animationInspector 712 * @param {DOMElement} panel 713 * @return {number} count 714 */ 715 const getDisplayedGraphCount = (animationInspector, panel) => { 716 const animationLength = animationInspector.state.animations.length; 717 if (animationLength === 0) { 718 return 0; 719 } 720 721 const inspectionPanelEl = panel.querySelector(".progress-inspection-panel"); 722 const itemEl = panel.querySelector(".animation-item"); 723 const listEl = panel.querySelector(".animation-list"); 724 const itemHeight = itemEl.offsetHeight; 725 // This calculation should be same as AnimationListContainer.updateDisplayableRange. 726 const count = Math.floor(listEl.offsetHeight / itemHeight) + 1; 727 const index = Math.floor(inspectionPanelEl.scrollTop / itemHeight); 728 729 return animationLength > index + count ? count : animationLength - index; 730 }; 731 732 /** 733 * Check whether the animations are pausing. 734 * 735 * @param {AnimationInspector} animationInspector 736 */ 737 function assertAnimationsPausing(animationInspector) { 738 assertAnimationsPausingOrRunning(animationInspector, true); 739 } 740 741 /** 742 * Check whether the animations are pausing/running. 743 * 744 * @param {AnimationInspector} animationInspector 745 * @param {boolean} shouldPause 746 */ 747 function assertAnimationsPausingOrRunning(animationInspector, shouldPause) { 748 const hasRunningAnimation = animationInspector.state.animations.some( 749 ({ state }) => state.playState === "running" 750 ); 751 752 if (shouldPause) { 753 is(hasRunningAnimation, false, "All animations should be paused"); 754 } else { 755 is(hasRunningAnimation, true, "Animations should be running at least one"); 756 } 757 } 758 759 /** 760 * Check whether the animations are running. 761 * 762 * @param {AnimationInspector} animationInspector 763 */ 764 function assertAnimationsRunning(animationInspector) { 765 assertAnimationsPausingOrRunning(animationInspector, false); 766 } 767 768 /** 769 * Check the <stop> element in the given linearGradientEl for the correct offset 770 * and color attributes. 771 * 772 * @param {Element} linearGradientEl 773 <linearGradient> element which has <stop> element. 774 * @param {number} offset 775 * float which represents the "offset" attribute of <stop>. 776 * @param {string} expectedColor 777 * e.g. rgb(0, 0, 255) 778 */ 779 function assertLinearGradient(linearGradientEl, offset, expectedColor) { 780 const stopEl = findStopElement(linearGradientEl, offset); 781 ok(stopEl, `stop element at offset ${offset} should exist`); 782 is( 783 stopEl.getAttribute("stop-color"), 784 expectedColor, 785 `stop-color of stop element at offset ${offset} should be ${expectedColor}` 786 ); 787 } 788 789 /** 790 * SummaryGraph is constructed by <path> element. 791 * This function checks the vertex of path segments. 792 * 793 * @param {Element} pathEl 794 * <path> element. 795 * @param {boolean} hasClosePath 796 * Set true if the path shoud be closing. 797 * @param {object} expectedValues 798 * JSON object format. We can test the vertex and color. 799 * e.g. 800 * [ 801 * { x: 0, y: 0 }, 802 * { x: 0, y: 1 }, 803 * ] 804 */ 805 function assertPathSegments(pathEl, hasClosePath, expectedValues) { 806 const pathData = pathEl.getPathData({ normalize: true }); 807 ok( 808 expectedValues.every(value => isPassingThrough(pathData, value.x, value.y)), 809 "unexpected path segment vertices" 810 ); 811 812 if (hasClosePath) { 813 ok(pathData.length, "Close path expected but path is empty"); 814 const closePathSeg = pathData.at(-1); 815 Assert.strictEqual( 816 closePathSeg.type.toLowerCase(), 817 "z", 818 "Close path not found" 819 ); 820 } 821 } 822 823 /** 824 * Check whether the given vertex is passing through on the path. 825 * 826 * @param {pathData} pathData - pathData of <path> element. 827 * @param {float} x - x of vertex. 828 * @param {float} y - y of vertex. 829 * @return {boolean} true: passing through, false: no on the path. 830 */ 831 function isPassingThrough(pathData, x, y) { 832 let previousX, previousY; 833 for (let i = 0; i < pathData.length; i++) { 834 const pathSeg = pathData[i]; 835 if (!pathSeg.values.length) { 836 continue; 837 } 838 let currentX, currentY; 839 switch (pathSeg.type) { 840 case "M": 841 case "L": 842 currentX = pathSeg.values[0]; 843 currentY = pathSeg.values[1]; 844 break; 845 case "C": 846 currentX = pathSeg.values[4]; 847 currentY = pathSeg.values[5]; 848 break; 849 } 850 currentX = parseFloat(currentX.toFixed(3)); 851 currentY = parseFloat(currentY.toFixed(3)); 852 if (currentX === x && currentY === y) { 853 return true; 854 } 855 if (previousX === undefined && previousY === undefined) { 856 previousX = currentX; 857 previousY = currentY; 858 } 859 if ( 860 previousX <= x && 861 x <= currentX && 862 Math.min(previousY, currentY) <= y && 863 y <= Math.max(previousY, currentY) 864 ) { 865 return true; 866 } 867 previousX = currentX; 868 previousY = currentY; 869 } 870 return false; 871 } 872 873 /** 874 * Return animation item element by the index. 875 * 876 * @param {DOMElement} panel 877 * #animation-container element. 878 * @param {number} index 879 * @return {DOMElement} 880 * Animation item element. 881 */ 882 async function findAnimationItemByIndex(panel, index) { 883 const itemEls = [...panel.querySelectorAll(".animation-item")]; 884 const itemEl = itemEls[index]; 885 if (!itemEl) { 886 return null; 887 } 888 889 itemEl.scrollIntoView(false); 890 891 await waitUntil( 892 () => 893 itemEl.querySelector(".animation-target .attrName") && 894 itemEl.querySelector(".animation-computed-timing-path") 895 ); 896 897 return itemEl; 898 } 899 900 /** 901 * Return animation item element by target node selector. 902 * This function compares betweem animation-target textContent and given selector. 903 * Then returns matched first item. 904 * 905 * @param {DOMElement} panel 906 * #animation-container element. 907 * @param {string} selector 908 * Selector of tested element. 909 * @return {DOMElement} 910 * Animation item element. 911 */ 912 async function findAnimationItemByTargetSelector(panel, selector) { 913 for (const itemEl of panel.querySelectorAll(".animation-item")) { 914 itemEl.scrollIntoView(false); 915 916 await waitUntil( 917 () => 918 itemEl.querySelector(".animation-target .attrName") && 919 itemEl.querySelector(".animation-computed-timing-path") 920 ); 921 922 const attrNameEl = itemEl.querySelector(".animation-target .attrName"); 923 const regexp = new RegExp(`\\${selector}(\\.|$)`, "gi"); 924 if (regexp.exec(attrNameEl.textContent)) { 925 return itemEl; 926 } 927 } 928 929 return null; 930 } 931 932 /** 933 * Return animation item element by given target element text of animation. 934 * 935 * @param {DOMElement} panel 936 * #animation-container element. 937 * @param {string} targetText 938 * text displayed to represent the animation target in the panel. 939 * @return {DOMElement|null} 940 * Animation item element. 941 */ 942 async function findAnimationItemByTargetText(panel, targetText) { 943 for (const itemEl of panel.querySelectorAll(".animation-item")) { 944 itemEl.scrollIntoView(false); 945 946 await waitUntil( 947 () => 948 itemEl.querySelector(".animation-target .attrName") && 949 itemEl.querySelector(".animation-computed-timing-path") 950 ); 951 952 const attrNameEl = itemEl.querySelector(".animation-target .attrName"); 953 if (attrNameEl.textContent.trim() === targetText) { 954 return itemEl; 955 } 956 } 957 958 return null; 959 } 960 961 /** 962 * Find the <stop> element which has the given offset in the given linearGradientEl. 963 * 964 * @param {Element} linearGradientEl 965 * <linearGradient> element which has <stop> element. 966 * @param {number} offset 967 * Float which represents the "offset" attribute of <stop>. 968 * @return {Element} 969 * If can't find suitable element, returns null. 970 */ 971 function findStopElement(linearGradientEl, offset) { 972 for (const stopEl of linearGradientEl.querySelectorAll("stop")) { 973 if (offset <= parseFloat(stopEl.getAttribute("offset"))) { 974 return stopEl; 975 } 976 } 977 978 return null; 979 } 980 981 /** 982 * Do test for keyframes-graph_computed-value-path-1/2. 983 * 984 * @param {Array} testData 985 */ 986 async function testKeyframesGraphComputedValuePath(testData) { 987 await addTab(URL_ROOT + "doc_multi_keyframes.html"); 988 await removeAnimatedElementsExcept(testData.map(t => `.${t.targetClass}`)); 989 const { animationInspector, panel } = await openAnimationInspector(); 990 991 for (const { properties, targetClass } of testData) { 992 info(`Checking keyframes graph for ${targetClass}`); 993 const onDetailRendered = animationInspector.once( 994 "animation-keyframes-rendered" 995 ); 996 await clickOnAnimationByTargetSelector( 997 animationInspector, 998 panel, 999 `.${targetClass}` 1000 ); 1001 await onDetailRendered; 1002 1003 for (const property of properties) { 1004 const { 1005 name, 1006 computedValuePathClass, 1007 expectedPathSegments, 1008 expectedStopColors, 1009 } = property; 1010 1011 const testTarget = `${name} in ${targetClass}`; 1012 info(`Checking keyframes graph for ${testTarget}`); 1013 info(`Checking keyframes graph path existence for ${testTarget}`); 1014 const keyframesGraphPathEl = panel.querySelector(`.${name}`); 1015 ok( 1016 keyframesGraphPathEl, 1017 `The keyframes graph path element of ${testTarget} should be existence` 1018 ); 1019 1020 info(`Checking computed value path existence for ${testTarget}`); 1021 const computedValuePathEl = keyframesGraphPathEl.querySelector( 1022 `.${computedValuePathClass}` 1023 ); 1024 ok( 1025 computedValuePathEl, 1026 `The computed value path element of ${testTarget} should be existence` 1027 ); 1028 1029 info(`Checking path segments for ${testTarget}`); 1030 const pathEl = computedValuePathEl.querySelector("path"); 1031 ok(pathEl, `The <path> element of ${testTarget} should be existence`); 1032 assertPathSegments(pathEl, true, expectedPathSegments); 1033 1034 if (!expectedStopColors) { 1035 continue; 1036 } 1037 1038 info(`Checking linearGradient for ${testTarget}`); 1039 const linearGradientEl = 1040 computedValuePathEl.querySelector("linearGradient"); 1041 ok( 1042 linearGradientEl, 1043 `The <linearGradientEl> element of ${testTarget} should be existence` 1044 ); 1045 1046 for (const expectedStopColor of expectedStopColors) { 1047 const { offset, color } = expectedStopColor; 1048 assertLinearGradient(linearGradientEl, offset, color); 1049 } 1050 } 1051 } 1052 } 1053 1054 /** 1055 * Check the adjusted current time and created time from specified two animations. 1056 * 1057 * @param {AnimationPlayerFront.state} animation1 1058 * @param {AnimationPlayerFront.state} animation2 1059 */ 1060 function checkAdjustingTheTime(animation1, animation2) { 1061 const adjustedCurrentTimeDiff = 1062 animation2.currentTime / animation2.playbackRate - 1063 animation1.currentTime / animation1.playbackRate; 1064 const createdTimeDiff = animation1.createdTime - animation2.createdTime; 1065 Assert.less( 1066 Math.abs(adjustedCurrentTimeDiff - createdTimeDiff), 1067 0.1, 1068 "Adjusted time is correct" 1069 ); 1070 }