largest-contentful-paint-helpers.js (6358B)
1 const image_delay = 2000; 2 const delay_pipe_value = image_delay / 1000; 3 4 const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => { 5 let timeout_id; 6 const timeout = new Promise((_, reject) => { 7 timeout_id = step_timeout(() => 8 reject(new DOMException(message, "TimeoutError")), delay) 9 }); 10 let result = null; 11 try { 12 result = await Promise.race([promise, timeout]); 13 clearTimeout(timeout_id); 14 } finally { 15 cleanup(); 16 } 17 return result; 18 }; 19 20 // Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values. 21 // The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry. 22 // The |options| parameter may contain some string values specifying the following: 23 // * 'renderTimeIs0': the renderTime should be 0 (image does not pass Timing-Allow-Origin checks). 24 // When not present, the renderTime should not be 0 (image passes the checks). 25 // * 'sizeLowerBound': the |expectedSize| is only a lower bound on the size attribute value. 26 // When not present, |expectedSize| must be exactly equal to the size attribute value. 27 // * 'approximateSize': the |expectedSize| is only approximate to the size attribute value. 28 // This option is mutually exclusive to 'sizeLowerBound'. 29 function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound, options = []) { 30 assert_equals(entry.name, '', "Entry name should be the empty string"); 31 assert_equals(entry.entryType, 'largest-contentful-paint', 32 "Entry type should be largest-contentful-paint"); 33 assert_equals(entry.duration, 0, "Entry duration should be 0"); 34 // The entry's url can be truncated. 35 assert_equals(expectedUrl.substr(0, 100), entry.url.substr(0, 100), 36 `Expected URL ${expectedUrl} should at least start with the entry's URL ${entry.url}`); 37 assert_equals(entry.id, expectedID, "Entry ID matches expected one"); 38 assert_equals(entry.element, document.getElementById(expectedID), 39 "Entry element is expected one"); 40 if (options.includes('skip')) { 41 return; 42 } 43 assert_greater_than_equal(performance.now(), entry.renderTime, 44 'renderTime should occur before the entry is dispatched to the observer.'); 45 assert_approx_equals(entry.startTime, entry.renderTime, 0.001, 46 'startTime should be equal to renderTime to the precision of 1 millisecond.'); 47 if (options.includes('sizeLowerBound')) { 48 assert_greater_than(entry.size, expectedSize); 49 } else if (options.includes('approximateSize')) { 50 assert_approx_equals(entry.size, expectedSize, 1); 51 } else{ 52 assert_equals(entry.size, expectedSize); 53 } 54 55 assert_greater_than_equal(entry.paintTime, timeLowerBound, 'paintTime should represent the time when the UA started painting'); 56 57 // PaintTimingMixin 58 if ("presentationTime" in entry && entry.presentationTime !== null) { 59 assert_greater_than(entry.presentationTime, entry.paintTime); 60 assert_equals(entry.presentationTime, entry.renderTime); 61 } else { 62 assert_equals(entry.renderTime, entry.paintTime); 63 } 64 65 if (options.includes('animated')) { 66 assert_less_than(entry.renderTime, image_delay, 67 'renderTime should be smaller than the delay applied to the second frame'); 68 assert_greater_than(entry.renderTime, 0, 69 'renderTime should be larger than 0'); 70 } 71 else { 72 assert_between_inclusive(entry.loadTime, timeLowerBound, entry.renderTime, 73 'loadTime should occur between the lower bound and the renderTime'); 74 } 75 } 76 77 const load_and_observe = url => { 78 return new Promise(resolve => { 79 (new PerformanceObserver(entryList => { 80 for (let entry of entryList.getEntries()) { 81 if (entry.url == url) { 82 resolve(entryList.getEntries()[0]); 83 } 84 } 85 })).observe({ type: 'largest-contentful-paint', buffered: true }); 86 const img = new Image(); 87 img.id = 'image_id'; 88 img.src = url; 89 document.body.appendChild(img); 90 }); 91 }; 92 93 const load_video_and_observe = url => { 94 return new Promise(resolve => { 95 (new PerformanceObserver(entryList => { 96 for (let entry of entryList.getEntries()) { 97 if (entry.url == url) { 98 resolve(entryList.getEntries()[0]); 99 } 100 } 101 })).observe({ type: 'largest-contentful-paint', buffered: true }); 102 const video = document.createElement("video"); 103 video.id = 'video_id'; 104 video.src = url; 105 video.autoplay = true; 106 video.muted = true; 107 video.loop = true; 108 document.body.appendChild(video); 109 }); 110 }; 111 112 const getLCPStartTime = (identifier) => { 113 return new Promise(resolve => { 114 new PerformanceObserver((entryList, observer) => { 115 entryList.getEntries().forEach(e => { 116 if (e.url.includes(identifier)) { 117 resolve(e); 118 observer.disconnect(); 119 } 120 }); 121 }).observe({ type: 'largest-contentful-paint', buffered: true }); 122 }); 123 } 124 125 const getFCPStartTime = () => { 126 return performance.getEntriesByName('first-contentful-paint')[0]; 127 } 128 129 const add_text = (text) => { 130 const paragraph = document.createElement('p'); 131 paragraph.innerHTML = text; 132 document.body.appendChild(paragraph); 133 } 134 135 const loadImage = (url, shouldBeIgnoredForLCP = false) => { 136 return new Promise(function (resolve, reject) { 137 let image = document.createElement('img'); 138 image.addEventListener('load', () => { resolve(image); }); 139 image.addEventListener('error', reject); 140 image.src = url; 141 if (shouldBeIgnoredForLCP) 142 image.style.opacity = 0; 143 document.body.appendChild(image); 144 }); 145 } 146 147 const checkLCPEntryForNonTaoImages = (times = {}) => { 148 const lcp = times['lcp']; 149 const fcp = times['fcp']; 150 const lcp_url_components = lcp.url.split('/'); 151 152 if (lcp.loadTime <= fcp.startTime) { 153 assert_approx_equals(lcp.startTime, fcp.startTime, 0.001, 154 'LCP start time should be the same as FCP for ' + 155 lcp_url_components[lcp_url_components.length - 1]) + 156 ' when LCP load time is less than FCP.'; 157 } else { 158 assert_approx_equals(lcp.startTime, lcp.loadTime, 0.001, 159 'LCP start time should be the same as LCP load time for ' + 160 lcp_url_components[lcp_url_components.length - 1]) + 161 ' when LCP load time is no less than FCP.'; 162 } 163 164 assert_equals(lcp.renderTime, 0, 165 'The LCP render time of Non-Tao image should always be 0.'); 166 } 167 168 const raf = () => { 169 return new Promise(resolve => requestAnimationFrame(resolve)); 170 }