summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/largest-contentful-paint/resources
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html12
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js22
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js173
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js8
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html15
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js128
-rw-r--r--testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py9
7 files changed, 367 insertions, 0 deletions
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html b/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html
new file mode 100644
index 0000000000..cd60025480
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Text</p>
+<script>
+ const observer = new PerformanceObserver(entryList => {
+ window.parent.triggerTest(entryList.getEntries()[0]);
+ });
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js b/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js
new file mode 100644
index 0000000000..bad078e35f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js
@@ -0,0 +1,22 @@
+async_test(t => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const observer = new PerformanceObserver(
+ t.step_func(entryList => {
+ entryList.getEntries().forEach(entry => {
+ // May receive a text entry. Ignore that entry.
+ if (!entry.url) {
+ return;
+ }
+ // The images should not have caused an entry, so fail test.
+ assert_unreached('Should not have received an entry! Received one with id '
+ + entryList.getEntries()[0].id);
+ });
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ // Images have been added but should not cause entries to be dispatched.
+ // Wait for 500ms and end test, ensuring no entry was created.
+ t.step_timeout(() => {
+ t.done();
+ }, 500);
+}, 'Images with opacity: 0, visibility: hidden, or display: none are not observable by LargestContentfulPaint.');
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
new file mode 100644
index 0000000000..3ac3705e8e
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
@@ -0,0 +1,173 @@
+const image_delay = 1000;
+const delay_pipe_value = image_delay / 1000;
+
+const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => {
+ let timeout_id;
+ const timeout = new Promise((_, reject) => {
+ timeout_id = step_timeout(() =>
+ reject(new DOMException(message, "TimeoutError")), delay)
+ });
+ let result = null;
+ try {
+ result = await Promise.race([promise, timeout]);
+ clearTimeout(timeout_id);
+ } finally {
+ cleanup();
+ }
+ return result;
+};
+
+// Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values.
+// The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry.
+// The |options| parameter may contain some string values specifying the following:
+// * 'renderTimeIs0': the renderTime should be 0 (image does not pass Timing-Allow-Origin checks).
+// When not present, the renderTime should not be 0 (image passes the checks).
+// * 'sizeLowerBound': the |expectedSize| is only a lower bound on the size attribute value.
+// When not present, |expectedSize| must be exactly equal to the size attribute value.
+// * 'approximateSize': the |expectedSize| is only approximate to the size attribute value.
+// This option is mutually exclusive to 'sizeLowerBound'.
+function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound, options = []) {
+ assert_equals(entry.name, '', "Entry name should be the empty string");
+ assert_equals(entry.entryType, 'largest-contentful-paint',
+ "Entry type should be largest-contentful-paint");
+ assert_equals(entry.duration, 0, "Entry duration should be 0");
+ // The entry's url can be truncated.
+ assert_equals(expectedUrl.substr(0, 100), entry.url.substr(0, 100),
+ `Expected URL ${expectedUrl} should at least start with the entry's URL ${entry.url}`);
+ assert_equals(entry.id, expectedID, "Entry ID matches expected one");
+ assert_equals(entry.element, document.getElementById(expectedID),
+ "Entry element is expected one");
+ if (options.includes('skip')) {
+ return;
+ }
+ if (options.includes('renderTimeIs0')) {
+ assert_equals(entry.renderTime, 0, 'renderTime should be 0');
+ assert_between_exclusive(entry.loadTime, timeLowerBound, performance.now(),
+ 'loadTime should be between the lower bound and the current time');
+ assert_approx_equals(entry.startTime, entry.loadTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ } else {
+ assert_between_exclusive(entry.loadTime, timeLowerBound, entry.renderTime,
+ 'loadTime should occur between the lower bound and the renderTime');
+ assert_greater_than_equal(performance.now(), entry.renderTime,
+ 'renderTime should occur before the entry is dispatched to the observer.');
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ }
+ if (options.includes('sizeLowerBound')) {
+ assert_greater_than(entry.size, expectedSize);
+ } else if (options.includes('approximateSize')) {
+ assert_approx_equals(entry.size, expectedSize, 1);
+ } else{
+ assert_equals(entry.size, expectedSize);
+ }
+
+ if (options.includes('animated')) {
+ assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime,
+ 'firstAnimatedFrameTime should be smaller than loadTime');
+ assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime,
+ 'firstAnimatedFrameTime should be smaller than renderTime');
+ assert_less_than(entry.firstAnimatedFrameTime, image_delay,
+ 'firstAnimatedFrameTime should be smaller than the delay applied to the second frame');
+ assert_greater_than(entry.firstAnimatedFrameTime, 0,
+ 'firstAnimatedFrameTime should be larger than 0');
+ }
+ if (options.includes('animated-zero')) {
+ assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0');
+ }
+}
+
+const load_and_observe = url => {
+ return new Promise(resolve => {
+ (new PerformanceObserver(entryList => {
+ for (let entry of entryList.getEntries()) {
+ if (entry.url == url) {
+ resolve(entryList.getEntries()[0]);
+ }
+ }
+ })).observe({ type: 'largest-contentful-paint', buffered: true });
+ const img = new Image();
+ img.id = 'image_id';
+ img.src = url;
+ document.body.appendChild(img);
+ });
+};
+
+const load_video_and_observe = url => {
+ return new Promise(resolve => {
+ (new PerformanceObserver(entryList => {
+ for (let entry of entryList.getEntries()) {
+ if (entry.url == url) {
+ resolve(entryList.getEntries()[0]);
+ }
+ }
+ })).observe({ type: 'largest-contentful-paint', buffered: true });
+ const video = document.createElement("video");
+ video.id = 'video_id';
+ video.src = url;
+ video.autoplay = true;
+ video.muted = true;
+ video.loop = true;
+ document.body.appendChild(video);
+ });
+};
+
+const getLCPStartTime = (identifier) => {
+ return new Promise(resolve => {
+ new PerformanceObserver((entryList, observer) => {
+ entryList.getEntries().forEach(e => {
+ if (e.url.includes(identifier)) {
+ resolve(e);
+ observer.disconnect();
+ }
+ });
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+}
+
+const getFCPStartTime = () => {
+ return performance.getEntriesByName('first-contentful-paint')[0];
+}
+
+const add_text = (text) => {
+ const paragraph = document.createElement('p');
+ paragraph.innerHTML = text;
+ document.body.appendChild(paragraph);
+}
+
+const loadImage = (url, shouldBeIgnoredForLCP = false) => {
+ return new Promise(function (resolve, reject) {
+ let image = document.createElement('img');
+ image.addEventListener('load', () => { resolve(image); });
+ image.addEventListener('error', reject);
+ image.src = url;
+ if (shouldBeIgnoredForLCP)
+ image.style.opacity = 0;
+ document.body.appendChild(image);
+ });
+}
+
+const checkLCPEntryForNonTaoImages = (times = {}) => {
+ const lcp = times['lcp'];
+ const fcp = times['fcp'];
+ const lcp_url_components = lcp.url.split('/');
+
+ if (lcp.loadTime <= fcp.startTime) {
+ assert_approx_equals(lcp.startTime, fcp.startTime, 0.001,
+ 'LCP start time should be the same as FCP for ' +
+ lcp_url_components[lcp_url_components.length - 1]) +
+ ' when LCP load time is less than FCP.';
+ } else {
+ assert_approx_equals(lcp.startTime, lcp.loadTime, 0.001,
+ 'LCP start time should be the same as LCP load time for ' +
+ lcp_url_components[lcp_url_components.length - 1]) +
+ ' when LCP load time is no less than FCP.';
+ }
+
+ assert_equals(lcp.renderTime, 0,
+ 'The LCP render time of Non-Tao image should always be 0.');
+}
+
+const raf = () => {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js
new file mode 100644
index 0000000000..c650a0b747
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js
@@ -0,0 +1,8 @@
+self.addEventListener("fetch", e => {
+ if (e.request.url.endsWith('green.svg')) {
+ e.respondWith(new Response(`<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50">
+ <rect fill="lime" width="100" height="50"/>
+ </svg>
+ `, { headers: { 'Content-Type': 'image/svg+xml' } }));
+ }
+});
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html
new file mode 100644
index 0000000000..069a50eae9
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+
+<body>
+ <script>
+ new PerformanceObserver(entries => {
+ window.parent.postMessage(entries.getEntries()[0].toJSON());
+ }).observe({ entryTypes: ["largest-contentful-paint"] });
+
+ const image = document.createElement("img");
+ image.src = "/images/green.svg";
+ image.id = "theImage";
+ document.body.appendChild(image);
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js b/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js
new file mode 100644
index 0000000000..1836f2e4ad
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js
@@ -0,0 +1,128 @@
+let counter = 0;
+const loadImage = size => {
+ return event => {
+ let zoom;
+ if (location.search.includes("replace")) {
+ zoom = document.getElementById("image");
+ } else {
+ zoom = new Image();
+ }
+ zoom.src=`/images/green-${size}.png`;
+ ++counter;
+ zoom.elementTiming = "zoom" + counter;
+ document.body.appendChild(zoom);
+ }
+};
+const loadBackgroundImage = size => {
+ return event => {
+ const div = document.createElement("div");
+ const [width, height] = size.split("x");
+ ++counter;
+ div.style = `background-image:
+ url(/images/green-${size}.png?${counter}); width: ${width}px; height: ${height}px`;
+ div.elementTiming = "zoom" + counter;
+ document.body.appendChild(div);
+ }
+};
+
+const registerMouseover = background => {
+ const image = document.getElementById("image");
+ const span = document.getElementById("span");
+ const func = background ? loadBackgroundImage : loadImage;
+ image.addEventListener("mouseover", func("100x50"));
+ span.addEventListener("mouseover", func("256x256"));
+}
+
+const dispatch_mouseover = () => {
+ span.dispatchEvent(new Event("mouseover"))
+};
+
+const wait_for_lcp_entries = async entries_expected => {
+ await new Promise(resolve => {
+ let entries_seen = 0;
+ const PO = new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (let entry of entries) {
+ if (entry.url) {
+ entries_seen++;
+ }
+ }
+ if (entries_seen == entries_expected) {
+ PO.disconnect();
+ resolve()
+ } else if (entries_seen > entries_expected) {
+ PO.disconnect();
+ reject();
+ }
+ });
+ PO.observe({type: "largest-contentful-paint", buffered: true});
+ });
+};
+const wait_for_element_timing_entry = async identifier => {
+ await new Promise(resolve => {
+ const PO = new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (let entry of entries) {
+ if (entry.identifier == identifier) {
+ PO.disconnect();
+ resolve()
+ }
+ }
+ });
+ PO.observe({type: "element", buffered: true});
+ });
+};
+const wait_for_resource_timing_entry = async name => {
+ await new Promise(resolve => {
+ const PO = new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (let entry of entries) {
+ if (entry.name.includes(name)) {
+ PO.disconnect();
+ resolve()
+ }
+ }
+ });
+ PO.observe({type: "resource", buffered: true});
+ });
+};
+
+const run_mouseover_test = background => {
+ promise_test(async t => {
+ // await the first LCP entry
+ await wait_for_lcp_entries(1);
+ // Hover over the image
+ registerMouseover(background);
+ if (test_driver) {
+ await new test_driver.Actions().pointerMove(0, 0, {origin: image}).send();
+ }
+ if (!background) {
+ await wait_for_element_timing_entry("zoom1");
+ } else {
+ await wait_for_resource_timing_entry("png?1");
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ // There's only a single LCP entry, because the zoom was skipped.
+ await wait_for_lcp_entries(1);
+
+ // Wait 600 ms as the heuristic is 500 ms.
+ // This will no longer be necessary once the heuristic relies on Task
+ // Attribution.
+ await new Promise(r => step_timeout(r, 600));
+
+ // Hover over the span.
+ if (test_driver) {
+ await new test_driver.Actions().pointerMove(0, 0, {origin: span}).send();
+ }
+ if (!background) {
+ await wait_for_element_timing_entry("zoom2");
+ } else {
+ await wait_for_resource_timing_entry("png?2");
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ // There are 2 LCP entries, as the image loaded due to span hover is a
+ // valid LCP candidate.
+ await wait_for_lcp_entries(2);
+ }, `LCP mouseover heuristics ignore ${background ?
+ "background" : "element"}-based zoom widgets`);
+}
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py b/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py
new file mode 100644
index 0000000000..780d5736c4
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py
@@ -0,0 +1,9 @@
+import time
+
+def main(request, response):
+ time.sleep(1)
+ return [ ("Content-Type", "text/css")], """
+ #text {
+ font-size: 4em;
+ }
+ """