diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/largest-contentful-paint | |
parent | Initial commit. (diff) | |
download | firefox-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/largest-contentful-paint')
75 files changed, 3255 insertions, 0 deletions
diff --git a/testing/web-platform/tests/largest-contentful-paint/META.yml b/testing/web-platform/tests/largest-contentful-paint/META.yml new file mode 100644 index 0000000000..e11810cc10 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/META.yml @@ -0,0 +1,4 @@ +spec: https://w3c.github.io/largest-contentful-paint/ +suggested_reviewers: + - npm1 + - yoavweiss diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html new file mode 100644 index 0000000000..a2c0d7975a --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe image.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 136 is the size of the animated GIF up until the first frame. + // The trickle pipe delays the response after the first frame by 1 second. + const url = window.location.origin + + `/images/anim-gr.gif?pipe=trickle(136:d${delay_pipe_value})`; + const entry = await load_and_observe(url); + // anim-gr.gif is 100 by 50. + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html new file mode 100644 index 0000000000..de59d5c5f7 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe image.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 142 is the size of the animated WebP up until the first frame. + // The trickle pipe delays the response after the first frame by 1 second. + const url = window.location.origin + + `/images/webp-animated.webp?pipe=trickle(142:d${delay_pipe_value})`; + const entry = await load_and_observe(url); + // webp-animated.webp is 11 by 29. + const size = 11 * 29; + checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html new file mode 100644 index 0000000000..cf7d262b0f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe image.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 262 is the size of the animated PNG up until the first frame, + // including the chunk that starts the second frame (indicating that + // the first frame data is done). + // The trickle pipe delays the response after the first frame by 1 second. + const url = window.location.origin + + `/images/anim-gr.png?pipe=trickle(262:d${delay_pipe_value})`; + const entry = await load_and_observe(url); + // anim-gr.png is 100 by 50. + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html new file mode 100644 index 0000000000..993883c607 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe image.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 262 is the size of the animated PNG up until the first frame, + // including the chunk that starts the second frame (indicating that + //the first frame data is done). + const {REMOTE_ORIGIN} = get_host_info(); + const url = REMOTE_ORIGIN + + '/images/anim-gr.png?pipe=trickle(262:d1)'; + const entry = await load_and_observe(url); + // anim-gr.png is 100 by 50. + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad, ["renderTimeIs0", "animated-zero"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html new file mode 100644 index 0000000000..137dde6638 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe image.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 262 is the size of the animated PNG up until the first frame, + // including the chunk that starts the second frame (indicating that + //the first frame data is done). + const {REMOTE_ORIGIN} = get_host_info(); + const url = REMOTE_ORIGIN + + '/images/anim-tao.png?pipe=trickle(262:d1)'; + const entry = await load_and_observe(url); + // anim-gr.png is 100 by 50. + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html new file mode 100644 index 0000000000..6bbc0958b1 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe image.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 262 is the size of the animated PNG up until the first frame, + // including the chunk that starts the second frame (indicating that + //the first frame data is done). + const url = window.location.origin + '/images/blue.png'; + const entry = await load_and_observe(url); + // blue.png is 133 by 106. + const size = 133 * 106; + checkImage(entry, url, 'image_id', size, beforeLoad, ["animated-zero"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-video.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-video.tentative.html new file mode 100644 index 0000000000..49bdd986f6 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-video.tentative.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Largest Contentful Paint: observe video.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/largest-contentful-paint-helpers.js"></script> +</head> +<body> + <script> + promise_test(async () => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + // 136 is the size of the animated GIF up until the first frame. + // The trickle pipe delays the response after the first frame by 1 second. + const url = window.location.origin + + `/media/test-1s.webm?pipe=trickle(1500:d${delay_pipe_value})`; + const entry = await load_video_and_observe(url); + // Video is 320 x 184. + const size = 320 * 184; + // TODO(yoav): Validate size as well as load and render times. "skip" is + // currently causing those checks to be skipped. + checkImage(entry, url, 'video_id', size, beforeLoad, ["skip"]); + }, "Same origin animated image is observable and has a first frame."); + </script> +</body> +</html> + diff --git a/testing/web-platform/tests/largest-contentful-paint/broken-image-icon.html b/testing/web-platform/tests/largest-contentful-paint/broken-image-icon.html new file mode 100644 index 0000000000..be27e34e6a --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/broken-image-icon.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Broken Image Icon Should Not Be LCP</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> + +<body> + <img src="../non-existent-image.jpg"> + <img src="/css/css-images/support/colors-16x8.png"> + + <script> + promise_test(async () => { + // Wait for the first-contentful-paint entry so that we know the broken image + // icon has been painted. + await new Promise(resolve => { + new PerformanceObserver((entryList, observer) => { + if (entryList.getEntriesByName('first-contentful-paint').length > 0) { + observer.disconnect(); + resolve(); + } + }).observe({ type: 'paint', buffered: true }); + }); + + // There should be only 1 LCP entry and it should be the colors-16x8.png though + // being smaller than the broken image icon. The broken image icon should not + // emit an LCP entry. + let LcpEntryListLength = await new Promise(resolve => { + new PerformanceObserver((entryList, observer) => { + if (entryList.getEntries().filter(e => e.url.includes('colors-16x8.png'))) { + observer.disconnect(); + resolve(entryList.getEntries().length); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + + assert_equals(LcpEntryListLength, 1, 'There should be one and only one LCP entry.'); + + }, "The broken image icon should not emit an LCP entry."); + </script> +</body> + +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/largest-contentful-paint/contracted-image.html b/testing/web-platform/tests/largest-contentful-paint/contracted-image.html new file mode 100644 index 0000000000..8816bf4ba9 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/contracted-image.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: contracted image bounded by display size.</title> +<style type="text/css"> + #image_id { + width: 50px; + height: 50px; + } +</style> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/black-rectangle.png'; + // black-rectangle.png is 100 x 50. It occupies 50 x 50 so size will be bounded by the displayed size. + const size = 50 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Largest Contentful Paint: |size| attribute is bounded by display size.'); +</script> +<img src='/images/black-rectangle.png' id='image_id'/> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/cross-origin-image.sub.html b/testing/web-platform/tests/largest-contentful-paint/cross-origin-image.sub.html new file mode 100644 index 0000000000..0cfdd1791b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/cross-origin-image.sub.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe cross-origin images but without renderTime.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = 'http://{{domains[www]}}:{{ports[http][1]}}/images/blue.png'; + // blue.png is 133 x 106. + const size = 133 * 106; + checkImage(entry, url, 'image_id', size, beforeLoad, ['renderTimeIs0']); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Cross-origin image is observable, with renderTime equal to 0.'); +</script> + +<img src='http://{{domains[www]}}:{{ports[http][1]}}/images/blue.png' id='image_id'/> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/element-only-when-fully-active.html b/testing/web-platform/tests/largest-contentful-paint/element-only-when-fully-active.html new file mode 100644 index 0000000000..519b249196 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/element-only-when-fully-active.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: element is only exposed for fully active documents.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<iframe src="resources/iframe-stores-entry.html" id="ifr"></iframe> +<script> + setup({"hide_test_state": true}); + let t = async_test('Only expose element attribute for fully active documents'); + window.triggerTest = t.step_func_done(entry => { + assert_not_equals(entry.element, null); + const iframe = document.getElementById('ifr'); + iframe.remove(); + assert_equals(entry.element, null); + }); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/expanded-image.html b/testing/web-platform/tests/largest-contentful-paint/expanded-image.html new file mode 100644 index 0000000000..90f803930c --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/expanded-image.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: expanded image bounded by intrinsic size.</title> +<style type="text/css"> + #image_id { + width: 300px; + height: 300px; + } +</style> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/black-rectangle.png'; + // black-rectangle.png is 100 x 50. It occupies 300 x 300 so size will be bounded by the intrinsic size. + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Largest Contentful Paint: |size| attribute is bounded by intrinsic size.'); +</script> +<img src='/images/black-rectangle.png' id='image_id'/> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/first-letter-background.html b/testing/web-platform/tests/largest-contentful-paint/first-letter-background.html new file mode 100644 index 0000000000..56ac105677 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/first-letter-background.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe element with background image in its first letter</title> +<body> +<style> +div::first-letter { + background-image: url('/images/black-rectangle.png'); +} +div { + font-size: 12px; +} +</style> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad = performance.now(); + let observedFirstLetter = false; + const observer = new PerformanceObserver( + t.step_func(function(entryList) { + const entry = entryList.getEntries()[entryList.getEntries().length -1]; + if (!observedFirstLetter) { + // When we haven't observed first-letter as LCP... + // If we happen to get a text entry due to text happening before the image, return. + if (entry.url === '') { + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeLoad); + assert_greater_than_equal(performance.now(), entry.renderTime); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0); + assert_equals(entry.loadTime, 0); + assert_equals(entry.id, 'target'); + assert_equals(entry.element, document.getElementById('target')); + } else { + const url = window.location.origin + '/images/black-rectangle.png'; + checkImage(entry, url, 'target', 0, beforeLoad, ['sizeLowerBound']); + } + + // Now change the div content to proceed to the second part of the test. + beforeLoad = performance.now(); + const div = document.createElement('div'); + div.id = 'target2'; + div.innerHTML = 'long text will now be LCP'; + document.body.appendChild(div); + observedFirstLetter = true; + } else { + // Ignore entries that are caused by the initial 'target'. + if (entry.id === 'target') + return; + // The LCP must now be text. + if (entry.url !== '') + assert_unreached('First-letter background should not be LCP!'); + + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeLoad, 'blaaa'); + assert_greater_than_equal(performance.now(), entry.renderTime, 'bleee'); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0); + assert_equals(entry.id, 'target2'); + const div = document.getElementById('target2'); + // Estimate the text size: 12 * 100 + assert_greater_than_equal(entry.size, 1200); + assert_equals(entry.loadTime, 0); + assert_equals(entry.element, div); + t.done(); + } + })); + observer.observe({entryTypes: ['largest-contentful-paint']}); + }, 'Largest Contentful Paint: first-letter is observable.'); +</script> +<div id='target'>A</div> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/first-paint-equals-lcp-text.html b/testing/web-platform/tests/largest-contentful-paint/first-paint-equals-lcp-text.html new file mode 100644 index 0000000000..50bccd072e --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/first-paint-equals-lcp-text.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>LargestContentfulPaint compared with FirstPaint and FirstContentfulPaint on single text page.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.PerformancePaintTiming, "PerformancePaintTiming is not implemented"); + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let firstPaintTime = 0; + let firstContentfulPaintTime = 0; + let largestContentfulPaintTime = 0; + const observer = new PerformanceObserver( + t.step_func(function(entryList) { + entryList.getEntries().forEach(entry => { + if (entry.name === 'first-paint') { + assert_equals(firstPaintTime, 0, 'Only one first-paint entry.'); + assert_equals(entry.entryType, 'paint'); + firstPaintTime = entry.startTime; + } else if (entry.name === 'first-contentful-paint') { + assert_equals(firstContentfulPaintTime, 0, 'Only one first-contentful-paint entry.'); + assert_equals(entry.entryType, 'paint'); + firstContentfulPaintTime = entry.startTime; + } else { + assert_equals(largestContentfulPaintTime, 0, 'Only one largest-contentful-paint entry.'); + assert_equals(entry.entryType, 'largest-contentful-paint'); + largestContentfulPaintTime = entry.renderTime; + } + // LCP fires necessarily after first-paint and first-contentful-paint. + if (largestContentfulPaintTime) { + assert_equals(firstContentfulPaintTime, largestContentfulPaintTime, 'FCP should equal LCP.'); + // In PaintTiming spec, first-paint isn't a hard requirement, browsers can support + // first-contentful-paint only. + if (firstPaintTime) { + assert_less_than_equal(firstPaintTime, firstContentfulPaintTime, 'FP should be less than or equal to FCP.'); + } + t.done(); + } + }); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + observer.observe({type: 'paint', buffered: true}); + }, 'FCP and LCP are the same when there is a single text element in the page.'); +</script> +<p>Text</p> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/idlharness.html b/testing/web-platform/tests/largest-contentful-paint/idlharness.html new file mode 100644 index 0000000000..5f5d286b35 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/idlharness.html @@ -0,0 +1,30 @@ +<!doctype html> +<title>Largest Contentful Paint IDL tests</title> +<link rel="help" href="https://wicg.github.io/largest-contentful-paint/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script> +'use strict'; + +idl_test( + ['largest-contentful-paint'], + ['performance-timeline', 'dom', 'hr-time'], + async (idl_array, t) => { + idl_array.add_objects({ + LargestContentfulPaint: ['lcp'] + }); + + window.lcp = await new Promise((resolve, reject) => { + const observer = new PerformanceObserver(entryList => { + resolve(entryList.getEntries()[0]); + }); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + t.step_timeout(() => reject('Timed out waiting for LargestContentfulPaint entry'), 3000); + }); + } +); +</script> +<!-- a contentful element to observe --> +<img src=/images/lcp-100x50.png> diff --git a/testing/web-platform/tests/largest-contentful-paint/iframe-content-not-observed.html b/testing/web-platform/tests/largest-contentful-paint/iframe-content-not-observed.html new file mode 100644 index 0000000000..e605e9f21f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/iframe-content-not-observed.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<head> +<title>Largest Contentful Paint: do NOT observe elements from same-origin iframes</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + async_test((t) => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const observer = new PerformanceObserver( + t.step_func_done(entryList => { + assert_unreached("Should not have received an entry!"); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + // After a delay, assume that no entry was produced. + t.step_timeout(() => { + t.done(); + }, 200); + }, 'Element in child iframe is not observed, even if same-origin.'); +</script> +<iframe src='resources/iframe-with-content.html'></iframe> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-TAO.sub.html b/testing/web-platform/tests/largest-contentful-paint/image-TAO.sub.html new file mode 100644 index 0000000000..296fe5e65b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-TAO.sub.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe cross origin images with various Timing-Allow-Origin headers</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<div id='my_div'></div> +<script> + setup({"hide_test_state": true}); + async_test(t => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const remote_img = 'http://{{domains[www]}}:{{ports[http][1]}}/element-timing/resources/TAOImage.py?' + + 'origin=' + window.location.origin +'&tao='; + const valid_tao = ['wildcard', 'origin', 'multi', 'multi_wildcard', 'match_origin', 'match_wildcard']; + const invalid_tao = ['null', 'space', 'uppercase']; + const div = document.getElementById('my_div'); + let img_length = 20; + function addImage(tao) { + const img = document.createElement('img'); + img.src = remote_img + tao; + img.id = tao; + // Set increasing size so that largest-contentful-paint captures all of them. + img_length++; + img.height = img_length; + img.width = img_length; + div.appendChild(img); + } + let img_count = 0; + const total_images = valid_tao.length + invalid_tao.length; + let beforeLoad; + new PerformanceObserver( + t.step_func(entryList => { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const tao = entry.id; + const url = remote_img + tao; + const size = img_length * img_length; + let options = valid_tao.includes(tao) ? [] : ['renderTimeIs0']; + checkImage(entry, url, tao, size, beforeLoad, options); + img_count++; + beforeLoad = performance.now(); + // Process valid TAO images first. + if (img_count < valid_tao.length) + addImage(valid_tao[img_count]); + // Then add invalid TAO images. + else if (img_count < total_images) + addImage(invalid_tao[img_count - valid_tao.length]); + // Once we've seen all the images, end the test. + else + t.done(); + }) + ).observe({type: 'largest-contentful-paint'}); + // Add first image, the rest will be added on each observer callback. + addImage(valid_tao[0]); + beforeLoad = performance.now(); + }, 'Cross-origin elements with valid TAO have correct renderTime, with invalid TAO have renderTime set to 0.'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-full-viewport.html b/testing/web-platform/tests/largest-contentful-paint/image-full-viewport.html new file mode 100644 index 0000000000..2f8e17fb32 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-full-viewport.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: size when image overflows</title> +<!-- In this test, an image with an intrinsic size of 100 x 50 is added, but + scaled up in order to overflow the viewport. It should not be reported. --> +<body> +<style> +body { + margin: 0px; +} +</style> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement.clientHeight; + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1, 'Should have received only one entry!'); + const entry = entryList.getEntries()[0]; + if (entry.url) + assert_unreached('Should not have received an image entry!'); + }) + ).observe({type: 'largest-contentful-paint', buffered: true}); + // Add an image, setting width and height equal to viewport. + img = document.createElement('img'); + img.src = '/images/lcp-100x50.png'; + img.id = 'image_id'; + img.width = viewportWidth * 2; + img.height = viewportHeight * 2; + img.onload = () => { + const p = document.createElement('p'); + p.innerHTML = 'a'; + p.style = 'position: absolute; top: 10px; left: 10px;'; + document.body.appendChild(p); + } + document.body.appendChild(img); + }, 'The intersectionRect of an img element overflowing is computed correctly'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-inside-svg.html b/testing/web-platform/tests/largest-contentful-paint/image-inside-svg.html new file mode 100644 index 0000000000..77e42fcc6d --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-inside-svg.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe image inside SVG</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> +setup({"hide_test_state": true}); +async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + new PerformanceObserver( + t.step_func_done(entryList => { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/blue.png'; + // blue.png is 133 by 106. + const size = 133 * 106; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ).observe({type: 'largest-contentful-paint', buffered: true}); +}, "Image inside SVG is observable."); +</script> +<svg width="300" height="300" id='svg_id'> + <image href='/images/blue.png' id='image_id'/> +</svg> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html b/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html new file mode 100644 index 0000000000..d66dc4a520 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<meta name="viewport" content="width=device-width, minimum-scale=1"> +<title>Largest Contentful Paint: size when image overflows</title> +<body> +<style> +body { + /* Preventing a scrollbar from showing and removing any margins simplifies + the calculations below. */ + overflow: hidden; + margin: 0px; +} +</style> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + setup({"hide_test_state": true}); + let beforeRender; + const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement.clientHeight; + const imgWidth = 100; + const imgHeight = 50; + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/lcp-100x50.png'; + // To compute the size, compute the percentage of the image visible and + // scale by its natural dimensions. In this test, the image is expanded to twice its size + // but place towards the bottom right corner of the viewport, so it is + // effectively clipped to 50% by 50% of its display size. Scaling by + // its natural width and height of 100px and 50px respectively, leads + // to a weighted size of 50 by 25. + const truncatedWidth = imgWidth / 2; + const truncatedHeight = imgHeight / 2; + const weightedSize = truncatedWidth * truncatedHeight; + checkImage(entry, url, 'image_id', weightedSize, beforeLoad); + }) + ).observe({type: 'largest-contentful-paint', buffered: true}); + // Add an image, setting width and height equal to viewport. + img = document.createElement('img'); + img.src = '/images/lcp-100x50.png'; + img.id = 'image_id'; + img.width = imgWidth * 2; + img.height = imgHeight * 2; + img.style.position = 'absolute'; + img.style.left = viewportWidth - imgWidth + 'px'; + img.style.top = viewportHeight - imgHeight + 'px'; + document.body.appendChild(img); + }, 'The intersectionRect of an img element overflowing is computed correctly'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-removed-before-load.html b/testing/web-platform/tests/largest-contentful-paint/image-removed-before-load.html new file mode 100644 index 0000000000..08e5ee56db --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-removed-before-load.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: largest image is reported.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<img id="target"/> +<img id="target2"/> +<script> + setup({"hide_test_state": true}); + const numInitial = 100; + const sleep = 1000; + const small_img_src = '/images/lcp-16x16.png'; + let beforeLoad; + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const img_src = '/element-timing/resources/progressive-image.py?name=square20.jpg&numInitial=' + + numInitial + '&sleep=' + sleep; + const img1 = document.getElementById('target') + img1.src = img_src; + // After a brief wait, remove the image and add a smaller image to target2. + t.step_timeout(() => { + img1.parentNode.removeChild(img1); + document.getElementById('target2').src = small_img_src; + beforeLoad = performance.now(); + }, 0); + new PerformanceObserver( + t.step_func(entryList => { + let images = entryList.getEntries().filter(e => e.id !== ''); + if (!images.length) + return; + assert_equals(images.length, 1, 'Should only receive one entry'); + const entry = images[0]; + checkImage(images[0], window.location.origin + small_img_src, 'target2', 16 * 16, + beforeLoad); + t.done(); + }) + ).observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Largest Contentful Paint: image removed before loaded does not produce entry.'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-src-change.html b/testing/web-platform/tests/largest-contentful-paint/image-src-change.html new file mode 100644 index 0000000000..33213a570e --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-src-change.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: src change triggers new entry.</title> + +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/largest-contentful-paint-helpers.js"></script> + <img src='' id='image_id' /> + <script> + setup({ "hide_test_state": true }); + + let first_image_src = '/images/black-rectangle.png'; + let second_image_src = '/images/blue.png'; + let image_id = 'image_id'; + + // Add listener for load event that is fired when image is loaded. + const image_load_promise = image_element => { + return new Promise(resolve => { + image_element.addEventListener('load', resolve); + }); + } + + // Create a promise that resolves when an LCP is observed. + const lcp_observation_promise = image_src => { + return new Promise(resolve => { + new PerformanceObserver((entryList) => { + let lcpEntry = entryList.getEntries().find(e => e.url.includes(image_src)); + + if (lcpEntry) { + resolve(lcpEntry); + } + + }).observe({ type: 'largest-contentful-paint' }); + }); + } + + const loadImageAndGetLCPEntry = async image_src => { + let LCPObserverPromise = lcp_observation_promise(image_src); + + let image_element = document.getElementById(image_id); + + let promise = image_load_promise(image_element); + + image_element.src = image_src; + + await promise; + + return await LCPObserverPromise; + } + + promise_test(async t => { + + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + + + // Load first image. + let beforeLoad = performance.now(); + + let first_LCP = await loadImageAndGetLCPEntry(first_image_src); + + // Verify first LCP entry correctness. The black-rectangle.png is 100 x 50. + checkImage(first_LCP, window.location.origin + first_image_src, image_id, 100 * 50, beforeLoad); + + // Load second image. + beforeLoad = performance.now(); + + let second_LCP = await loadImageAndGetLCPEntry(second_image_src); + + // Verify second LCP entry correctness. The blue.png is 133 by 106. + checkImage(second_LCP, window.location.origin + second_image_src, image_id, 133 * 106, beforeLoad); + + }, 'Largest Contentful Paint: changing src causes a new entry to be dispatched.'); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-sw-same-origin.https.html b/testing/web-platform/tests/largest-contentful-paint/image-sw-same-origin.https.html new file mode 100644 index 0000000000..3f375008c5 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-sw-same-origin.https.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: same-origin service worker should not be treated as TAO-fail</title> + +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/largest-contentful-paint-helpers.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + <script> + setup(() => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + }); + + promise_test(async t => { + const scope = "resources/lcp-sw.https.html"; + + const registration = + await service_worker_unregister_and_register(t, "resources/lcp-sw-from-cache.js", "resources/"); + + await wait_for_state(t, registration.installing, "activated"); + t.add_cleanup(() => registration.unregister()); + const iframe = document.createElement("iframe"); + iframe.src = scope; + document.body.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + const entry = await new Promise(resolve => window.addEventListener("message", e => resolve(e.data))); + + assert_equals(entry.id, "theImage"); + assert_not_equals(entry.renderTime, 0); + }, "Same-origin images served from a service-worker should have a correct renderTime"); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html b/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html new file mode 100644 index 0000000000..5de0790281 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html @@ -0,0 +1,128 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: Largest image is reported.</title> +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/largest-contentful-paint-helpers.js"></script> + <script src="/common/utils.js"></script> + <script> + setup({"hide_test_state": true}); + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint not implemented"); + const imageURL = `${window.location.origin}/images/blue.png`; + + async function load_image_and_get_lcp_size(t, imageStyle = {}, containerStyle = {}) { + const popup = window.open(); + t.add_cleanup(() => popup.close()); + const image = popup.document.createElement('img'); + image.src = imageURL; + + // We decode the image to get the natural size (though it's a constant) + await image.decode(); + const naturalSize = image.width * image.height; + const container = popup.document.createElement('div'); + container.appendChild(image); + + const applyStyle = (el, style = {}) => + Object.entries(style).forEach(([k, v]) => el.style.setProperty(k, v)); + applyStyle(image, imageStyle); + applyStyle(container, containerStyle); + image.id = token(); + container.id = token(); + + const entryReported = new Promise(resolve => + new popup.PerformanceObserver(entryList => { + entryList.getEntries().forEach(entry => { + if (entry.id === image.id || entry.id === container.id) { + resolve(entry.size); + } + }); + }).observe({ type: 'largest-contentful-paint', buffered: true }) + ); + + popup.document.body.appendChild(container); + + return { + lcpSize: await entryReported, + naturalSize + }; + } + + /* We set the image to display: none when testing background, + so that only the background is reported + and not the image itself */ + const load_background_image_and_get_lcp_size = (t, style) => + load_image_and_get_lcp_size(t, { display: 'none' }, { + position: 'absolute', + 'background-image': `url(${imageURL})`, + ...style, + }); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t); + assert_equals(lcpSize, naturalSize); + }, 'Non-scaled image should report the natural size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t, + { width: '50px', height: '50px' }); + assert_equals(lcpSize, 50 * 50); + }, 'A downscaled image (width/height) should report the displayed size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t, + { transform: 'scale(0.5)' }); + assert_equals(Math.floor(lcpSize), Math.floor(naturalSize / 4)); + }, 'A downscaled image (using scale) should report the displayed size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t, + { width: '500px', height: '500px' }); + assert_equals(lcpSize, naturalSize); + }, 'An upscaled image (width/height) should report the natural size'); + + /* TODO(crbug.com/1484431): + Need to dig deeper into this test, to verify the implementation of scale() + Currently unable to upscale image*/ + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t, + { transform: 'scale(1.0)' }); + assert_equals(Math.floor(lcpSize), Math.floor(naturalSize)); + }, 'An upscaled image (using scale) should report the natural size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t, + { 'object-size': '300px 300px' }); + assert_equals(Math.floor(lcpSize), Math.floor(naturalSize)); + }, 'An upscaled image (using object-size) should report the natural size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_image_and_get_lcp_size(t, + { 'object-position': '-100px 0' }); + assert_equals(lcpSize, 3498); + }, 'Intersecting element with partial-intersecting image to report image intersection'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_background_image_and_get_lcp_size(t, + { width: '50px', height: '50px' }); + assert_equals(lcpSize, 50 * 50); + }, 'A background image larger than the container should report the container size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_background_image_and_get_lcp_size(t, + { width: '300px', height: '300px' }); + assert_equals(lcpSize, naturalSize); + }, 'A background image smaller than the container should report the natural size'); + + promise_test(async t => { + const { naturalSize, lcpSize } = await load_background_image_and_get_lcp_size(t, + { + width: '300px', + height: '300px', + 'background-size': '10px 10px', + 'background-repeat': 'no-repeat' + }); + assert_equals(lcpSize, 100); + }, 'A scaled-down background image should report the background size'); + </script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/largest-contentful-paint/initially-invisible-images.html b/testing/web-platform/tests/largest-contentful-paint/initially-invisible-images.html new file mode 100644 index 0000000000..061a0140d1 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/initially-invisible-images.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<head> +<title>Largest Contentful Paint: initially out-of-viewport image gets an LCP entry once they are visible.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + .flex-container { + display: flex; + flex-direction: row; + width: 1000px; + overflow-x: hidden; + scroll-behavior: auto; + } +</style> +</head> +<body> +<div> +<div class='flex-container' id="container"> + <img src='/images/lcp-100x50.png?pipe=trickle(d1)' width="1000" height="1000"/> + <img src='/images/lcp-1x1.png?1' width="1000" height="1000"/> + <img src='/images/lcp-1x1.png?2' width="1000" height="1000"/> + <img src='/images/lcp-1x1.png?3' width="1000" height="1000"/> +</div> +</div> +<script> +// Spin the carousel +setup({"hide_test_state": true}); +const images = document.querySelectorAll('img'); + +let selected = 0; +const container = document.getElementById("container"); +const transition = () => { + container.scrollLeft = selected * 1000; + selected = (selected + 1) % images.length; +} + +container.scrollLeft=1000; +setInterval(transition, 1000); + +promise_test(async t => { + + return new Promise(resolve => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const observer = new PerformanceObserver(entryList => { + entryList.getEntries().forEach(entry => { + // May receive a text entry. Ignore that entry. + if (!entry.url) { + return; + } + assert_true(entry.url.includes("lcp-100x50.png"), "Re-visible image has an entry"); + resolve(); + }); + }); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + t.step_timeout(() => { + assert_unreached("The image should have become visible by now, which should have triggered an LCP entry."); + t.done(); + }, 2000); + }); +}, 'Image visibility: out-of-viewport images are observable by LargestContentfulPaint once they become visible.'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-1.html b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-1.html new file mode 100644 index 0000000000..6b33c425b7 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-1.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<title>Largest Contentful Paint: invisible images are not observable</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/invisible-images.js"></script> +<style> + .opacity0 { + opacity: 0; + } + .visibilityHidden { + visibility: hidden; + } + .displayNone { + display: none; + } + .willChangeTransform { + will-change: transform; + } + .willChangeOpacity { + will-change: opacity; + } +</style> +<script> +setup({"hide_test_state": true}); +</script> +<img src='/images/blue.png' class='opacity0 willChangeTransform' id='opacity0-willChangeTransform'/> +<img src='/images/green.png' class='visibilityHidden willChangeTransform' id='visibilityHidden'/> +<img src='/images/red.png' class='displayNone willChangeTransform' id='displayNone'/> +<img src='/images/blue.png' class='opacity0 willChangeOpacity' id='opacity0-willChangeOpacity'/> +<div class='opacity0 composited'><img src='/images/yellow.png' id='divOpacity0'/></div> +<div class='visibilityHidden composited'><img src='/images/yellow.png' id='divVisibilityHidden'/></div> +<div class='displayNone composited'><img src='/images/yellow.png' id='divDisplayNone'/></div> diff --git a/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-2.html b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-2.html new file mode 100644 index 0000000000..8ab32ebb1f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-2.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<title>Largest Contentful Paint: invisible images are not observable</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/invisible-images.js"></script> +<style> + .opacity0 { + opacity: 0; + } + .visibilityHidden { + visibility: hidden; + } + .displayNone { + display: none; + } + .composited { + will-change: transform; + } + img { + border: 2px solid black; + will-change: transform; + } +</style> +<div class='opacity0 composited'><img src='/images/yellow.png' id='divOpacity0'/></div> +<div class='visibilityHidden composited'><img src='/images/yellow.png' id='divVisibilityHidden'/></div> +<div class='displayNone composited'><img src='/images/yellow.png' id='divDisplayNone'/></div> diff --git a/testing/web-platform/tests/largest-contentful-paint/invisible-images.html b/testing/web-platform/tests/largest-contentful-paint/invisible-images.html new file mode 100644 index 0000000000..997d70f777 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/invisible-images.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<title>Largest Contentful Paint: invisible images are not observable</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/invisible-images.js"></script> +<style> + .opacity0 { + opacity: 0; + } + .visibilityHidden { + visibility: hidden; + } + .displayNone { + display: none; + } +</style> +<img src='/images/blue.png' class='opacity0' id='opacity0'/> +<img src='/images/green.png' class='visibilityHidden' id='visibilityHidden'/> +<img src='/images/red.png' class='displayNone' id='displayNone'/> +<div class='opacity0'><img src='/images/yellow.png' id='divOpacity0'/></div> +<div class='visibilityHidden'><img src='/images/yellow.png' id='divVisibilityHidden'/></div> +<div class='displayNone'><img src='/images/yellow.png' id='divDisplayNone'/></div> diff --git a/testing/web-platform/tests/largest-contentful-paint/larger-image.html b/testing/web-platform/tests/largest-contentful-paint/larger-image.html new file mode 100644 index 0000000000..9415cbc32f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/larger-image.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: largest image is reported.</title> + +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/largest-contentful-paint-helpers.js"></script> + <!-- There is some text and some images. We care about lcp-133x106.png being reported, as it is the largest. --> + <p>This is some text! :)</p> + <img src='' id='lcp1' /> + <img src='' id='lcp2' /> + <img src='' id='lcp3' /> + <p>More text!</p> + <script> + // Add listener for load event that is fired when image is loaded. + function image_load_promise(image_element) { + return new Promise(resolve => { + image_element.addEventListener('load', resolve); + }); + } + + promise_test(async (t) => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + + let promise = image_load_promise(document.getElementById('lcp1')); + document.getElementById('lcp1').src = '/images/lcp-100x50.png'; + await promise; + + const beforeLoad = performance.now(); + + promise = image_load_promise(document.getElementById('lcp2')); + document.getElementById('lcp2').src = '/images/lcp-133x106.png'; + await promise; + + promise = image_load_promise(document.getElementById('lcp3')); + document.getElementById('lcp3').src = '/images/lcp-100x50-alt.png'; + await promise; + + const observer = new PerformanceObserver( + t.step_func(entryList => { + entryList.getEntries().forEach(entry => { + // The text or other image could be reported as LCP if it is rendered before the larger image. + if (entry.id !== 'lcp2') + return; + + const url = window.location.origin + '/images/lcp-133x106.png'; + const size = 133 * 106; + checkImage(entry, url, 'lcp2', size, beforeLoad); + t.done(); + }) + }) + ); + observer.observe({ type: 'largest-contentful-paint', buffered: true }); + }, 'Largest Contentful Paint: largest image is reported.'); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/larger-text.html b/testing/web-platform/tests/largest-contentful-paint/larger-text.html new file mode 100644 index 0000000000..c577899ecc --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/larger-text.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: largest text is reported.</title> + +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <style type="text/css"> + #text2 { + position: absolute; + width: auto; + white-space: nowrap; + } + + </style> + <!-- These are some text and some tiny images. We care about the largest text. --> + <img id='image1' /> + <div id='text1'></div> + <div id='text2'></div> + <img id='image2' /> + <script> + const load_image = async (id, url) => { + await new Promise(resolve => { + const image = document.getElementById(id); + image.addEventListener('load', resolve); + image.src = url; + }); + } + + const load_text = (id, text) => { + let div = document.getElementById(id); + div.innerHTML = text; + } + + promise_test(async (t) => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeRender = performance.now(); + + // Load images and add texts. + await load_image('image1', '/images/lcp-1x1.png'); + + load_text('text1', 'This is some text.'); + + load_text('text2', 'This is more text so it will be the Largest Contentful Paint!'); + + await load_image('image2', '/images/lcp-2x2.png'); + + await new Promise(resolve => { + new PerformanceObserver( + (entryList, observer) => { + entryList.getEntries().forEach(entry => { + // The tiny images or text1 could be reported as LCP if it is rendered before text2. + if (entry.id !== 'text2') + return; + + assert_equals(entry.entryType, 'largest-contentful-paint', + 'The entry entryType should be largest-contentful-paint.'); + + assert_greater_than_equal(entry.renderTime, beforeRender, + 'The entry renderTime should be greater than or equal to the beforeRender.'); + + assert_greater_than_equal(performance.now(), entry.renderTime, + 'The performance.now() timestamp should be greater than or equal to the entry renderTime.'); + + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'The entry startTime should be equal to renderTime to the precision of 1 millisecond.'); + + assert_equals(entry.duration, 0, 'The entry duration should be 0.'); + + const div = document.getElementById('text2'); + + // The div styling makes it approximate the text size. + assert_greater_than_equal(entry.size, (div.clientHeight - 5) * (div.clientWidth - 5), + 'Reported LCP size should not be significantly smaller than the text2 div.'); + + assert_less_than_equal(entry.size, (div.clientHeight + 1) * (div.clientWidth + 1), + 'Reported LCP size should not be larger than the text2 div.'); + + assert_equals(entry.loadTime, 0, 'The entry loadTime should be 0.'); + + assert_equals(entry.url, '', 'The entry url should be empty.'); + + assert_equals(entry.element, div, 'The entry element should be test2 div.'); + + observer.disconnect(); + + resolve(); + }) + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + }, 'Largest Contentful Paint: largest text is reported.'); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/loadTime-after-appendChild.html b/testing/web-platform/tests/largest-contentful-paint/loadTime-after-appendChild.html new file mode 100644 index 0000000000..52d8f0663b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/loadTime-after-appendChild.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: delayed appended image.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad; + const observer = new PerformanceObserver( + t.step_func_done(entryList => { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/black-rectangle.png'; + // blue.png is 100 by 50. + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + const img = document.createElement('img'); + img.src = '/images/black-rectangle.png'; + img.id = 'image_id'; + t.step_timeout(() => { + beforeLoad = performance.now(); + document.getElementById('image_div').appendChild(img); + }, 200) + }, 'Image loadTime occurs after appendChild is called.'); +</script> +<div id='image_div'></div> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-background.tentative.html b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-background.tentative.html new file mode 100644 index 0000000000..ddd4f9672e --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-background.tentative.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> + <title>LCP mouseover heuristics ignore background-based zoom widgets</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-actions.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="resources/mouseover-utils.js"></script> +</head> +<body> + <img src="/images/lcp-16x16.png" id=image> + <span id=span style="display: inline-block;width: 15px; height: 15px"></span> + <script> + run_mouseover_test(/*background=*/true); + </script> +</body> + diff --git a/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-element.tentative.html b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-element.tentative.html new file mode 100644 index 0000000000..afc4b2a50d --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-element.tentative.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <title>LCP mouseover heuristics ignore element-based zoom widgets</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-actions.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="resources/mouseover-utils.js"></script> +</head> +<body> + <img src="/images/lcp-16x16.png" id=image> + <span id=span style="display: inline-block;width: 15px; height: 15px"></span> + <script> + run_mouseover_test(/*background=*/false); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/multiple-image-same-src.html b/testing/web-platform/tests/largest-contentful-paint/multiple-image-same-src.html new file mode 100644 index 0000000000..192a7a1dec --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/multiple-image-same-src.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint:dynamically appended image with different + dimensions but same src triggers new entry.</title> +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/largest-contentful-paint-helpers.js"></script> + <img src='/images/black-rectangle.png' id='image_id' width="50" /> + <script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad = performance.now(); + let beforeSecondImageLoad = performance.now(); + let firstCallback = true; + // black-rectangle.png is 50 x 25. + const original_rect_size = 50 * 25; + // The bigger black rectangle is 100 x 50, but defined with width="50". + const bigger_rect_size = 100 * 50; + const observer = new PerformanceObserver( + t.step_func(function (entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/black-rectangle.png'; + if (firstCallback) { + // Checks the original black rectangle. + // TODO(https://crbug.com/1411616): we're testing approximated values. + checkImage(entry, url, 'image_id', original_rect_size, + beforeLoad, ["approximateSize"]); + // Creates a new bigger black rectangle. + const img = document.createElement('img'); + img.id = 'new_image_id'; + img.src = url; + img.width = 100; + beforeSecondImageLoad = performance.now(); + firstCallback = false; + document.body.appendChild(img); + } else { + // Checks the new black rectangle. + checkImage(entry, url, 'new_image_id', bigger_rect_size, beforeSecondImageLoad); + t.done(); + } + }) + ); + observer.observe({ type: 'largest-contentful-paint', buffered: true }); + }, 'Largest Contentful Paint:dynamically appended image with different ' + + 'dimensions but same src triggers new entry.'); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/multiple-redirects-TAO.html b/testing/web-platform/tests/largest-contentful-paint/multiple-redirects-TAO.html new file mode 100644 index 0000000000..b9745176bd --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/multiple-redirects-TAO.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates some Timing-Allow-Origin header usage in multiple redirects.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script src="/common/get-host-info.sub.js"></script> +</head> +<img id='image'></img> +<body> +<script> +setup({"hide_test_state": true}); +async_test(t => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let destUrl = get_host_info().HTTP_REMOTE_ORIGIN + + '/element-timing/resources/multiple-redirects.py?'; + destUrl += 'redirect_count=2'; + // The final resource has '*' in TAO header, so will not affect the result. + destUrl += '&final_resource=/element-timing/resources/circle-tao.svg'; + destUrl += '&origin1=' + get_host_info().UNAUTHENTICATED_ORIGIN; + destUrl += '&origin2=' + get_host_info().HTTP_REMOTE_ORIGIN; + const taoCombinations = [ + {tao1: location.origin, tao2: location.origin, passes: false}, + {tao1: location.origin, tao2: get_host_info().HTTP_REMOTE_ORIGIN, passes: false}, + {tao1: location.origin, tao2: 'null', passes: true}, + {tao1: location.origin, tao2: '*', passes: true}, + {tao1: location.origin, tao2: location.origin, passes: false}, + {tao1: 'null', tao2: '*', passes: false}, + {tao1: '*', tao2: 'null', passes: true}, + ]; + const sizes = [28*28, 33*33, 40*40, 50*50, 66*66, 100*100, 200*200]; + function getURL(item) { + return destUrl + '&tao1=' + item.tao1 + '&tao2=' + item.tao2; + } + function setImage(index) { + const image = document.getElementById('image'); + const item = taoCombinations[index]; + image.src = getURL(item); + // Use monotonic sizes to get all the images! + image.width = 200 / (7 - index); + } + let observedCount = 0; + let beforeLoad = performance.now(); + new PerformanceObserver(t.step_func(entries => { + assert_equals(entries.getEntries().length, 1, 'There should be a single entry.'); + const e = entries.getEntries()[0]; + const item = taoCombinations[observedCount]; + const url = getURL(item); + const options = item.passes ? [] : ['renderTimeIs0']; + checkImage(e, url, 'image', sizes[observedCount], beforeLoad, options); + observedCount++; + if (observedCount === taoCombinations.length) { + t.done(); + } else { + beforeLoad = performance.now(); + setImage(observedCount); + } + })).observe({entryTypes: ['largest-contentful-paint']}); + setImage(0); +}, 'Cross-origin images with passing/failing TAO should/shouldn\'t have its renderTime set.'); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-after-fcp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-after-fcp.tentative.html new file mode 100644 index 0000000000..06b065be3a --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-after-fcp.tentative.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Non-Tao Image Load and Render After FCP. +</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script src="/common/get-host-info.sub.js"></script> + +<body> + <script> + promise_test(async t => { + // Add text so that FCP is set and image rendering time would be larger than FCP. + add_text('Add to set FCP'); + + // wait for 1 animation frame so that FCP is set. + await raf(); + + const non_tao_image_url = get_host_info().OTHER_ORIGIN + '/images/blue.png'; + + await loadImage(non_tao_image_url); + + lcp = await getLCPStartTime('blue.png'); + + fcp = getFCPStartTime(); + + checkLCPEntryForNonTaoImages({'lcp':lcp, 'fcp':fcp}); + }, 'Non-Tao Image Load and Render After FCP.') + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-after.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-after.tentative.html new file mode 100644 index 0000000000..57f29c3535 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-after.tentative.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Non-Tao Image Load Before FCP and Render After FCP. +</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script src="/common/get-host-info.sub.js"></script> + +<body> + <script> + promise_test(async t => { + const non_tao_image_url = get_host_info().OTHER_ORIGIN + '/images/blue.png'; + + img = await loadImage(non_tao_image_url, true); + + // Add text so that FCP is set and image rendering time would be larger than FCP. + add_text('Add to set FCP'); + + // wait for 1 animation frame so that FCP is set. + await raf(); + + // Pass empty string to select the LCP entry corresponding to the text as LCP.url is + // empty for text elements. + lcp = await getLCPStartTime(''); + + img.style.opacity = 1; + + lcp = await getLCPStartTime('blue.png'); + + fcp = getFCPStartTime(); + + checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp }); + }, 'Non-Tao Image Load Before FCP and Render After FCP.') + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-at-fcp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-at-fcp.tentative.html new file mode 100644 index 0000000000..b209b50c8b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-at-fcp.tentative.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Non-Tao Image Load Before LCP and Render at the Same Time of FCP. +</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script src="/common/get-host-info.sub.js"></script> + +<body> + <script> + promise_test(async t => { + const non_tao_image_url = get_host_info().OTHER_ORIGIN + '/images/blue.png'; + + await loadImage(non_tao_image_url); + + lcp = await getLCPStartTime('blue.png'); + + fcp = getFCPStartTime(); + + checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp }); + }, 'Non-Tao Image Load Before LCP and Render at the Same Time of FCP.') + </script> + <!-- <img src='{{location[scheme]}}://{{hosts[alt][www]}}:{{ports[http][0]}}/images/blue.png'> --> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-subsequent-lcp-candidate.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-subsequent-lcp-candidate.tentative.html new file mode 100644 index 0000000000..4d799c1b05 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-subsequent-lcp-candidate.tentative.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Non-Tao Image Subsequent LCP candidates. +</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script src="/common/get-host-info.sub.js"></script> + +<body> + <script> + promise_test(async t => { + let small_image_path = '/images/lcp-100x50.png'; + let big_image_path = '/images/lcp-256x256.png'; + let non_tao_scheme = get_host_info().OTHER_ORIGIN; + + // Load non-tao image with 0 opacity so it won't trigger an LCP entry. + big_image = await loadImage(non_tao_scheme + big_image_path, true); + + // Load a smaller non-tao image with 0 opacity. + small_image = await loadImage(non_tao_scheme + small_image_path, true); + + // Add text so that FCP is set. + add_text('text'); + + // wait for 1 animation frame so that FCP is set. + await raf(); + + // The url of text LCP element is empty. + lcp = await getLCPStartTime(''); + + // Rendered loaded image after FCP. + small_image.style.opacity = 1; + lcp = await getLCPStartTime(small_image_path); + + fcp = getFCPStartTime(); + + checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp }); + + // This is to verify subsequent LCP candidates also have the start time set correctly. + big_image.style.opacity = 1; + + lcp = await getLCPStartTime(big_image_path); + + checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp }); + + }, 'Non-Tao Image Subsequent LCP candidates.') + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-after-untrusted-scroll.html b/testing/web-platform/tests/largest-contentful-paint/observe-after-untrusted-scroll.html new file mode 100644 index 0000000000..c84f922e5e --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-after-untrusted-scroll.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe image.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + + const url = window.location.origin + '/images/blue.png'; + // blue.png is 133 by 106. + const size = 133 * 106; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Same-origin image after a JS initiated scroll event is observable.'); + document.body.dispatchEvent(new Event('scroll')); + const image = new Image(); + image.id = 'image_id'; + image.src = '/images/blue.png'; + document.body.appendChild(image); +</script> + +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-image.html new file mode 100644 index 0000000000..f674e8c37d --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-image.html @@ -0,0 +1,29 @@ +<!doctype html> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe element with css generated image</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<style> + #target::before { + content: url('/images/black-rectangle.png'); + } +</style> +<body> + <p id="target"></p> + <script> + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad = performance.now(); + let observedFirstLetter = false; + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + const entry = entryList.getEntries()[entryList.getEntries().length -1]; + const url = window.location.origin + '/images/black-rectangle.png'; + checkImage(entry, url, 'target', 0, beforeLoad, ['sizeLowerBound']); + })); + observer.observe({entryTypes: ['largest-contentful-paint']}); + }, 'Largest Contentful Paint: CSS generated image is observable.'); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-text.html b/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-text.html new file mode 100644 index 0000000000..21ae68585b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-text.html @@ -0,0 +1,88 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + #css-generated-text::before { + content: 'This is some text generated via css'; + font-size: 12px; + } + + #css-generated-text-attr::before { + content: attr(data-text); + font-size: 12px; + } + + #css-generated-text-inline-elem::before { + content: 'This is some more text generated via css that should be displayed via a span tag'; + font-size: 12px; + } +</style> +<body> + <script> + setup({"hide_test_state": true}); + const checkText = (entry, expectedSize, expectedID, beforeRender) => { + assert_equals(entry.entryType, 'largest-contentful-paint', + 'Entry should be of type largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeRender, + 'Render time should be greater than time just before rendering'); + assert_greater_than_equal(performance.now(), entry.renderTime, + 'renderTime should be less than current time'); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0, 'duration should be 0'); + assert_greater_than_equal(entry.size, expectedSize, + 'Size should match expected size'); + assert_equals(entry.loadTime, 0, 'loadTime should be zero'); + assert_equals(entry.id, expectedID, 'ID should match expected ID'); + assert_equals(entry.url, '', 'URL should be empty'); + assert_equals(entry.element, document.getElementById(expectedID), + 'Entry element is expected element'); + } + + const runTest = (element, testName) => { + const elementId = element.id; + // The element should be atleast 12px in width + // and 100px across based on font size and text length. + const elemSizeLowerBound = 1200; + promise_test(t => { + return new Promise((resolve, reject) => { + assert_implements(window.LargestContentfulPaint, + "LargestContentfulPaint is not implemented"); + const observer = new PerformanceObserver(resolve); + observer.observe({ type: 'largest-contentful-paint' }); + beforeRender = performance.now(); + document.body.appendChild(element); + + step_timeout(() => { + reject(new Error('timeout, LCP candidate not detected')); + }, 1000) + }).then(entryList => { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + checkText(entry, elemSizeLowerBound, elementId, beforeRender); + }); + }, testName); + } + + const cssGeneratedTextElem = document.createElement('p'); + cssGeneratedTextElem.id = 'css-generated-text'; + runTest(cssGeneratedTextElem, + "CSS generated text is observable as a LargestContentfulPaint candidate"); + + const cssGeneratedTextAttrElem = document.createElement('p'); + cssGeneratedTextAttrElem.id = 'css-generated-text-attr'; + cssGeneratedTextAttrElem.setAttribute('data-text', + 'This is some text generated using content:attr() via css'); + runTest(cssGeneratedTextAttrElem, + "Text generated with CSS using content:attr() is observable as a LargestContentfulPaint candidate"); + + const cssGeneratedTextAttrInlineElemBlockWrapper = document.createElement('div'); + cssGeneratedTextAttrInlineElemBlockWrapper.id = 'css-generated-text-inline-elem-block-wrapper'; + const cssGeneratedTextInlineElem = document.createElement('span'); + cssGeneratedTextInlineElem.id = 'css-generated-text-inline-elem'; + cssGeneratedTextAttrInlineElemBlockWrapper.appendChild(cssGeneratedTextInlineElem); + runTest(cssGeneratedTextAttrInlineElemBlockWrapper, + "CSS generated text on a inline element is observable as a LargestContentfulPaint candidate"); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-image.html new file mode 100644 index 0000000000..707840671b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-image.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe image.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/blue.png'; + // blue.png is 133 by 106. + const size = 133 * 106; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Same-origin image is observable.'); +</script> + +<img src='/images/blue.png' id='image_id'/> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-mathml.html b/testing/web-platform/tests/largest-contentful-paint/observe-mathml.html new file mode 100644 index 0000000000..f527f188c7 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-mathml.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe element rendered by MathML</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<style> +mathml { + font-size: 12px; +} +</style> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad; + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeRender); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0); + // Some lower bound: height of at least 12 px. + // Width of at least 100 px. + assert_greater_than(entry.size, 1200); + assert_equals(entry.loadTime, 0); + assert_equals(entry.id, 'mathml'); + assert_equals(entry.url, ''); + assert_equals(entry.element, document.getElementById('mathml')); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + beforeRender = performance.now(); + }, 'Element rendered by MathML is observable'); +</script> +<math display="block"> + <mrow> + <msup> + <mi id="mathml">This is important text! :)</mi> + </msup> + </mrow> +</math> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-random-namespace.html b/testing/web-platform/tests/largest-contentful-paint/observe-random-namespace.html new file mode 100644 index 0000000000..6fc0bbbb23 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-random-namespace.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe element created in a random namespace</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<style> + div { + display: block; + } +</style> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeRender; + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeRender); + assert_greater_than_equal(performance.now(), entry.renderTime); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0); + // Some lower bound: height of at least 12 px. + // Width of at least 100 px. + assert_greater_than(entry.size, 1200); + assert_equals(entry.loadTime, 0); + assert_equals(entry.url, ''); + assert_equals(entry.id, 'my_text'); + assert_equals(entry.element, document.getElementById("my_text")); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + + const div = document.createElementNS("random", "div"); + div.innerHTML = "This is important text! :)"; + div.id = "my_text"; + beforeRender = performance.now(); + document.body.appendChild(div); + }, 'Element created with different namespace is observable'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-background-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-background-image.html new file mode 100644 index 0000000000..bc0b399a18 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-background-image.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe image.</title> +<style> + #target { + background-image: url('/images/green.svg'); + width: 100px; + height: 50px; + } +</style> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + let url = window.location.origin + '/images/green.svg'; + // green.svg is 100 by 50 + const size = 100 * 50; + checkImage(entry, url, 'target', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + t.step_timeout(() => { + assert_unreached("The image should have triggered an LCP entry."); + t.done(); + }, 1000); + }, 'Same-origin SVG background image is observable.'); +</script> + +<div id="target" width="100" height="50"></div> +</body> + diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-background-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-background-image.html new file mode 100644 index 0000000000..53fd1b7273 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-background-image.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe image.</title> +<style> + #target { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><rect fill="lime" width="100" height="50"/></svg>'); + width: 100px; + height: 50px; + } +</style> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + let url = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"'; + url += ' width="100" height="50"><rect fill="lime" width="100"'; + url += ' height="50"/></svg>'; + // green.svg is 100 by 50 + const size = 100 * 50; + checkImage(entry, url, 'target', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + t.step_timeout(() => { + assert_unreached("The image should have triggered an LCP entry."); + t.done(); + }, 1000); + }, 'Data-URI background SVG image is observable.'); +</script> + +<div id="target" width="100" height="50"></div> +</body> + diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-image.html new file mode 100644 index 0000000000..00ea314e14 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-image.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe image.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + let url = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"'; + url += ' width="100" height="50"><rect fill="lime" width="100"'; + url += ' height="50"/></svg>'; + // green.svg is 100 by 50 + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Same-origin image is observable.'); +</script> + +<img src='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><rect fill="lime" width="100" height="50"/></svg>' id='image_id'/> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-image.html new file mode 100644 index 0000000000..3a6e0f6f23 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-image.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe SVG image.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/green.svg'; + // green.svg is 100 by 50 + const size = 100 * 50; + checkImage(entry, url, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Same-origin image is observable.'); +</script> + +<img src='/images/green.svg' id='image_id'/> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-text.html b/testing/web-platform/tests/largest-contentful-paint/observe-text.html new file mode 100644 index 0000000000..5d0244b7e3 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/observe-text.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe text.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> +p { + font-size: 12px; +} +</style> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeRender; + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeRender); + assert_greater_than_equal(performance.now(), entry.renderTime); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0); + // Some lower bound: height of at least 12 px. + // Width of at least 100 px. + // TODO: find a good way to bound text width. + assert_greater_than_equal(entry.size, 1200); + assert_equals(entry.loadTime, 0); + assert_equals(entry.id, 'my_text'); + assert_equals(entry.url, ''); + assert_equals(entry.element, document.getElementById('my_text')); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + beforeRender = performance.now(); + }, 'Text element is observable as a LargestContentfulPaint candidate.'); +</script> + +<p id='my_text'>This is important text! :)</p> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html b/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html new file mode 100644 index 0000000000..06691cef22 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: src change triggers new entry.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<img src='/images/lcp-1x1.png' id='image_id' width="133" height="106"/> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad = performance.now(); + document.getElementById('image_id').src = '/images/lcp-133x106.png'; + const url = window.location.origin + '/images/lcp-133x106.png'; + const observer = new PerformanceObserver( + t.step_func(function(entryList) { + let entries = entryList.getEntries().filter(e => e.url === url); + if (entries.length === 0) + return; + assert_equals(entries.length, 1); + const entry = entries[0]; + const size = 133 * 106; + checkImage(entry, url, 'image_id', size, beforeLoad); + t.done(); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Largest Contentful Paint: changing src causes a new entry to be dispatched.'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/progressively-loaded-image.html b/testing/web-platform/tests/largest-contentful-paint/progressively-loaded-image.html new file mode 100644 index 0000000000..25475e3ab9 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/progressively-loaded-image.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: Progressively Loaded Image</title> +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/largest-contentful-paint-helpers.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script> + promise_test(async function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const imgElement = document.getElementById('image_id'); + const imageSrc = imgElement.currentSrc; + const imageWidth = imgElement.naturalWidth; + const imageHeight = imgElement.naturalHeight; + const size = imageWidth * imageHeight; + const lcpLoadTime = entry.loadTime; + const lcpRenderTime = entry.renderTime; + const resLoadTime = calculateResourceLoadTime(imageSrc); + assert_greater_than_equal(lcpLoadTime,resLoadTime,'Load before LCP event fired'); + assert_greater_than_equal(lcpRenderTime,resLoadTime,'Load before LCP event rendered'); + checkImage(entry, imageSrc, 'image_id', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}) + }, 'Ensure Progressive JPEG LCP load and render events occur after resource loading'); + + function calculateResourceLoadTime(imageSrc) { + const entries = performance.getEntriesByName(imageSrc); + const resourceEntry = entries[0]; + assert_equals(entries.length, 1, 'No Resource timing entry found'); + return resourceEntry.responseEnd; + } + </script> + <img src='/images/computer.jpg?pipe=trickle(982:d1)' id='image_id'/> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/largest-contentful-paint/redirects-tao-star.html b/testing/web-platform/tests/largest-contentful-paint/redirects-tao-star.html new file mode 100644 index 0000000000..5607ed792e --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/redirects-tao-star.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates LargestContentfulPaint information for cross-origin redirect chain images.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script src=/common/get-host-info.sub.js></script> +</head> +<body> +<script> +setup({"hide_test_state": true}); +async_test(t => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let destUrl = get_host_info().HTTP_REMOTE_ORIGIN + + '/resource-timing/resources/multi_redirect.py?'; + destUrl += 'page_origin=' + get_host_info().HTTP_ORIGIN; + destUrl += '&cross_origin=' + get_host_info().HTTP_REMOTE_ORIGIN; + destUrl += '&final_resource=/element-timing/resources/circle-tao.svg'; + destUrl += '&tao_steps='; + const sizes = [50*50, 66*66, 100*100, 200*200]; + + const image = document.createElement('img'); + image.src = destUrl + '0'; + image.setAttribute('id', 'id'); + image.width = 200 / 4; + document.body.appendChild(image); + + let numObserved = 0; + let beforeLoad = performance.now(); + new PerformanceObserver(t.step_func(entries => { + assert_equals(entries.getEntries().length, 1); + const entry = entries.getEntries()[0]; + const options = numObserved === 3 ? [] : ['renderTimeIs0']; + checkImage(entry, destUrl + numObserved, 'id', sizes[numObserved], beforeLoad, options); + numObserved++; + if (numObserved === 4) + t.done(); + else { + // Change the image to trigger a new LCP entry. + const img = document.getElementById('id'); + image.src = destUrl + numObserved; + // Use monotonically increasing image sizes to trigger LCP every time. + image.width = 200 / (4 - numObserved); + beforeLoad = performance.now(); + } + })).observe({type: 'largest-contentful-paint'}); +}, 'Cross-origin image without TAO should not have its renderTime set, with full TAO it should.'); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/largest-contentful-paint/repeated-image.html b/testing/web-platform/tests/largest-contentful-paint/repeated-image.html new file mode 100644 index 0000000000..c69cc5b615 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/repeated-image.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: repeated image.</title> +<style> + #image_id { + width: 10px; + height: 10px; + } +</style> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> + setup({"hide_test_state": true}); + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeLoad = performance.now(); + let firstCallback = true; + const url = window.location.origin + '/images/black-rectangle.png'; + const observer = new PerformanceObserver( + t.step_func(entryList => { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + + // First image is shrunk to be 10 x 10. The second image is added at its natural size: 100 x 50. + const size = firstCallback ? 10 * 10 : 100 * 50; + const id = firstCallback ? 'image_id' : 'second_id'; + checkImage(entry, url, id, size, beforeLoad); + if (firstCallback) { + const img = document.createElement('img'); + img.src = '/images/black-rectangle.png'; + img.id = 'second_id'; + beforeLoad = performance.now(); + document.getElementById('image_div').appendChild(img); + firstCallback = false; + return; + } else { + t.done(); + } + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Repeated image produces different timestamps.'); +</script> +<img src='/images/black-rectangle.png' id='image_id'/> +<div id='image_div'></div> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/resized-image-not-reconsidered.html b/testing/web-platform/tests/largest-contentful-paint/resized-image-not-reconsidered.html new file mode 100644 index 0000000000..6e195b89f9 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resized-image-not-reconsidered.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Resized Image Is Not Reconsidered as LCP.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <img src='/images/lcp-256x256.png' id='image_id' height="100" width="50" /> + <script> + let image_id = 'image_id'; + + // Create a promise that resolves when an LCP is observed. + const lcp_observation_promise = image_src => { + return new Promise(resolve => { + new PerformanceObserver((entryList) => { + let lcpEntries = entryList.getEntries().filter(e => e.id == image_id); + + if (lcpEntries) { + resolve(lcpEntries); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + } + + promise_test(async t => { + const lcpEntriesInitial = await lcp_observation_promise(); + assert_equals(lcpEntriesInitial.length, 1); + + // Resize image. + document.getElementById('image_id').height = 150; + + // Wait for a repaint. + const lcpEntriesAfterImageResizing = + await new Promise(resolve => { + t.step_timeout(window.requestAnimationFrame(async () => { + resolve(await lcp_observation_promise()); + }), 100); + }); + + // No additional LCP entry is emitted after the image is resized to be larger. + assert_equals(lcpEntriesAfterImageResizing.length, 1); + assert_true(lcpEntriesInitial[0] === lcpEntriesAfterImageResizing[0]); + }, "Resized image should not be reconsidered as LCP"); + </script> +</body>
\ No newline at end of file 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..52f7036466 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js @@ -0,0 +1,173 @@ +const image_delay = 2000; +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..60a29ed04e --- /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/lcp-${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/lcp-${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; + } + """ diff --git a/testing/web-platform/tests/largest-contentful-paint/same-origin-redirects.html b/testing/web-platform/tests/largest-contentful-paint/same-origin-redirects.html new file mode 100644 index 0000000000..b5cf9da2d1 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/same-origin-redirects.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates LargestContentfulPaint for same-origin redirect chain.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +</head> +<body> +<script> +async_test(t => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + // First redirect + let destUrl = '/common/redirect.py?location=' + // Second redirect + destUrl += '/common/redirect.py?location=' + // Image without TAO headers. + destUrl += '/element-timing/resources/square20.png'; + let beforeLoad; + new PerformanceObserver(t.step_func_done(entries => { + assert_equals(entries.getEntries().length, 1, 'There should be one entry'); + const entry = entries.getEntries()[0]; + checkImage(entry, location.origin + destUrl, 'id', 20*20, beforeLoad); + })).observe({entryTypes: ['largest-contentful-paint']}); + const image = document.createElement('img'); + image.src = destUrl; + image.setAttribute('id', 'id') + document.body.appendChild(image); + beforeLoad = performance.now(); +}, 'Same-origin image redirect without TAO should have its renderTime set.'); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/largest-contentful-paint/supported-lcp-type.html b/testing/web-platform/tests/largest-contentful-paint/supported-lcp-type.html new file mode 100644 index 0000000000..25d4eaa036 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/supported-lcp-type.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<head> +<title>PerformanceObserver.supportedEntryTypes contains "largest-contentful-paint"</title> +</head> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(() => { + if (typeof PerformanceObserver.supportedEntryTypes === "undefined") + assert_unreached("supportedEntryTypes is not supported."); + assert_greater_than(PerformanceObserver.supportedEntryTypes.indexOf("largest-contentful-paint"), -1, + "There should be an entry 'largest-contentful-paint' in PerformanceObserver.supportedEntryTypes"); +}, "supportedEntryTypes contains 'largest-contentful-paint'."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/text-with-display-style.html b/testing/web-platform/tests/largest-contentful-paint/text-with-display-style.html new file mode 100644 index 0000000000..69edc168c5 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/text-with-display-style.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe text with display style.</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> +#title { + display: flex; +} +</style> +<h1 id='title'>I am a title!</h1> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let beforeRender; + /* In this test, we first observe a header with style 'display: flex'. + * Once observed, we remove it and add a header with style 'display: grid'. + * And once that is observed, we remove it and add a header with style 'display: block'. + * At each step, we check the values of the entries received. + */ + let observedFlex = false; + let observedGrid = false; + const observer = new PerformanceObserver( + t.step_func(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_greater_than_equal(entry.renderTime, beforeRender); + assert_greater_than_equal(performance.now(), entry.renderTime); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + assert_equals(entry.duration, 0); + // TODO: find a good way to bound text size. + assert_greater_than_equal(entry.size, 500); + assert_equals(entry.url, ''); + assert_equals(entry.loadTime, 0); + if (!observedFlex) { + observedFlex = true; + assert_equals(entry.id, 'title'); + const title = document.getElementById('title'); + assert_equals(entry.element, title); + // Remove 'display: flex' and add 'display: grid' text. + title.parentNode.removeChild(title); + const title2 = document.createElement('h1'); + title2.id = 'title2'; + title2.style = 'display: grid'; + title2.innerHTML = 'I am a second title!'; + document.body.appendChild(title2); + beforeRender = performance.now(); + } else if (!observedGrid) { + observedGrid = true; + assert_equals(entry.id, 'title2'); + const title2 = document.getElementById('title2'); + assert_equals(entry.element, title2); + // Remove 'display: grid' and add 'display: block' text. + title2.parentNode.removeChild(title2); + const title3 = document.createElement('h1'); + title3.id = 'title3'; + title3.style = 'display: block'; + title3.innerHTML = 'I am the third and last title!'; + document.body.appendChild(title3); + beforeRender = performance.now(); + } else { + assert_equals(entry.id, 'title3'); + const title3 = document.getElementById('title3'); + assert_equals(entry.element, title3); + t.done(); + } + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + beforeRender = performance.now(); + }, 'Text with display style is observable.'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/toJSON.html b/testing/web-platform/tests/largest-contentful-paint/toJSON.html new file mode 100644 index 0000000000..5ea84eeb2b --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/toJSON.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: toJSON</title> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<p>Text!</p> +<script> + async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + const entry = entryList.getEntries()[0]; + assert_equals(typeof(entry.toJSON), 'function'); + const json = entry.toJSON(); + assert_equals(typeof(json), 'object'); + const keys = [ + // PerformanceEntry + 'name', + 'entryType', + 'startTime', + 'duration', + // LargestContentfulPaint + 'renderTime', + 'loadTime', + 'size', + 'id', + 'url', + ]; + for (const key of keys) { + assert_equals(json[key], entry[key], + 'LargestContentfulPaint ${key} entry does not match its toJSON value'); + } + assert_equals(json['element'], undefined, 'toJSON should not include element'); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + }, 'Test toJSON() in LargestContentfulPaint.'); +</script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/update-on-style-change.tentative.html b/testing/web-platform/tests/largest-contentful-paint/update-on-style-change.tentative.html new file mode 100644 index 0000000000..dd34e61b0f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/update-on-style-change.tentative.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>LargestContentfulPaint entries should generate for updates to previous LargestContentfulPaint nodes.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <script> + promise_test(() => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + let countLcp = 0; + let firstLcp = null; + const timeoutPromise = new Promise(resolve => step_timeout(() => { + resolve(new Error('Did not observe two LCP entries')) + }, 3 * 1000)); + const testPromise = new Promise(resolve => { + new PerformanceObserver(list => { + const entries = list.getEntries(); + for (const entry of entries) { + ++countLcp; + assert_equals(entry.entryType, 'largest-contentful-paint'); + assert_equals(entry.id, 'text'); + if (countLcp == 1) { + firstLcp = entry; + } else if (countLcp == 2) { + assert_more_than(entry.startTime, firstLcp.startTime); + assert_more_than(entry.size, firstLcp.size); + resolve(); + } + } + }).observe({ entryTypes: ['largest-contentful-paint'] }); + }); + return Promise.race([timeoutPromise, testPromise]); + }) + </script> + <div id="text">text</div> + <link rel="stylesheet" href="/resources/slow-style-change.py"> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html b/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html new file mode 100644 index 0000000000..01d9aa6925 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html @@ -0,0 +1,51 @@ +<!doctype html> +<html> +<title>This test verifies a video element of data uri src triggers an LCP entry + to be emitted</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <script> + const get_lcp_entry = () => { + return new Promise(resolve => { + new PerformanceObserver((list, observer) => { + if (list.getEntries()) { + observer.disconnect(); + resolve(list.getEntries()[0]); + } + }).observe({ type: "largest-contentful-paint", buffered: true }); + }); + }; + + promise_test(async t => { + lcpEntry = await get_lcp_entry(); + assert_true(lcpEntry.url.startsWith('data:video/webm;base64,GkXfo59C')); + }, "Video of data URI src should trigger an LCP entry to be emitted.") + </script> + <!-- + This is the base64 encoding of the images/pattern.webm video file. + --> + <video autoplay muted src="data:video/webm;base64,GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAQxEU2bdLpNu4tT + q4QVSalmU6yBoU27i1OrhBZUrmtTrIHYTbuMU6uEElTDZ1OsggEpTbuMU6uEHFO7a1OsggQb7AEA + AAAAAABZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmsirXsYMPQkBNgI1MYXZm + NTkuMjcuMTAwV0GNTGF2ZjU5LjI3LjEwMESJiECpQgAAAAAAFlSua8yuAQAAAAAAAEPXgQFzxYil + qEthhRpETZyBACK1nIN1bmSIgQCGhVZfVlA5g4EBI+ODhAH8oFXglLCBFLqBFJqBAlWwiFW3gQJV + uIECElTDZ0CBc3OgY8CAZ8iaRaOHRU5DT0RFUkSHjUxhdmY1OS4yNy4xMDBzc9tjwItjxYilqEth + hRpETWfIpUWjh0VOQ09ERVJEh5hMYXZjNTkuMzcuMTAwIGxpYnZweC12cDlnyKJFo4hEVVJBVElP + TkSHlDAwOjAwOjAzLjIzMzAwMDAwMAAAH0O2dUJl54EAo0G3gQAAgKJJg0IAACYAJsAHBIODAEAA + BnAAAHhP///9Q/65f////wmeP/////rwFR5Is7//mm/jI7F//4raSRl5AVkn///8c/8phq//5sl4 + 3m13ew5////YzH/quJ1///+7PmxlWQ33kfuEh+P//7od24Cqpi5QNC9i////+ElnriauN////ElQ + YZcxov4Yx///9zJe/hlKf///2wteAkNJPo3jxlcx//aCdOF2s2+6yc3qmgtrvSadpUHyhzKvZLHQ + HPfw/yBM392pGbYDf////Jt2l5ctakp//pFL/jdkaumyv2/hPNwTDXLuzq3Hshvf+xED6liPj/Yl + Op29O0x//+S1annta3jdCP//jCP//93mv/MS/st8352/8MPJZhn/uSe0wfT3LGr8+KY/dBfBjEv/ + 2aBMTfwh//RLcswNR+aoOCq2VZWOWaQEeS9kChnBNTwP4XHAhU1XXFbX//ZpyKr76trSCwv3k9AS + sQfD3WUHFH00UV+Dz33eUpd1rRhdo1WocotOnDqd3RNvs1Tx6BX9JBJ7CS/6OMSd1WhPK+CvqTn4 + Z1Op01X0KcAq7I08pT5UGSpQAKOTgQGQAKYAQJKcAEoAAAMgAABDQKOTgQMgAKYAQJKcAEoAAAMg + AABDQKOTgQSwAKYAQJKcAEoAAAMgAABDQKOTgQZAAKYAQJKcAEoAAAMgAABDQKOTgQfQAKYAQJKc + AEoAAAMgAABDQKOTgQlgAKYAQJKcAEoAAAMgAABDQKOTgQrwAKYAQJKcAEoAAAMgAABDQKOTgQyA + AKYAQJKcAEoAAAMgAABDQBxTu2uRu4+zgQC3iveBAfGCAbDwgQM=" width="600"></video> +</body> + +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/video-poster.html b/testing/web-platform/tests/largest-contentful-paint/video-poster.html new file mode 100644 index 0000000000..aaf2ce8a74 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/video-poster.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Largest Contentful Paint: observe video poster image</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/largest-contentful-paint-helpers.js"></script> +<script> +setup({"hide_test_state": true}); +async_test(function (t) { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const beforeLoad = performance.now(); + const observer = new PerformanceObserver( + t.step_func_done(function(entryList) { + assert_equals(entryList.getEntries().length, 1); + const entry = entryList.getEntries()[0]; + const url = window.location.origin + '/images/blue.png'; + // blue.png is 133 by 106. + const size = 133 * 106; + checkImage(entry, url, 'the_poster', size, beforeLoad); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); +}, "Able to observe a video's poster image."); +</script> +<video id='the_poster' src='/media/test.mp4' poster='/images/blue.png'></video> diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-block.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-block.html new file mode 100644 index 0000000000..572442f2a2 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-block.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<!-- + Web-font styled text that gets resized during block period should not make a + LCP emission. +--> +<style> + @font-face { + font-family: 'ADTestFaceBlock'; + src: url('/fonts/AD.woff'); + font-display: block; + } + + .test { + font-family: 'ADTestFaceBlock'; + } + +</style> +<div class="test">LCP: Web Font Styled Text Resize</div> +<script> + function LCPEntryList(t) { + return new Promise(resolve => { + let = lcpEntries = []; + new PerformanceObserver((entryList, observer) => { + lcpEntries = lcpEntries.concat(entryList.getEntries()); + if (lcpEntries) { + // Adding timeout to wait a bit more so that if there are more than + // expected LCP entries emitted, they can be observed. + t.step_timeout(() => { + resolve(lcpEntries); + observer.disconnect(); + }, 200); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + } + + promise_test(async t => { + await document.fonts.ready; + + // Verify web font is downloaded. + assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported"); + let url = '/fonts/AD.woff'; + var absoluteURL = new URL(url, location.href).href; + assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font\ + should be downloaded'); + + // Verify web font is available. + assert_true(document.fonts.check('16px ADTestFaceBlock'), 'Font should be the web font added'); + + // Verify there is only one LCP entry. + let entryList = await LCPEntryList(t); + assert_equals(entryList.length, 1, 'Web font styled text resize that occurs during block period should not make a new LCP emission.'); + + }, "LCP should be not updated if the web font styled text resize occurs during the block period."); +</script> diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-after-interaction.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-after-interaction.html new file mode 100644 index 0000000000..3ba02bab5a --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-after-interaction.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src=/resources/testdriver.js></script> +<script src=/resources/testdriver-actions.js></script> +<script src=/resources/testdriver-vendor.js></script> +<style></style> +<!-- + Web-font styled text that gets resized after an interaction stops LCP + observation should not make a new LCP emission. +--> +<textarea id='input'></textarea> +<div class="test">LCP: Web Font Styled Text Resize</div> +<script> + function addCSSRules() { + styleSheet = document.styleSheets[0]; + fontRuleSet = "@font-face {\ + font-family: 'ADTestFaceInteraction';\ + src: url('/fonts/AD.woff');\ + font-display: swap;\ + }"; + fontAtRule = ".test {\ + font-family: 'ADTestFaceInteraction';\ + } "; + styleSheet.insertRule(fontRuleSet); + styleSheet.insertRule(fontAtRule); + } + + function LCPEntryList(t) { + return new Promise(resolve => { + let = lcpEntries = []; + new PerformanceObserver((entryList, observer) => { + lcpEntries = lcpEntries.concat(entryList.getEntries()); + if (lcpEntries) { + // Adding timeout to wait a bit more so that if there are more than + // expected LCP entries emitted, they can be observed. + t.step_timeout(() => { + resolve(lcpEntries); + observer.disconnect(); + }, 200); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + } + + promise_test(async t => { + await document.fonts.ready; + let system_font_size = document.getElementsByClassName('test')[0].offsetHeight; + + // Verify an LCP entry is emitted. + let entryList = await LCPEntryList(t); + assert_equals(entryList.length, 1, 'Text with system font should make a LCP emission.'); + + let lcpEntryBeforeInteraction = entryList[0]; + + // Add event listener so that CSS rule would be added after there's an + // input. + let inputElement = document.getElementById('input'); + inputElement.addEventListener('keydown', addCSSRules); + + // Send key as input. + await test_driver.send_keys(inputElement, 'k'); + + // Wait for web font to load. + await document.fonts.ready; + + // Verify web font is downloaded. + assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported"); + let url = '/fonts/AD.woff'; + var absoluteURL = new URL(url, location.href).href; + assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font should be downloaded.'); + + // Verify web font is available. + assert_true(document.fonts.check('16px ADTestFaceInteraction'), 'Font should be the web font added'); + + // Verify web font is applied. + let web_font_size = document.getElementsByClassName('test')[0].offsetHeight; + assert_not_equals(web_font_size, system_font_size, 'Web font swap should happen'); + + // Assert there is only 1 LCP entry, which verifies the added web font does + // not make a new LCP entry after an input. + entryList = await LCPEntryList(t); + assert_equals(entryList.length, 1, 'Text with system font should not make a LCP emission.'); + + // Verify the LCP entry is the same one emitted before interaction by + // asserting the size is the same. + assert_equals(lcpEntryBeforeInteraction.size, entryList[0].size, 'There should be only 1 LCP entry emitted.'); + + }, "LCP should be not updated if the web font styled text resize occurs after an interaction happens"); +</script> diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-smaller.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-smaller.html new file mode 100644 index 0000000000..253038eb8d --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-smaller.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + @font-face { + font-family: 'TestFaceSmaller'; + src: url('/fonts/CSSTest/csstest-basic-regular.ttf?pipe=trickle(d0.5)'); + size-adjust: 30%; + font-display: swap; + } + + .test { + font-family: 'TestFaceSmaller'; + } + +</style> +<!-- + Web-font styled text that gets resized during swap period should not make a + LCP emission if the new size is smaller than the current LCP element size. +--> +<div class="test">LCP: Web Font Styled Text Resize</div> +<script> + function LCPEntryList(t) { + return new Promise(resolve => { + let = lcpEntries = []; + new PerformanceObserver((entryList, observer) => { + lcpEntries = lcpEntries.concat(entryList.getEntries()); + if (lcpEntries) { + // Adding timeout to wait a bit more so that if there are more than + // expected LCP entries emitted, they can be observed. + t.step_timeout(() => { + resolve(lcpEntries); + observer.disconnect(); + }, 200); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + } + + promise_test(async t => { + await document.fonts.ready; + + // Verify web font is loaded. + assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported"); + let url = '/fonts/CSSTest/csstest-basic-regular.ttf?pipe=trickle(d0.5)'; + var absoluteURL = new URL(url, location.href).href; + assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font should be downloaded'); + + // Verify web font is available. + assert_true(document.fonts.check('10px TestFaceSmaller'), 'Font should be the web font added'); + + // Verify there is only one LCP entry. + let entryList = await LCPEntryList(t); + assert_equals(entryList.length, 1, 'Web font styled text resize that occurs during swap period but is smaller should not make a new LCP emission.') + + }, "LCP should be not updated if the web font styled text resizes to be smaller during the swap period"); +</script> diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-subnode.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-subnode.html new file mode 100644 index 0000000000..eca5f3590f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-subnode.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + @font-face { + font-family: 'ADTestFaceSwapSubnode'; + src: url('/fonts/AD.woff?pipe=trickle(d0.5)'); + font-display: swap; + } + + .test { + font-family: 'ADTestFaceSwapSubnode'; + } + +</style> +<!-- + Web-font styled subnode text that gets resized during swap period should make + a LCP emission if the new size is larger than the existing LCP element size. +--> +<div class="test"> + <span>LCP: Web Font Styled Text Resize</span> +</div> +<script> + function LCPEntryList() { + return new Promise(resolve => { + let = lcpEntries = []; + new PerformanceObserver((entryList, observer) => { + lcpEntries = lcpEntries.concat(entryList.getEntries()); + if (lcpEntries.length == 2) { + resolve(lcpEntries); + observer.disconnect(); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + } + + promise_test(async t => { + await document.fonts.ready; + + // Verify web font is downloaded. + assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported"); + let url = '/fonts/AD.woff?pipe=trickle(d0.5)'; + var absoluteURL = new URL(url, location.href).href; + assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font\ + should be downloaded'); + + // Verify web font is available. + assert_true(document.fonts.check('16px ADTestFaceSwapSubnode'), 'Font should be the web font added'); + + let entryList = await LCPEntryList(); + + // Verify there are 2 LCP entries emitted. + assert_equals(entryList.length, 2, 'There should be 2 LCP entries. The 1st one corresponds to the system font and the 2nd the web font.') + + // Verify the size of 2nd LCP entry is larger than that of the 1st one. + assert_greater_than(entryList[1].size, entryList[0].size, 'The size of 2nd LCP entry should be larger than that of the 1st one.'); + + }, "LCP should be updated if the web font styled text resizes to be larger during the swap period"); +</script> diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap.html new file mode 100644 index 0000000000..61c00fad20 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + @font-face { + font-family: 'ADTestFaceSwap'; + src: url('/fonts/AD.woff?pipe=trickle(d0.5)'); + font-display: swap; + } + + .test { + font-family: 'ADTestFaceSwap'; + } + +</style> +<!-- + Web-font styled text that gets resized during swap period should make a + LCP emission if the new size is larger than the existing LCP element size. +--> +<div class="test">LCP: Web Font Styled Text Resize</div> +<script> + function LCPEntryList() { + return new Promise(resolve => { + let = lcpEntries = []; + new PerformanceObserver((entryList, observer) => { + lcpEntries = lcpEntries.concat(entryList.getEntries()); + if (lcpEntries.length == 2) { + resolve(lcpEntries); + observer.disconnect(); + } + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); + } + + promise_test(async t => { + await document.fonts.ready; + + // Verify web font is downloaded. + assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported"); + let url = '/fonts/AD.woff?pipe=trickle(d0.5)'; + var absoluteURL = new URL(url, location.href).href; + assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font\ + should be downloaded'); + + // Verify web font is available. + assert_true(document.fonts.check('16px ADTestFaceSwap'), 'Font should be the web font added'); + + let entryList = await LCPEntryList(); + + // Verify there are 2 LCP entries emitted. + assert_equals(entryList.length, 2, 'There should be 2 LCP entries. The 1st one corresponds to the system font and the 2nd the web font.') + + // Verify the size of 2nd LCP entry is larger than that of the 1st one. + assert_greater_than(entryList[1].size, entryList[0].size, 'The size of 2ndLCP entry should be larger than that of the 1st one.'); + + }, "LCP should be updated if the web font styled text resizes to be larger during the swap period"); +</script> |