util.js (3197B)
1 // Utilities for Layout Instability tests. 2 3 // Returns a promise that is resolved when the specified number of animation 4 // frames has occurred. 5 waitForAnimationFrames = frameCount => { 6 return new Promise(resolve => { 7 const handleFrame = () => { 8 if (--frameCount <= 0) 9 resolve(); 10 else 11 requestAnimationFrame(handleFrame); 12 }; 13 requestAnimationFrame(handleFrame); 14 }); 15 }; 16 17 // Returns a promise that is resolved when the next animation frame occurs. 18 waitForAnimationFrame = () => waitForAnimationFrames(1); 19 20 // Helper to compute an expected layout shift score based on an expected impact 21 // region and max move distance for a particular animation frame. 22 computeExpectedScore = (impactRegionArea, moveDistance) => { 23 const docElement = document.documentElement; 24 25 const viewWidth = docElement.clientWidth; 26 const viewHeight = docElement.clientHeight; 27 28 const viewArea = viewWidth * viewHeight; 29 const viewMaxDim = Math.max(viewWidth, viewHeight); 30 31 const impactFraction = impactRegionArea / viewArea; 32 const distanceFraction = moveDistance / viewMaxDim; 33 34 return impactFraction * distanceFraction; 35 }; 36 37 // An list to record all the entries with startTime and score. 38 let watcher_entry_record = []; 39 40 // An object that tracks the document cumulative layout shift score. 41 // Usage: 42 // 43 // const watcher = new ScoreWatcher; 44 // ... 45 // assert_equals(watcher.score, expectedScore); 46 // 47 // The score reflects only layout shifts that occur after the ScoreWatcher is 48 // constructed. 49 ScoreWatcher = function() { 50 if (PerformanceObserver.supportedEntryTypes.indexOf("layout-shift") == -1) 51 throw new Error("Layout Instability API not supported"); 52 this.score = 0; 53 this.scoreWithInputExclusion = 0; 54 const resetPromise = () => { 55 this.promise = new Promise(resolve => { 56 this.resolve = () => { 57 resetPromise(); 58 resolve(); 59 } 60 }); 61 }; 62 resetPromise(); 63 const observer = new PerformanceObserver(list => { 64 list.getEntries().forEach(entry => { 65 this.lastEntry = entry; 66 this.score += entry.value; 67 watcher_entry_record.push({startTime: entry.startTime, score: entry.value, hadRecentInput : entry.hadRecentInput}); 68 if (!entry.hadRecentInput) 69 this.scoreWithInputExclusion += entry.value; 70 this.resolve(); 71 }); 72 }); 73 observer.observe({entryTypes: ['layout-shift']}); 74 }; 75 76 ScoreWatcher.prototype.checkExpectation = function(expectation) { 77 if (expectation.score != undefined) 78 assert_equals(this.score, expectation.score); 79 if (expectation.sources) 80 check_sources(expectation.sources, this.lastEntry.sources); 81 }; 82 83 ScoreWatcher.prototype.get_entry_record = function() { 84 return watcher_entry_record; 85 }; 86 87 check_sources = (expect_sources, actual_sources) => { 88 assert_equals(expect_sources.length, actual_sources.length); 89 let rect_match = (e, a) => 90 e[0] == a.x && e[1] == a.y && e[2] == a.width && e[3] == a.height; 91 let match = e => a => 92 e.node === a.node && 93 rect_match(e.previousRect, a.previousRect) && 94 rect_match(e.currentRect, a.currentRect); 95 for (let e of expect_sources) 96 assert_true(actual_sources.some(match(e)), e.node + " not found"); 97 };