utils.js (5422B)
1 const windowLoaded = new Promise(resolve => window.addEventListener('load', resolve)); 2 if ("setup" in globalThis) { 3 setup(() => 4 assert_implements(window.PerformanceLongAnimationFrameTiming, 5 'Long animation frames are not supported.')); 6 } 7 8 const very_long_frame_duration = 360; 9 const no_long_frame_timeout = very_long_frame_duration * 2; 10 const waiting_for_long_frame_timeout = very_long_frame_duration * 10; 11 12 function loaf_promise(t) { 13 return new Promise(resolve => { 14 const observer = new PerformanceObserver(entries => { 15 const entry = entries.getEntries()[0]; 16 // TODO: understand why we need this 5ms epsilon. 17 if (entry.duration > very_long_frame_duration - 5) { 18 observer.disconnect(); 19 resolve(entry); 20 } 21 }); 22 23 t.add_cleanup(() => observer.disconnect()); 24 25 observer.observe({entryTypes: ['long-animation-frame']}); 26 }); 27 } 28 29 function busy_wait(ms_delay = very_long_frame_duration) { 30 const deadline = performance.now() + ms_delay; 31 while (performance.now() < deadline) {} 32 } 33 34 function generate_long_animation_frame(duration = very_long_frame_duration) { 35 busy_wait(duration / 2); 36 const reference_time = performance.now(); 37 busy_wait(duration / 2); 38 return new Promise(resolve => new PerformanceObserver((entries, observer) => { 39 const entry = entries.getEntries().find(e => 40 ((e.startTime < reference_time) && 41 (reference_time < (e.startTime + e.duration)))); 42 if (entry) { 43 observer.disconnect(); 44 resolve(entry); 45 } 46 }).observe({type: "long-animation-frame"})); 47 } 48 49 async function expect_long_frame(cb, t) { 50 await windowLoaded; 51 const timeout = new Promise((resolve, reject) => 52 t.step_timeout(() => resolve("timeout"), waiting_for_long_frame_timeout)); 53 let resolve_loaf; 54 const received_loaf = new Promise(resolve => { resolve_loaf = resolve; }); 55 const generate_loaf = (duration = very_long_frame_duration) => 56 generate_long_animation_frame(duration).then(resolve_loaf); 57 window.generate_loaf_now = generate_loaf; 58 await cb(t, generate_loaf); 59 const entry = await Promise.race([ 60 received_loaf, 61 timeout 62 ]); 63 delete window.generate_loaf_now; 64 return entry; 65 } 66 67 function generate_long_animation_frame(duration = 120) { 68 busy_wait(duration / 2); 69 const reference_time = performance.now(); 70 busy_wait(duration / 2); 71 return new Promise(resolve => new PerformanceObserver((entries, observer) => { 72 const entry = entries.getEntries().find(e => 73 (e.startTime < reference_time) && 74 (reference_time < (e.startTime + e.duration))); 75 if (entry) { 76 observer.disconnect(); 77 resolve(entry); 78 } 79 }).observe({type: "long-animation-frame"})); 80 } 81 82 async function expect_long_frame_with_script(cb, predicate, t) { 83 const entry = await expect_long_frame(cb, t); 84 for (const script of entry.scripts ?? []) { 85 if (predicate(script, entry)) 86 return [entry, script]; 87 } 88 89 return []; 90 } 91 92 async function expect_no_long_frame(cb, t) { 93 await windowLoaded; 94 for (let i = 0; i < 5; ++i) { 95 const receivedLongFrame = loaf_promise(t); 96 await cb(); 97 const result = await Promise.race([receivedLongFrame, 98 new Promise(resolve => t.step_timeout(() => resolve("timeout"), 99 no_long_frame_timeout))]); 100 if (result === "timeout") 101 return false; 102 } 103 104 throw new Error("Consistently creates long frame"); 105 } 106 107 async function prepare_exec_iframe(t, origin) { 108 const iframe = document.createElement("iframe"); 109 t.add_cleanup(() => iframe.remove()); 110 const url = new URL("/common/dispatcher/remote-executor.html", origin); 111 const uuid = token(); 112 url.searchParams.set("uuid", uuid); 113 iframe.src = url.href; 114 document.body.appendChild(iframe); 115 await new Promise(resolve => iframe.addEventListener("load", resolve)); 116 return [new RemoteContext(uuid), iframe]; 117 } 118 119 120 async function prepare_exec_popup(t, origin) { 121 const url = new URL("/common/dispatcher/remote-executor.html", origin); 122 const uuid = token(); 123 url.searchParams.set("uuid", uuid); 124 const popup = window.open(url); 125 t.add_cleanup(() => popup.close()); 126 return [new RemoteContext(uuid), popup]; 127 } 128 129 function test_loaf_script(cb, invoker, invokerType, label) { 130 promise_test(async t => { 131 let [entry, script] = []; 132 [entry, script] = await expect_long_frame_with_script(cb, 133 script => ( 134 script.invokerType === invokerType && 135 script.invoker.startsWith(invoker)), t); 136 137 assert_true(!!entry, "Entry detected"); 138 assert_greater_than_equal(entry.duration, script.duration); 139 assert_greater_than_equal(script.executionStart, script.startTime); 140 assert_greater_than_equal(script.startTime, entry.startTime) 141 assert_equals(script.window, window); 142 assert_equals(script.forcedStyleAndLayoutDuration, 0); 143 assert_equals(script.windowAttribution, "self"); 144 }, `LoAF script: ${invoker} ${invokerType},${label ? ` ${label}` : ''}`); 145 146 } 147 148 function test_self_user_callback(cb, invoker, label) { 149 test_loaf_script(cb, invoker, "user-callback", label); 150 } 151 152 function test_self_event_listener(cb, invoker, label) { 153 test_loaf_script(cb, invoker, "event-listener", label); 154 } 155 156 function test_promise_script(cb, resolve_or_reject, invoker, label) { 157 test_loaf_script(cb, invoker, `${resolve_or_reject}-promise`, label); 158 } 159 160 function test_self_script_block(cb, invoker, type) { 161 test_loaf_script(cb, invoker, type); 162 }