summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/long-animation-frame
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/long-animation-frame
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/long-animation-frame')
-rw-r--r--testing/web-platform/tests/long-animation-frame/META.yml2
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-basic.html57
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-blocking-duration.html65
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-buffered.html28
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-event-blocking-duration.html30
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-event-listener.html44
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-first-ui-event.html81
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-idle.html36
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-crossorigin.html27
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-same-origin.html35
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-self.html31
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-pause-duration.html30
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-popup.html25
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-promise.html52
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-script-block.html77
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-script-nested-callback.html41
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-script-window-attribution.html61
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location-redirect.html95
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location.html57
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-stream-source-location.html26
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-stream.html39
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-supportedEntryTypes.html22
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-timeline.html27
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-toJSON.html46
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-ui-event-render-start.html46
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-user-callback.html56
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-visibility.html26
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/loaf-window-only.worker.js11
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/busy.js4
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/event-generates-loaf.js10
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-after-callback.js7
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-in-microtask-after-callback.js7
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/promise-generates-loaf.js4
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/raf-generates-loaf.js5
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/stream-promise-generates-loaf.js12
-rw-r--r--testing/web-platform/tests/long-animation-frame/tentative/resources/utils.js131
36 files changed, 1353 insertions, 0 deletions
diff --git a/testing/web-platform/tests/long-animation-frame/META.yml b/testing/web-platform/tests/long-animation-frame/META.yml
new file mode 100644
index 0000000000..769c325aee
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/META.yml
@@ -0,0 +1,2 @@
+suggested_reviewers:
+ - noamr
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-basic.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-basic.html
new file mode 100644
index 0000000000..c6d3f8e32a
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-basic.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: basic</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: basic</h1>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ await expect_long_frame(() => busy_wait(), t);
+}, 'A long busy wait is a long animation frame');
+
+promise_test(async t => {
+ await expect_long_frame(() => requestAnimationFrame(busy_wait), t);
+}, 'A long busy wait in a requestAnimationFrame is a long animation frame');
+
+promise_test(async t => {
+ const segment_duration = very_long_frame_duration / 2;
+ const entry = await expect_long_frame(async () => {
+ busy_wait(segment_duration);
+ await new Promise(resolve => requestAnimationFrame(() => {
+ busy_wait(segment_duration)
+ resolve();
+ }));
+ }, t);
+
+ assert_not_equals(entry, "timeout");
+ assert_greater_than_equal(entry.renderStart - entry.startTime, segment_duration);
+}, 'A long busy wait split between a task and a requestAnimationFrame is a long animation frame');
+
+promise_test(async t => {
+ const segment_duration = very_long_frame_duration / 3;
+ const entry = await expect_long_frame(async () => {
+ const element = document.createElement("div");
+ document.body.appendChild(element);
+ t.add_cleanup(() => element.remove());
+ busy_wait(segment_duration);
+ requestAnimationFrame(() => {
+ busy_wait(segment_duration);
+ });
+
+ new ResizeObserver(() => {
+ busy_wait(segment_duration);
+ }).observe(element);
+ }, t);
+
+ assert_not_equals(entry, "timeout");
+ assert_greater_than_equal(entry.renderStart - entry.startTime, segment_duration);
+ assert_greater_than_equal(entry.styleAndLayoutStart - entry.renderStart, segment_duration);
+}, 'ResizeObservers should create a long-frame and affect layoutStartTime');
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-blocking-duration.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-blocking-duration.html
new file mode 100644
index 0000000000..e56a98649e
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-blocking-duration.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: basic</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: blocking duration</h1>
+<div id="log"></div>
+<script>
+
+function loaf_blocking_duration_test(run, label) {
+ const OVERHEAD_EPSILON = 5;
+ const BLOCKING_THRESHOLD = 50;
+ promise_test(async t => {
+ let found = false;
+ for (let i = 0; i < 10 && !found; ++i) {
+ const longtask_promise = new Promise(resolve => new PerformanceObserver(
+ (entries, observer) => {
+ resolve(entries.getEntries());
+ observer.disconnect();
+ }).observe({entryTypes: ["longtask"]}));
+ const [longtask_entries, loaf_entry] = await Promise.all(
+ [longtask_promise, expect_long_frame(run, t)]);
+ const overlapping = longtask_entries.filter(longtask =>
+ (longtask.startTime >= loaf_entry.startTime &&
+ longtask.startTime < (loaf_entry.startTime + loaf_entry.duration) &&
+ (!loaf_entry.renderStart ||
+ (longtask.startTime < loaf_entry.renderStart - OVERHEAD_EPSILON))));
+
+ const longest_index = overlapping.reduce(
+ (max, cur, i) => cur > overlapping[max] ? i : max, 0);
+ let expected_blocking_duration = 0;
+ overlapping.forEach(({duration}, i) => {
+ if (i === longest_index && loaf_entry.renderStart)
+ duration += loaf_entry.startTime + loaf_entry.duration -
+ loaf_entry.renderStart;
+ expected_blocking_duration += Math.max(0, duration - BLOCKING_THRESHOLD);
+ });
+
+ if (!overlapping.length && loaf_entry.renderStart) {
+ expected_blocking_duration =
+ Math.max(0,
+ loaf_entry.startTime + loaf_entry.duration - loaf_entry.renderStart -
+ BLOCKING_THRESHOLD);
+ }
+
+ if (Math.abs(loaf_entry.blockingDuration - expected_blocking_duration) <
+ OVERHEAD_EPSILON) {
+ found = true;
+ }
+ }
+ assert_true(found);
+ }, `LoAF blockingDuration should be equivalent to long tasks: ${label}`);
+}
+
+loaf_blocking_duration_test(t => t.step_timeout(busy_wait), "Non-rendering");
+loaf_blocking_duration_test(t => t.step_timeout(() => {
+ busy_wait();
+ requestAnimationFrame(busy_wait);
+}), "Rendering");
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-buffered.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-buffered.html
new file mode 100644
index 0000000000..1a07036b15
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-buffered.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: basic</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: buffered</h1>
+<div id="log"></div>
+<script>
+promise_test(async t => {
+ busy_wait(very_long_frame_duration);
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ const result = await new Promise(resolve => {
+ new PerformanceObserver(t.step_func(entries => {
+ for (const e of entries.getEntries()) {
+ if (e.duration >= very_long_frame_duration)
+ resolve("entry-found");
+ }
+ })).observe({type: 'long-animation-frame', buffered: true});
+ t.step_timeout(() => resolve("timeout"), waiting_for_long_frame_timeout);
+ });
+ assert_equals(result, "entry-found");
+}, 'PerformanceObserver with buffered flag can see previous long-animation-frame entries.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-event-blocking-duration.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-event-blocking-duration.html
new file mode 100644
index 0000000000..ed31244a1d
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-event-blocking-duration.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: blocking duration with events</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: blocking duration with events</h1>
+<div id="log"></div>
+<output id="output"></output>
+<script>
+promise_test(async t => {
+ const [entry, script] = await expect_long_frame_with_script(async () => {
+ const button = document.createElement("button");
+ button.innerText = "click";
+ button.addEventListener("click", () => busy_wait());
+ document.body.append(button);
+ t.add_cleanup(() => button.remove());
+ await test_driver.click(button);
+ }, (script) => script.invoker === "BUTTON.onclick", t);
+ assert_greater_than(entry.duration, 50);
+ assert_greater_than_equal(entry.blockingDuration, 300);
+}, "LoAF generated by events should generate correct blockingDuration");
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-event-listener.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-event-listener.html
new file mode 100644
index 0000000000..f866a1dfd8
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-event-listener.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: basic</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: event handlers</h1>
+<div id="log"></div>
+<script>
+
+test_self_event_listener(t => {
+ const img = document.createElement("img");
+ img.src = "/images/green.png";
+ img.addEventListener("load", () => {
+ busy_wait();
+ });
+ img.id = "image";
+ document.body.appendChild(img);
+ t.add_cleanup(() => img.remove());
+}, "IMG#image.onload");
+
+test_self_event_listener(t => {
+ const img = document.createElement("img");
+ img.src = "/images/green.png";
+ img.addEventListener("load", () => {
+ busy_wait();
+ });
+ document.body.appendChild(img);
+ t.add_cleanup(() => img.remove());
+}, "IMG[src=/images/green.png].onload");
+
+test_self_event_listener(t => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "/common/dummy.xml");
+ xhr.addEventListener("load", () => {
+ busy_wait();
+ });
+ xhr.send();
+}, "XMLHttpRequest.onload");
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-first-ui-event.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-first-ui-event.html
new file mode 100644
index 0000000000..7e32010189
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-first-ui-event.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: first UI Event</title>
+<meta name="timeout" content="long">
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-actions.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: First UI Event</h1>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ const button = document.createElement("button");
+ button.innerText = "Click";
+ document.body.appendChild(button);
+ t.add_cleanup(() => button.remove());
+ const eventPromise = new Promise(resolve => button.addEventListener("click", event => {
+ busy_wait();
+ resolve(event);
+ }));
+ const entryPromise = expect_long_frame_with_script(() => {
+ test_driver.click(button);
+ }, s => s.invoker === "BUTTON.onclick", t);
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ const event = await eventPromise;
+ const [entry] = await entryPromise;
+ assert_equals(entry.firstUIEventTimestamp, event.timeStamp);
+}, "LoAF should expose firstUIEventTimestamp for click events");
+
+promise_test(async t => {
+ const button = document.createElement("button");
+ button.innerText = "Hover";
+ document.body.appendChild(button);
+ t.add_cleanup(() => button.remove());
+ let expectedTimestamp = null;
+ const entryPromise = expect_long_frame_with_script(async () => {
+ const eventPromise = new Promise(resolve => button.addEventListener("pointermove", event => {
+ busy_wait();
+ expectedTimestamp = event.timeStamp;
+ resolve();
+ }));
+
+ const actions = new test_driver.Actions()
+ .pointerMove(0, 0, {origin: button})
+ .pointerDown()
+ .pointerUp();
+ await actions.send();
+ await eventPromise;
+ }, (script, entry) =>
+ script.invoker === "BUTTON.onpointermove" &&
+ entry.firstUIEventTimestamp === expectedTimestamp, t);
+}, "LoAF should expose firstUIEventTimestamp for pointermove events");
+
+promise_test(async t => {
+ const button = document.createElement("button");
+ button.innerText = "Click";
+ document.body.appendChild(button);
+ t.add_cleanup(() => button.remove());
+ let firstUIEventTimestamp = null;
+ const eventPromise = new Promise(resolve => button.addEventListener("click", event => {
+ if (firstUIEventTimestamp)
+ resolve(event);
+ else {
+ firstUIEventTimestamp = event.timeStamp;
+ busy_wait();
+ }
+ }));
+ const entryPromise = expect_long_frame_with_script(() => {
+ test_driver.click(button);
+ test_driver.click(button);
+ }, s => s.invoker === "BUTTON.onclick", t);
+ const [event, [entry]] = await Promise.all([eventPromise, entryPromise]);
+ assert_equals(entry.firstUIEventTimestamp, firstUIEventTimestamp);
+}, "firstUIEventTimestamp doesn't have to come from a long script");
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-idle.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-idle.html
new file mode 100644
index 0000000000..bc9f910bb1
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-idle.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: requestIdleCallback</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: requestIdleCallback</h1>
+<div id="log"></div>
+<script>
+setup(() =>
+ assert_implements(window.requestIdleCallback,
+ 'requestIdleCallback is not supported.'));
+
+/*
+promise_test(async t => {
+ await expect_no_long_frame(() => requestIdleCallback(busy_wait), t);
+}, 'A long busy wait in an idle callback is not a long animation frame');
+*/
+
+promise_test(async t => {
+ const segment_duration = very_long_frame_duration / 2;
+ requestIdleCallback(() => {
+ busy_wait(segment_duration);
+ requestAnimationFrame(() => {
+ busy_wait(segment_duration);
+ });
+ });
+ await expect_long_frame(() => {}, t);
+}, 'A long busy wait split between an idle callback and a ' +
+ 'requestAnimationFrame is a long animation frame');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-crossorigin.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-crossorigin.html
new file mode 100644
index 0000000000..16ecfd7017
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-crossorigin.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: iframes (cross-origin)</title>
+<meta name="timeout" content="long">
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<div id="log"></div>
+<script>
+const host_info = get_host_info();
+
+for (const type of ["REMOTE_ORIGIN", "HTTP_NOTSAMESITE_ORIGIN"]) {
+ promise_test(async t => {
+ const [executor] = await prepare_exec_iframe(t, host_info[type]);
+ await expect_no_long_frame(() => executor.execute_script((duration) => {
+ const deadline = performance.now() + duration;
+ while (performance.now() < deadline) {}
+ }, [very_long_frame_duration]), t);
+ }, `A long busy wait in a ${type} iframe is not a long animation frame`);
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-same-origin.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-same-origin.html
new file mode 100644
index 0000000000..65dc89f29a
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-same-origin.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: iframes (same-origin)</title>
+<meta name="timeout" content="long">
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<div id="log"></div>
+<script>
+const host_info = get_host_info();
+const {ORIGIN, REMOTE_ORIGIN, HTTP_NOTSAMESITE_ORIGIN} = host_info;
+
+promise_test(async t => {
+ const [executor] = await prepare_exec_iframe(t, ORIGIN);
+ await expect_no_long_frame(() => executor.execute_script((duration) => {
+ const deadline = performance.now() + duration;
+ while (performance.now() < deadline) {}
+ }, [very_long_frame_duration]), t);
+}, 'A long busy wait without render in a same-origin iframe is not a long animation frame');
+
+promise_test(async t => {
+ const [executor] = await prepare_exec_iframe(t, ORIGIN);
+ await expect_long_frame(() => executor.execute_script(async (duration) => {
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+ const deadline = performance.now() + duration;
+ while (performance.now() < deadline) {}
+ }, [very_long_frame_duration]), t);
+}, 'A long busy wait in a same-origin requestAnimationFrame is a long animation frame');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-self.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-self.html
new file mode 100644
index 0000000000..7511ff1072
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-iframe-self.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: iframes (self)</title>
+<meta name="timeout" content="long">
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<div id="log"></div>
+<script>
+const host_info = get_host_info();
+
+for (const origin of ["ORIGIN", "REMOTE_ORIGIN", "HTTP_NOTSAMESITE_ORIGIN"]) {
+ promise_test(async t => {
+ const [executor] = await prepare_exec_iframe(t, host_info[origin]);
+ const entry = await executor.execute_script(async (duration) => {
+ const entryPromise = new Promise(resolve => new PerformanceObserver(list => {
+ resolve(list.getEntries(0));
+ }).observe({entryTypes: ["long-animation-frame"]}));
+ const deadline = performance.now() + duration;
+ while (performance.now() < deadline) {}
+ return entryPromise;
+ }, [very_long_frame_duration]);
+ }, `frames receive own long animation frames (${origin})`);
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-pause-duration.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-pause-duration.html
new file mode 100644
index 0000000000..a4181239d4
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-pause-duration.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: pause</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: pause</h1>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ const pause_duration = very_long_frame_duration / 2;
+ [entry, script] = await expect_long_frame_with_script(() => t.step_timeout(() => {
+ busy_wait(pause_duration);
+ const sync_xhr = new XMLHttpRequest();
+ sync_xhr.open("GET", `/xhr/resources/delay.py?ms=${pause_duration}`, /*async=*/false);
+ sync_xhr.send();
+ }, 0), script => (
+ script.invoker === "TimerHandler:setTimeout" &&
+ script.duration >= very_long_frame_duration), t);
+ assert_true("pauseDuration" in script);
+ assert_greater_than(script.pauseDuration, pause_duration);
+}, "Synchronous XHR should be counted as pauseDuration");
+
+// TODO: Test for alert/confirm, requires WPT infra changes.
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-popup.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-popup.html
new file mode 100644
index 0000000000..3f9758953e
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-popup.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: popups</title>
+<meta name="timeout" content="long">
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<div id="log"></div>
+<script>
+const host_info = get_host_info();
+const { ORIGIN } = host_info;
+
+promise_test(async t => {
+ const [executor] = await prepare_exec_popup(t, ORIGIN);
+ await expect_no_long_frame(() => executor.execute_script((duration) => {
+ const deadline = performance.now() + duration;
+ while (performance.now() < deadline) {}
+ }, [very_long_frame_duration]), t);
+}, 'A long busy wait in a same-origin popup is a not long animation frame');
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-promise.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-promise.html
new file mode 100644
index 0000000000..5ead569c8a
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-promise.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: promise resolvers</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: promise resolvers</h1>
+<div id="log"></div>
+<script type="module">
+
+const {REMOTE_ORIGIN} = get_host_info();
+
+test_promise_script(async t => {
+ await fetch("/common/dummy.xml");
+ busy_wait(very_long_frame_duration);
+}, "resolve", "Window.fetch.then");
+
+test_promise_script(async t => {
+ const response = await fetch("/common/dummy.xml");
+ await response.text();
+ busy_wait(very_long_frame_duration);
+}, "resolve", "Response.text.then");
+
+test_promise_script(async t => {
+ const response = await fetch("/common/dummy.xml");
+ await response.arrayBuffer();
+ busy_wait(very_long_frame_duration);
+}, "resolve", "Response.arrayBuffer.then");
+
+test_promise_script(async t => {
+ const response = await fetch("/fetch/api/resources/data.json");
+ await response.json();
+ busy_wait(very_long_frame_duration);
+}, "resolve", "Response.json.then");
+
+test_promise_script(async t => {
+ const response = await import("/loading/resources/dummy.js");
+ busy_wait(very_long_frame_duration);
+}, "resolve", "import.then");
+
+test_promise_script(async t => {
+ fetch(new URL("/common/dummy.xml", REMOTE_ORIGIN).href, {mode: "cors"})
+ .catch(() => {
+ busy_wait(very_long_frame_duration);
+ })
+}, "reject", "Window.fetch.catch" );
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-block.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-block.html
new file mode 100644
index 0000000000..759b31f9a1
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-block.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: script blocks</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<script src="resources/busy.js?from-parser"></script>
+<body>
+<h1>Long Animation Frame: script blocks</h1>
+<div id="log"></div>
+<script>
+promise_test(async t => {
+ await new Promise(resolve => {
+ new PerformanceObserver((list, observer) => {
+ if (list.getEntries().find(loaf => loaf.scripts.some(script =>
+ script.invoker === new URL("resources/busy.js?from-parser", location.href).href))) {
+ observer.disconnect();
+ resolve();
+ }
+ }).observe({type: "long-animation-frame", buffered: true});
+ })
+}, "Parser-inserted classic script should generate a long script");
+
+test_self_script_block(t => {
+ const script = document.createElement("script");
+ script.type = "module";
+ script.innerHTML = `(${busy_wait.toString()})()`;
+ document.body.appendChild(script);
+}, location.href, "module-script");
+
+test_self_script_block(t => {
+ const script = document.createElement("script");
+ script.src = "resources/busy.js";
+ document.body.appendChild(script);
+}, new URL("resources/busy.js", location.href).href, "classic-script");
+
+test_self_script_block(t => {
+ const uid = token();
+ const script = document.createElement("script");
+ script.src = `resources/busy.js?token=${uid}`;
+ script.type = "module";
+ document.body.appendChild(script);
+}, new URL("resources/busy.js", location.href).href, "module-script");
+
+test_self_script_block(t => {
+ const uid = token();
+ const script = document.createElement("script");
+ script.type = "module";
+ script.innerHTML = `import("./resources/busy.js?import=${uid}");`;
+ document.body.appendChild(script);
+}, new URL("resources/busy.js?import", location.href).href, "module-script");
+
+const busy_wait_str = ` (function() {
+ const deadline = performance.now() + 365;
+ while (performance.now() < deadline) {}
+ })()
+`;
+
+const data_url = `data:text/javascript;charset=utf-8,${encodeURIComponent(busy_wait_str)}`;
+
+test_self_script_block(t => {
+ const script = document.createElement("script");
+ script.src = data_url;
+ document.body.appendChild(script);
+}, "data:", "classic-script");
+
+const blob_url = URL.createObjectURL(new Blob([busy_wait_str], {type: "text/javascript"}));
+
+test_self_script_block(t => {
+ const script = document.createElement("script");
+ script.src = blob_url;
+ document.body.appendChild(script);
+}, blob_url, "classic-script");
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-nested-callback.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-nested-callback.html
new file mode 100644
index 0000000000..8d1304fc80
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-nested-callback.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: nested scripts</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: nested scripts</h1>
+<div id="log"></div>
+<div id="dummy"></div>
+<script>
+promise_test(async t => {
+ const [entry, script] = await expect_long_frame_with_script(t => {
+ const script_element = document.createElement("script");
+ script_element.async = true;
+ script_element.src = "resources/loaf-after-callback.js";
+ t.add_cleanup(() => script_element.remove());
+ document.body.appendChild(script_element);
+ }, script =>
+ script.invoker === new URL("resources/loaf-after-callback.js", location.href).href,
+ t);
+ assert_greater_than_equal(script.duration, very_long_frame_duration);
+}, "a callback inside a script block should not mask LoAFs that come afterwards")
+
+promise_test(async t => {
+ const [entry, script] = await expect_long_frame_with_script(t => {
+ const script_element = document.createElement("script");
+ script_element.async = true;
+ script_element.src = "resources/loaf-in-microtask-after-callback.js";
+ t.add_cleanup(() => script_element.remove());
+ document.body.appendChild(script_element);
+ }, script =>
+ script.invoker === new URL("resources/loaf-in-microtask-after-callback.js", location.href).href,
+ t);
+ assert_greater_than_equal(script.duration, very_long_frame_duration);
+}, "a callback inside a script block should not mask LoAFs in a microtask")
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-window-attribution.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-window-attribution.html
new file mode 100644
index 0000000000..0b8f45bed0
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-script-window-attribution.html
@@ -0,0 +1,61 @@
+
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: window attribution</title>
+<meta name="timeout" content="long">
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.js"></script>
+<div id="log"></div>
+<script>
+
+const host_info = get_host_info();
+const {ORIGIN, REMOTE_ORIGIN, HTTP_NOTSAMESITE_ORIGIN} = host_info;
+
+promise_test (async t => {
+ const [entry, script] = await expect_long_frame_with_script(() => {
+ requestAnimationFrame(() => busy_wait());
+ }, () => true, t);
+ assert_equals(script.windowAttribution, "self");
+ assert_equals(script.window, window);
+}, 'Scripts in this window should be self-attributed');
+
+promise_test (async t => {
+ let found = false;
+ for (let i = 0; i < 10 && !found; ++i) {
+ const [executor, iframe] = await prepare_exec_iframe(t, ORIGIN);
+ const [entry, script] = await expect_long_frame_with_script(() =>
+ executor.execute_script(async (duration) => {
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+ const deadline = performance.now() + duration;
+ while (performance.now() < deadline) {}
+ }, [very_long_frame_duration]), () => true, t);
+
+ if (script.windowAttribution === "descendant" && script.window === iframe.contentWindow) {
+ found = true;
+ }
+ }
+
+ assert_true(found);
+}, 'Scripts in subframes should be descendant-attributed');
+
+promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "/resource-timing/resources/green.html";
+ document.body.append(iframe);
+ t.add_cleanup(() => iframe.remove());
+ const [entry, script] = await expect_long_frame_with_script(
+ () => requestAnimationFrame(busy_wait), () => true, t);
+ const in_iframe = iframe.contentWindow.performance.getEntriesByType("long-animation-frame").some(
+ e => e.scripts.some(script_in_iframe => script_in_iframe.invoker === script.invoker)
+ );
+
+ assert_false(in_iframe, "IFrame should not receive the LoAF entry");
+}, 'A long busy wait in a requestAnimationFrame should not be observable by same-origin iframes');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location-redirect.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location-redirect.html
new file mode 100644
index 0000000000..c0bb96b1ec
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location-redirect.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: source location should not expose cross-origin redirects</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: source location with cross-origin redirects</h1>
+<div id="log"></div>
+<script>
+
+const {REMOTE_ORIGIN} = get_host_info();
+
+function corsify(url, cors) {
+ if (!cors)
+ return url.href;
+ url.searchParams.set("pipe", "header(Access-Control-Allow-Origin, *)");
+ return url.href;
+}
+
+const targetURL = (path, cors) =>
+ corsify(new URL(path, REMOTE_ORIGIN), cors);
+
+function crossOriginRedirectURL(path, cors) {
+ const url = new URL(`/common/redirect.py`, REMOTE_ORIGIN);
+ url.searchParams.set("location", targetURL(path, cors));
+ return corsify(url, cors);
+}
+
+function test_source_location_with_redirect({path, type, name}) {
+ for (let scriptType of ["classic-opaque", "classic-cors", "module"]) {
+ const cors = scriptType !== "classic-opaque";
+ promise_test(async t => {
+ const VERY_LONG_FRAME_DURATION = 360;
+ const preRedirectURL = crossOriginRedirectURL(path, cors);
+ const postRedirectURL = targetURL(path, cors);
+ let [entry, script] = await expect_long_frame_with_script(() => {
+ const script = document.createElement("script");
+ script.src = preRedirectURL;
+ if (scriptType === "module")
+ script.type = "module";
+ else if (cors)
+ script.crossOrigin = "anonymous";
+ document.body.appendChild(script);
+ t.add_cleanup(() => script.remove());
+ }, script => script.duration >= VERY_LONG_FRAME_DURATION - 5, t);
+
+ const result =
+ script.sourceURL.startsWith(postRedirectURL) ? "post-redirect" :
+ script.sourceURL.startsWith(preRedirectURL) ? "pre-redirect" :
+ script.sourceURL === "" ?
+ "empty" : "other";
+
+ assert_not_equals(result, "other", `Unexpected source location ${script.sourceURL}`);
+ if (!cors)
+ assert_equals(script.executionStart, script.startTime, "Opaque scripts should hide execution start time");
+
+ if (cors) {
+ assert_not_equals(result, "empty", "CORS-ok scripts should expose sourceLocation");
+ } else {
+ assert_not_equals(result, "post-redirect", "No-CORS classic scripts should not expose post-redirect URL");
+ assert_equals(script.sourceCharPosition, type === "script-block" ? 0 : -1, "No-CORS classic scripts should not expose character index");
+ }
+ }, `Test ${type} with ${scriptType}`);
+ }
+}
+
+test_source_location_with_redirect({
+ path: "/long-animation-frame/tentative/resources/busy.js",
+ type: "script-block"
+});
+
+test_source_location_with_redirect({
+ path: "/long-animation-frame/tentative/resources/raf-generates-loaf.js",
+ name: "FrameRequestCallback",
+ type: "user-callback"
+});
+test_source_location_with_redirect({
+ path: "/long-animation-frame/tentative/resources/event-generates-loaf.js",
+ type: "event-listener",
+ name: "XMLHttpRequest.onload"
+});
+
+test_source_location_with_redirect({
+ path: "/long-animation-frame/tentative/resources/promise-generates-loaf.js",
+ type: "resolve-promise",
+ name: "Window.fetch.then"
+});
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location.html
new file mode 100644
index 0000000000..51e8565644
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-source-location.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: source location extraction</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: source location extraction</h1>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ const [entry, script] = await expect_long_frame_with_script(() => {
+ requestAnimationFrame(function non_bound_function() {
+ busy_wait();
+ });
+ }, script => script.invoker === "FrameRequestCallback", t);
+ assert_equals(script.sourceURL, location.href);
+ assert_equals(script.sourceFunctionName, "non_bound_function");
+ assert_greater_than(script.sourceCharPosition, 0);
+}, "Source location should be extracted from non-bound functions");
+
+promise_test(async t => {
+ const [entry, script] = await expect_long_frame_with_script(() => {
+ const object = {};
+ requestAnimationFrame((function my_bound_function() {
+ busy_wait();
+ }).bind(object));
+ }, script => script.invoker === "FrameRequestCallback", t);
+ assert_equals(script.sourceURL, location.href);
+ assert_equals(script.sourceFunctionName, "my_bound_function");
+ assert_greater_than(script.sourceCharPosition, 0);
+}, "Source location should be extracted from bound functions");
+
+promise_test(async t => {
+ const [entry, script] = await expect_long_frame_with_script(() => {
+ t.step_timeout(function my_timeout() {
+ busy_wait();
+ });
+ }, script => script.invoker === "TimerHandler:setTimeout" && script.sourceURL, t );
+ assert_true(script.sourceURL.includes("testharness.js"));
+}, "Source location should be extracted for setTimeout");
+
+promise_test(async t => {
+ const scriptLocation = new URL("resources/promise-generates-loaf.js", location.href);
+ const [entry, script] = await expect_long_frame_with_script(() => {
+ const scriptElement = document.createElement("script");
+ scriptElement.src = scriptLocation;
+ document.body.appendChild(scriptElement);
+ }, script => script.invoker === "Window.fetch.then", t);
+ assert_true(script.sourceURL.includes("promise-generates-loaf.js"));
+}, "Source location should be extracted for promises");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-stream-source-location.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-stream-source-location.html
new file mode 100644
index 0000000000..0fd30859d7
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-stream-source-location.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: source location extraction for streams</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: source location extraction for streams</h1>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ const scriptLocation = new URL("resources/stream-promise-generates-loaf.js", location.href);
+ const [entry, script] = await expect_long_frame_with_script(() => {
+ const scriptElement = document.createElement("script");
+ scriptElement.src = scriptLocation;
+ document.body.appendChild(scriptElement);
+ }, script => script.invoker === "StreamPromise.resolve.then", t);
+
+ assert_true(script.sourceURL.includes("stream-promise-generates-loaf.js"));
+}, "Source location should be extracted for stream promises");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-stream.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-stream.html
new file mode 100644
index 0000000000..e35bc2f9aa
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-stream.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: stream promise resolvers</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: stream promise resolvers</h1>
+<div id="log"></div>
+<script>
+
+test_promise_script(async t => {
+ const response = await fetch("/common/dummy.xml");
+ await response.body.getReader().read();
+ busy_wait(very_long_frame_duration);
+}, "resolve", "ReadableStreamDefaultReader.read.then");
+
+test_promise_script(async t => {
+ const response = await fetch("/common/dummy.xml");
+ await response.body.getReader({ mode: "byob" }).read(new Int32Array(1000));
+ busy_wait(very_long_frame_duration);
+}, "resolve", "ReadableStreamBYOBReader.read.then");
+
+test_promise_script(async t => {
+ const response = await fetch("/common/dummy.xml");
+ const {readable, writable} = new TransformStream({
+ start() {},
+ transform() {
+ busy_wait(very_long_frame_duration);
+ }
+ });
+ response.body.pipeTo(writable);
+ await readable.getReader().read();
+}, "resolve", "StreamPromise.resolve");
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-supportedEntryTypes.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-supportedEntryTypes.html
new file mode 100644
index 0000000000..efa01481fa
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-supportedEntryTypes.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: supportedEntryTypes</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<h1>Long Animation Frame: supportedEntryTypes</h1>
+<div id="log"></div>
+<script>
+
+setup(() =>
+ assert_implements(window.PerformanceLongAnimationFrameTiming,
+ 'Long animation frames are not supported.'));
+
+test(() => {
+ assert_true(PerformanceObserver.supportedEntryTypes.includes("long-animation-frame"));
+}, 'supportedEntryTypes should include long-animation-frame');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-timeline.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-timeline.html
new file mode 100644
index 0000000000..c434a26ef8
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-timeline.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: basic</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: basic</h1>
+<div id="log"></div>
+<script>
+promise_test(async t => {
+ busy_wait(very_long_frame_duration);
+ const is_loaf = entry => entry.duration >= very_long_frame_duration &&
+ entry.entryType == "long-animation-frame";
+
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+ const entry_from_all = [...performance.getEntries()].find(is_loaf);
+ const entry_by_type = [...performance.getEntriesByType("long-animation-frame")].find(is_loaf);
+ const entry_by_name = [...performance.getEntriesByName("long-animation-frame")].find(is_loaf);
+ assert_true(!!entry_from_all, "LoAF Entry found");
+ assert_equals(entry_from_all, entry_by_type);
+ assert_equals(entry_from_all, entry_by_name);
+}, 'LoAF entries are available in the performnace timeline');
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-toJSON.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-toJSON.html
new file mode 100644
index 0000000000..5b249e6972
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-toJSON.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+ promise_test(async t => {
+ window.onload = () => {
+ // Trigger a long task.
+ const begin = window.performance.now();
+ while (window.performance.now() < begin + 60);
+ };
+
+ assert_implements(window.PerformanceLongAnimationFrameTiming, 'Lon are not supported.');
+ const entry = await new Promise(resolve => new PerformanceObserver(
+ t.step_func(entryList => {
+ const entries = entryList.getEntries();
+ assert_greater_than_equal(entries.length, 1);
+ resolve(entries[0]);
+ })).observe({entryTypes: ["long-animation-frame"]}));
+
+ assert_equals(typeof(entry.toJSON), 'function');
+ const entryJSON = entry.toJSON();
+ assert_equals(typeof(entryJSON), 'object');
+ // Check attributes inheritted from PerformanceEntry.
+ const performanceEntryKeys = [
+ 'name',
+ 'entryType',
+ 'startTime',
+ 'duration',
+ 'renderStart',
+ 'styleAndLayoutStart',
+ 'blockingTime',
+ 'firstUIEventTimestamp'
+ ];
+ for (const key of performanceEntryKeys) {
+ assert_equals(entryJSON[key], entry[key],
+ `entry.toJSON().${key} should match entry.${key}`);
+ }
+
+ }, 'Test toJSON() in PerformanceLongAnimationFrameTiming');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-ui-event-render-start.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-ui-event-render-start.html
new file mode 100644
index 0000000000..01d50d47e9
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-ui-event-render-start.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: UI events and renderStart</title>
+<meta name="timeout" content="long">
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-actions.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: UI events and renderStart</h1>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ const BUSY_DURATION_1 = 150;
+ const BUSY_DURATION_2 = 250;
+ const eventPromise = new Promise(resolve => document.body.addEventListener("pointermove", () => {
+ busy_wait(BUSY_DURATION_1);
+ requestAnimationFrame(() => busy_wait(BUSY_DURATION_2));
+ resolve();
+ }, {passive: true}));
+ const actions = new test_driver.Actions();
+ await actions.pointerMove(10, 10, {origin: document.body})
+ .pointerDown()
+ .pointerMove(3, 3)
+ .pointerUp()
+ .send();
+
+ const scriptPredicate = s => s.invoker === "BODY.onpointermove";
+
+ const loaf = await new Promise(resolve =>
+ new PerformanceObserver(entries => {
+ const entry = entries.getEntries().find(
+ e => e.scripts.some(scriptPredicate));
+ resolve(entry);
+ }).observe({type: "long-animation-frame", buffered: true}));
+
+ const script = loaf.scripts.find(scriptPredicate)
+ assert_greater_than_equal(loaf.renderStart, script.startTime + script.duration);
+ assert_greater_than_equal(loaf.blockingDuration, BUSY_DURATION_1 + BUSY_DURATION_2 - 50);
+}, "UI events should always be before renderStart but still affect blockingDuration")
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-user-callback.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-user-callback.html
new file mode 100644
index 0000000000..3d868af87f
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-user-callback.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Long Animation Frame Timing: basic</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/utils.js"></script>
+
+<body>
+<h1>Long Animation Frame: user callbacks</h1>
+<div id="log"></div>
+<script>
+
+test_self_user_callback(t =>
+ t.step_timeout(() => busy_wait()), "TimerHandler:setTimeout");
+
+test_self_user_callback(() => {
+ const interval = setInterval(() => {
+ busy_wait();
+ clearInterval(interval);
+ }, 10);
+}, "TimerHandler:setInterval");
+test_self_user_callback(() =>
+ requestAnimationFrame(() => busy_wait()), "FrameRequestCallback");
+
+test_self_user_callback(t => {
+ const element = document.createElement("div");
+ document.body.appendChild(element);
+ t.add_cleanup(() => element.remove());
+ new ResizeObserver((entries, observer) => {
+ busy_wait(very_long_frame_duration);
+ observer.disconnect();
+ }).observe(element);
+}, "ResizeObserverCallback");
+
+test_self_user_callback(t => {
+ const element = document.createElement("div");
+ element.innerText = "123";
+ t.add_cleanup(() => element.remove());
+ new IntersectionObserver((entries, observer) => {
+ busy_wait(very_long_frame_duration);
+ observer.disconnect();
+ }).observe(element);
+ document.body.appendChild(element);
+}, "IntersectionObserverCallback");
+
+test_self_user_callback(t =>
+ scheduler.postTask(() => busy_wait()), "SchedulerPostTaskCallback");
+
+ test_self_user_callback(t => {
+ new PerformanceObserver(() => busy_wait()).observe(
+ {type: "navigation", buffered: true});
+}, "PerformanceObserverCallback");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-visibility.html b/testing/web-platform/tests/long-animation-frame/tentative/loaf-visibility.html
new file mode 100644
index 0000000000..97038e3073
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-visibility.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Long Animation Frame Timing: iframes</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/page-visibility/resources/window_state_context.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+ <div id="log"></div>
+<script>
+
+promise_test(async t => {
+ const {minimize, restore} = window_state_context(t);
+ await minimize();
+ expect_no_long_frame(busy_wait, t);
+ await restore();
+ expect_long_frame(busy_wait, t);
+}, 'Invisible windows do not report long animation frames');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/loaf-window-only.worker.js b/testing/web-platform/tests/long-animation-frame/tentative/loaf-window-only.worker.js
new file mode 100644
index 0000000000..c1f0439c4b
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/loaf-window-only.worker.js
@@ -0,0 +1,11 @@
+importScripts("/resources/testharness.js");
+
+test(() => {
+ assert_false(PerformanceObserver.supportedEntryTypes.includes("long-animation-frame"));
+}, 'PerformanceObserver should not include "long-animation-frame" in workers');
+
+test(() => {
+ assert_false("PerformanceLongAnimationFrameTiming" in self);
+}, 'PerformanceLongAnimationFrameTiming should not be exposed in workers');
+
+done();
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/busy.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/busy.js
new file mode 100644
index 0000000000..9d761b6de5
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/busy.js
@@ -0,0 +1,4 @@
+(() => {
+ const deadline = performance.now() + 360;
+ while (performance.now() < deadline) {}
+})();
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/event-generates-loaf.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/event-generates-loaf.js
new file mode 100644
index 0000000000..fe7c3eca2b
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/event-generates-loaf.js
@@ -0,0 +1,10 @@
+(() => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', '/common/dummy.xml');
+ xhr.addEventListener('load', () => {
+ const deadline = performance.now() + 360;
+ while (performance.now() < deadline) {
+ }
+ });
+ xhr.send();
+})();
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-after-callback.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-after-callback.js
new file mode 100644
index 0000000000..d9ac74ccf4
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-after-callback.js
@@ -0,0 +1,7 @@
+(function() {
+ busy_wait(60);
+ new URLSearchParams([["a", "hello"]]).forEach((value, key) => {
+ document.querySelector("#dummy").innerText += value;
+ });
+ busy_wait();
+})();
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-in-microtask-after-callback.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-in-microtask-after-callback.js
new file mode 100644
index 0000000000..c73b604f6d
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/loaf-in-microtask-after-callback.js
@@ -0,0 +1,7 @@
+(function() {
+ busy_wait(60);
+ Promise.resolve().then(busy_wait);
+ new URLSearchParams([["a", "hello"]]).forEach((value, key) => {
+ document.querySelector("#dummy").innerText += value;
+ });
+})();
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/promise-generates-loaf.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/promise-generates-loaf.js
new file mode 100644
index 0000000000..53369f7226
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/promise-generates-loaf.js
@@ -0,0 +1,4 @@
+fetch("/common/dummy.xml").then(() => {
+ const deadline = performance.now() + 360;
+ while (performance.now() < deadline) {}
+});
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/raf-generates-loaf.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/raf-generates-loaf.js
new file mode 100644
index 0000000000..b7544af4e5
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/raf-generates-loaf.js
@@ -0,0 +1,5 @@
+requestAnimationFrame(() => {
+ const deadline = performance.now() + 360;
+ while (performance.now() < deadline) {
+ }
+});
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/stream-promise-generates-loaf.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/stream-promise-generates-loaf.js
new file mode 100644
index 0000000000..35e7920bb2
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/stream-promise-generates-loaf.js
@@ -0,0 +1,12 @@
+(async() => {
+ const response = await fetch("/common/dummy.xml");
+ const {readable, writable} = new TransformStream({
+ start() {},
+ transform() {
+ const deadline = performance.now() + 360;
+ while (performance.now() < deadline) {}
+ }
+ });
+ response.body.pipeTo(writable);
+ await readable.getReader().read();
+})();
diff --git a/testing/web-platform/tests/long-animation-frame/tentative/resources/utils.js b/testing/web-platform/tests/long-animation-frame/tentative/resources/utils.js
new file mode 100644
index 0000000000..aa537d39a7
--- /dev/null
+++ b/testing/web-platform/tests/long-animation-frame/tentative/resources/utils.js
@@ -0,0 +1,131 @@
+const windowLoaded = new Promise(resolve => window.addEventListener('load', resolve));
+setup(() =>
+ assert_implements(window.PerformanceLongAnimationFrameTiming,
+ 'Long animation frames are not supported.'));
+
+const very_long_frame_duration = 360;
+const no_long_frame_timeout = very_long_frame_duration * 2;
+const waiting_for_long_frame_timeout = very_long_frame_duration * 10;
+
+function loaf_promise(t) {
+ return new Promise(resolve => {
+ const observer = new PerformanceObserver(entries => {
+ const entry = entries.getEntries()[0];
+ // TODO: understand why we need this 5ms epsilon.
+ if (entry.duration > very_long_frame_duration - 5) {
+ observer.disconnect();
+ resolve(entry);
+ }
+ });
+
+ t.add_cleanup(() => observer.disconnect());
+
+ observer.observe({entryTypes: ['long-animation-frame']});
+ });
+}
+
+function busy_wait(ms_delay = very_long_frame_duration) {
+ const deadline = performance.now() + ms_delay;
+ while (performance.now() < deadline) {}
+}
+
+async function expect_long_frame(cb, t) {
+ await windowLoaded;
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ const timeout = new Promise((resolve, reject) =>
+ t.step_timeout(() => resolve("timeout"), waiting_for_long_frame_timeout));
+ const receivedLongFrame = loaf_promise(t);
+ await cb(t);
+ const entry = await Promise.race([
+ receivedLongFrame,
+ timeout
+ ]);
+ return entry;
+}
+
+async function expect_long_frame_with_script(cb, predicate, t) {
+ for (let i = 0; i < 10; ++i) {
+ const entry = await expect_long_frame(cb, t);
+ if (entry === "timeout" || !entry.scripts.length)
+ continue;
+ for (const script of entry.scripts) {
+ if (predicate(script, entry))
+ return [entry, script];
+ }
+ }
+
+ return [];
+}
+
+async function expect_no_long_frame(cb, t) {
+ await windowLoaded;
+ for (let i = 0; i < 5; ++i) {
+ const receivedLongFrame = loaf_promise(t);
+ await cb();
+ const result = await Promise.race([receivedLongFrame,
+ new Promise(resolve => t.step_timeout(() => resolve("timeout"),
+ no_long_frame_timeout))]);
+ if (result === "timeout")
+ return false;
+ }
+
+ throw new Error("Consistently creates long frame");
+}
+
+async function prepare_exec_iframe(t, origin) {
+ const iframe = document.createElement("iframe");
+ t.add_cleanup(() => iframe.remove());
+ const url = new URL("/common/dispatcher/remote-executor.html", origin);
+ const uuid = token();
+ url.searchParams.set("uuid", uuid);
+ iframe.src = url.href;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", resolve));
+ return [new RemoteContext(uuid), iframe];
+}
+
+
+async function prepare_exec_popup(t, origin) {
+ const url = new URL("/common/dispatcher/remote-executor.html", origin);
+ const uuid = token();
+ url.searchParams.set("uuid", uuid);
+ const popup = window.open(url);
+ t.add_cleanup(() => popup.close());
+ return [new RemoteContext(uuid), popup];
+}
+function test_loaf_script(cb, invoker, invokerType, label) {
+ promise_test(async t => {
+ let [entry, script] = [];
+ [entry, script] = await expect_long_frame_with_script(cb,
+ script => (
+ script.invokerType === invokerType &&
+ script.invoker.startsWith(invoker) &&
+ script.duration >= very_long_frame_duration), t);
+
+ assert_true(!!entry, "Entry detected");
+ assert_greater_than_equal(script.duration, very_long_frame_duration);
+ assert_greater_than_equal(entry.duration, script.duration);
+ assert_greater_than_equal(script.executionStart, script.startTime);
+ assert_greater_than_equal(script.startTime, entry.startTime)
+ assert_equals(script.window, window);
+ assert_equals(script.forcedStyleAndLayoutDuration, 0);
+ assert_equals(script.windowAttribution, "self");
+}, `LoAF script: ${invoker} ${invokerType},${label ? ` ${label}` : ''}`);
+
+}
+
+function test_self_user_callback(cb, invoker, label) {
+ test_loaf_script(cb, invoker, "user-callback", label);
+}
+
+function test_self_event_listener(cb, invoker) {
+ test_loaf_script(cb, invoker, "event-listener");
+}
+
+function test_promise_script(cb, resolve_or_reject, invoker, label) {
+ test_loaf_script(cb, invoker, `${resolve_or_reject}-promise`, label);
+}
+
+function test_self_script_block(cb, invoker, type) {
+ test_loaf_script(cb, invoker, type);
+}