test-render-blocking.js (4186B)
1 // Observes the `load` event of an EventTarget, or the finishing of a resource 2 // given its url. Requires `/preload/resources/preload_helper.js` for the latter 3 // usage. 4 class LoadObserver { 5 constructor(target) { 6 this.finishTime = null; 7 this.load = new Promise((resolve, reject) => { 8 if (target.addEventListener) { 9 target.addEventListener('load', ev => { 10 this.finishTime = ev.timeStamp; 11 resolve(ev); 12 }); 13 target.addEventListener('error', reject); 14 } else if (typeof target === 'string') { 15 const observer = new PerformanceObserver(() => { 16 if (numberOfResourceTimingEntries(target)) { 17 this.finishTime = performance.now(); 18 resolve(); 19 } 20 }); 21 observer.observe({type: 'resource', buffered: true}); 22 } else { 23 reject('Unsupported target for LoadObserver'); 24 } 25 }); 26 } 27 28 get finished() { 29 return this.finishTime !== null; 30 } 31 } 32 33 // Observes the insertion of a script/parser-blocking element into DOM via 34 // MutationObserver, so that we can access the element before it's loaded. 35 function nodeInserted(parentNode, predicate) { 36 return new Promise(resolve => { 37 function callback(mutationList) { 38 for (let mutation of mutationList) { 39 for (let node of mutation.addedNodes) { 40 if (predicate(node)) 41 resolve(node); 42 } 43 } 44 } 45 new MutationObserver(callback).observe(parentNode, {childList: true}); 46 }); 47 } 48 49 function createAutofocusTarget() { 50 const autofocusTarget = document.createElement('textarea'); 51 autofocusTarget.setAttribute('autofocus', ''); 52 // We may not have a body element at this point if we are testing a 53 // script-blocking stylesheet. Hence, the new element is added to 54 // documentElement. 55 document.documentElement.appendChild(autofocusTarget); 56 return autofocusTarget; 57 } 58 59 function createScrollTarget() { 60 const scrollTarget = document.createElement('div'); 61 scrollTarget.style.overflow = 'scroll'; 62 scrollTarget.style.height = '100px'; 63 const scrollContent = document.createElement('div'); 64 scrollContent.style.height = '200px'; 65 scrollTarget.appendChild(scrollContent); 66 document.documentElement.appendChild(scrollTarget); 67 return scrollTarget; 68 } 69 70 function createAnimationTarget() { 71 const style = document.createElement('style'); 72 style.textContent = ` 73 @keyframes anim { 74 from { height: 100px; } 75 to { height: 200px; } 76 } 77 `; 78 const animationTarget = document.createElement('div'); 79 animationTarget.style.backgroundColor = 'green'; 80 animationTarget.style.height = '50px'; 81 animationTarget.style.animation = 'anim 100ms'; 82 document.documentElement.appendChild(style); 83 document.documentElement.appendChild(animationTarget); 84 return animationTarget; 85 } 86 87 // Error margin for comparing timestamps of paint and load events, in case they 88 // are reported by different threads. 89 const epsilon = 50; 90 91 function test_render_blocking(optionalElementOrUrl, finalTest, finalTestTitle) { 92 // Ideally, we should observe the 'load' event on the specific render-blocking 93 // elements. However, this is not possible for script-blocking stylesheets, so 94 // we have to observe the 'load' event on 'window' instead. 95 if (!(optionalElementOrUrl instanceof HTMLElement) && 96 typeof optionalElementOrUrl !== 'string') { 97 finalTestTitle = finalTest; 98 finalTest = optionalElementOrUrl; 99 optionalElementOrUrl = undefined; 100 } 101 const loadObserver = new LoadObserver(optionalElementOrUrl || window); 102 103 promise_test(async test => { 104 assert_implements(window.PerformancePaintTiming); 105 106 await test.step_wait(() => performance.getEntriesByType('paint').length); 107 108 assert_true(loadObserver.finished); 109 for (let entry of performance.getEntriesByType('paint')) { 110 assert_greater_than(entry.startTime, loadObserver.finishTime - epsilon, 111 `${entry.name} should occur after loading render-blocking resources`); 112 } 113 }, 'Rendering is blocked before render-blocking resources are loaded'); 114 115 promise_test(test => { 116 return loadObserver.load.then(() => finalTest(test)); 117 }, finalTestTitle); 118 }