diff options
Diffstat (limited to 'testing/web-platform/tests/largest-contentful-paint')
69 files changed, 3080 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/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..84d1c7ff9a --- /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/green-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..e67e21a17c --- /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/green-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..1aee495fe1 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<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/green-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/green-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..3e557a4fdc --- /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/green-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..b3ce16f03f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html @@ -0,0 +1,107 @@ +<!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}); + setup(() => + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is 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'})); + popup.document.body.appendChild(container); + return { + lcpSize: (await await_with_timeout(1000, 'not reported', 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'); + + promise_test(async t => { + const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {transform: 'scale(2)'}); + 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); + }, 'An intersecting element with a partial-intersecting image (object-position) should report the 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 10x'}); + assert_equals(lcpSize, 100); + }, 'A scaled-down background image should report the background size'); +</script> +</body> 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..b4d68a5cb9 --- /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/yellow.png?pipe=trickle(d1)' width="1000" height="1000"/> + <img src='/images/green-1x1.png?1' width="1000" height="1000"/> + <img src='/images/green-1x1.png?2' width="1000" height="1000"/> + <img src='/images/green-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("yellow.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..948f00d0c6 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/larger-image.html @@ -0,0 +1,58 @@ +<!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 blue.png being reported, as it is the largest. --> + <p>This is some text! :)</p> + <img src='' id='red' /> + <img src='' id='blue' /> + <img src='' id='black' /> + <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('red')); + document.getElementById('red').src = '/images/red.png'; + await promise; + + const beforeLoad = performance.now(); + + promise = image_load_promise(document.getElementById('blue')); + document.getElementById('blue').src = '/images/blue.png'; + await promise; + + promise = image_load_promise(document.getElementById('black')); + document.getElementById('black').src = '/images/black-rectangle.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 blue image. + if (entry.id !== 'blue') + return; + + const url = window.location.origin + '/images/blue.png'; + // blue.png is 133 by 106. + const size = 133 * 106; + checkImage(entry, url, 'blue', 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..8758c1c839 --- /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='green1' /> + <div id='text1'></div> + <div id='text2'></div> + <img id='green2' /> + <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('green1', '/images/green-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('green2', '/images/green-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..16cbd0f0cb --- /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/green-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..bbd87235e8 --- /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/green-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..50f9a229ea --- /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/green-100x50.png'; + let big_image_path = '/images/green-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-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-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..6a2ce5c7c6 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html @@ -0,0 +1,31 @@ +<!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/green-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/blue.png'; + const url = window.location.origin + '/images/blue.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]; + // blue.png is 133 by 106. + 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/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/resources/iframe-stores-entry.html b/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html new file mode 100644 index 0000000000..cd60025480 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<body> +<p>Text</p> +<script> + const observer = new PerformanceObserver(entryList => { + window.parent.triggerTest(entryList.getEntries()[0]); + }); + observer.observe({type: 'largest-contentful-paint', buffered: true}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js b/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js new file mode 100644 index 0000000000..bad078e35f --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js @@ -0,0 +1,22 @@ +async_test(t => { + assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"); + const observer = new PerformanceObserver( + t.step_func(entryList => { + entryList.getEntries().forEach(entry => { + // May receive a text entry. Ignore that entry. + if (!entry.url) { + return; + } + // The images should not have caused an entry, so fail test. + assert_unreached('Should not have received an entry! Received one with id ' + + entryList.getEntries()[0].id); + }); + }) + ); + observer.observe({type: 'largest-contentful-paint', buffered: true}); + // Images have been added but should not cause entries to be dispatched. + // Wait for 500ms and end test, ensuring no entry was created. + t.step_timeout(() => { + t.done(); + }, 500); +}, 'Images with opacity: 0, visibility: hidden, or display: none are not observable by LargestContentfulPaint.'); diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js new file mode 100644 index 0000000000..3ac3705e8e --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js @@ -0,0 +1,173 @@ +const image_delay = 1000; +const delay_pipe_value = image_delay / 1000; + +const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => { + let timeout_id; + const timeout = new Promise((_, reject) => { + timeout_id = step_timeout(() => + reject(new DOMException(message, "TimeoutError")), delay) + }); + let result = null; + try { + result = await Promise.race([promise, timeout]); + clearTimeout(timeout_id); + } finally { + cleanup(); + } + return result; +}; + +// Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values. +// The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry. +// The |options| parameter may contain some string values specifying the following: +// * 'renderTimeIs0': the renderTime should be 0 (image does not pass Timing-Allow-Origin checks). +// When not present, the renderTime should not be 0 (image passes the checks). +// * 'sizeLowerBound': the |expectedSize| is only a lower bound on the size attribute value. +// When not present, |expectedSize| must be exactly equal to the size attribute value. +// * 'approximateSize': the |expectedSize| is only approximate to the size attribute value. +// This option is mutually exclusive to 'sizeLowerBound'. +function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound, options = []) { + assert_equals(entry.name, '', "Entry name should be the empty string"); + assert_equals(entry.entryType, 'largest-contentful-paint', + "Entry type should be largest-contentful-paint"); + assert_equals(entry.duration, 0, "Entry duration should be 0"); + // The entry's url can be truncated. + assert_equals(expectedUrl.substr(0, 100), entry.url.substr(0, 100), + `Expected URL ${expectedUrl} should at least start with the entry's URL ${entry.url}`); + assert_equals(entry.id, expectedID, "Entry ID matches expected one"); + assert_equals(entry.element, document.getElementById(expectedID), + "Entry element is expected one"); + if (options.includes('skip')) { + return; + } + if (options.includes('renderTimeIs0')) { + assert_equals(entry.renderTime, 0, 'renderTime should be 0'); + assert_between_exclusive(entry.loadTime, timeLowerBound, performance.now(), + 'loadTime should be between the lower bound and the current time'); + assert_approx_equals(entry.startTime, entry.loadTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + } else { + assert_between_exclusive(entry.loadTime, timeLowerBound, entry.renderTime, + 'loadTime should occur between the lower bound and the renderTime'); + assert_greater_than_equal(performance.now(), entry.renderTime, + 'renderTime should occur before the entry is dispatched to the observer.'); + assert_approx_equals(entry.startTime, entry.renderTime, 0.001, + 'startTime should be equal to renderTime to the precision of 1 millisecond.'); + } + if (options.includes('sizeLowerBound')) { + assert_greater_than(entry.size, expectedSize); + } else if (options.includes('approximateSize')) { + assert_approx_equals(entry.size, expectedSize, 1); + } else{ + assert_equals(entry.size, expectedSize); + } + + if (options.includes('animated')) { + assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime, + 'firstAnimatedFrameTime should be smaller than loadTime'); + assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime, + 'firstAnimatedFrameTime should be smaller than renderTime'); + assert_less_than(entry.firstAnimatedFrameTime, image_delay, + 'firstAnimatedFrameTime should be smaller than the delay applied to the second frame'); + assert_greater_than(entry.firstAnimatedFrameTime, 0, + 'firstAnimatedFrameTime should be larger than 0'); + } + if (options.includes('animated-zero')) { + assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0'); + } +} + +const load_and_observe = url => { + return new Promise(resolve => { + (new PerformanceObserver(entryList => { + for (let entry of entryList.getEntries()) { + if (entry.url == url) { + resolve(entryList.getEntries()[0]); + } + } + })).observe({ type: 'largest-contentful-paint', buffered: true }); + const img = new Image(); + img.id = 'image_id'; + img.src = url; + document.body.appendChild(img); + }); +}; + +const load_video_and_observe = url => { + return new Promise(resolve => { + (new PerformanceObserver(entryList => { + for (let entry of entryList.getEntries()) { + if (entry.url == url) { + resolve(entryList.getEntries()[0]); + } + } + })).observe({ type: 'largest-contentful-paint', buffered: true }); + const video = document.createElement("video"); + video.id = 'video_id'; + video.src = url; + video.autoplay = true; + video.muted = true; + video.loop = true; + document.body.appendChild(video); + }); +}; + +const getLCPStartTime = (identifier) => { + return new Promise(resolve => { + new PerformanceObserver((entryList, observer) => { + entryList.getEntries().forEach(e => { + if (e.url.includes(identifier)) { + resolve(e); + observer.disconnect(); + } + }); + }).observe({ type: 'largest-contentful-paint', buffered: true }); + }); +} + +const getFCPStartTime = () => { + return performance.getEntriesByName('first-contentful-paint')[0]; +} + +const add_text = (text) => { + const paragraph = document.createElement('p'); + paragraph.innerHTML = text; + document.body.appendChild(paragraph); +} + +const loadImage = (url, shouldBeIgnoredForLCP = false) => { + return new Promise(function (resolve, reject) { + let image = document.createElement('img'); + image.addEventListener('load', () => { resolve(image); }); + image.addEventListener('error', reject); + image.src = url; + if (shouldBeIgnoredForLCP) + image.style.opacity = 0; + document.body.appendChild(image); + }); +} + +const checkLCPEntryForNonTaoImages = (times = {}) => { + const lcp = times['lcp']; + const fcp = times['fcp']; + const lcp_url_components = lcp.url.split('/'); + + if (lcp.loadTime <= fcp.startTime) { + assert_approx_equals(lcp.startTime, fcp.startTime, 0.001, + 'LCP start time should be the same as FCP for ' + + lcp_url_components[lcp_url_components.length - 1]) + + ' when LCP load time is less than FCP.'; + } else { + assert_approx_equals(lcp.startTime, lcp.loadTime, 0.001, + 'LCP start time should be the same as LCP load time for ' + + lcp_url_components[lcp_url_components.length - 1]) + + ' when LCP load time is no less than FCP.'; + } + + assert_equals(lcp.renderTime, 0, + 'The LCP render time of Non-Tao image should always be 0.'); +} + +const raf = () => { + return new Promise(resolve => requestAnimationFrame(resolve)); +} diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js new file mode 100644 index 0000000000..c650a0b747 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js @@ -0,0 +1,8 @@ +self.addEventListener("fetch", e => { + if (e.request.url.endsWith('green.svg')) { + e.respondWith(new Response(`<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"> + <rect fill="lime" width="100" height="50"/> + </svg> + `, { headers: { 'Content-Type': 'image/svg+xml' } })); + } +}); diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html new file mode 100644 index 0000000000..069a50eae9 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> + +<body> + <script> + new PerformanceObserver(entries => { + window.parent.postMessage(entries.getEntries()[0].toJSON()); + }).observe({ entryTypes: ["largest-contentful-paint"] }); + + const image = document.createElement("img"); + image.src = "/images/green.svg"; + image.id = "theImage"; + document.body.appendChild(image); + </script> +</body> diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js b/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js new file mode 100644 index 0000000000..1836f2e4ad --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js @@ -0,0 +1,128 @@ +let counter = 0; +const loadImage = size => { + return event => { + let zoom; + if (location.search.includes("replace")) { + zoom = document.getElementById("image"); + } else { + zoom = new Image(); + } + zoom.src=`/images/green-${size}.png`; + ++counter; + zoom.elementTiming = "zoom" + counter; + document.body.appendChild(zoom); + } +}; +const loadBackgroundImage = size => { + return event => { + const div = document.createElement("div"); + const [width, height] = size.split("x"); + ++counter; + div.style = `background-image: + url(/images/green-${size}.png?${counter}); width: ${width}px; height: ${height}px`; + div.elementTiming = "zoom" + counter; + document.body.appendChild(div); + } +}; + +const registerMouseover = background => { + const image = document.getElementById("image"); + const span = document.getElementById("span"); + const func = background ? loadBackgroundImage : loadImage; + image.addEventListener("mouseover", func("100x50")); + span.addEventListener("mouseover", func("256x256")); +} + +const dispatch_mouseover = () => { + span.dispatchEvent(new Event("mouseover")) +}; + +const wait_for_lcp_entries = async entries_expected => { + await new Promise(resolve => { + let entries_seen = 0; + const PO = new PerformanceObserver(list => { + const entries = list.getEntries(); + for (let entry of entries) { + if (entry.url) { + entries_seen++; + } + } + if (entries_seen == entries_expected) { + PO.disconnect(); + resolve() + } else if (entries_seen > entries_expected) { + PO.disconnect(); + reject(); + } + }); + PO.observe({type: "largest-contentful-paint", buffered: true}); + }); +}; +const wait_for_element_timing_entry = async identifier => { + await new Promise(resolve => { + const PO = new PerformanceObserver(list => { + const entries = list.getEntries(); + for (let entry of entries) { + if (entry.identifier == identifier) { + PO.disconnect(); + resolve() + } + } + }); + PO.observe({type: "element", buffered: true}); + }); +}; +const wait_for_resource_timing_entry = async name => { + await new Promise(resolve => { + const PO = new PerformanceObserver(list => { + const entries = list.getEntries(); + for (let entry of entries) { + if (entry.name.includes(name)) { + PO.disconnect(); + resolve() + } + } + }); + PO.observe({type: "resource", buffered: true}); + }); +}; + +const run_mouseover_test = background => { + promise_test(async t => { + // await the first LCP entry + await wait_for_lcp_entries(1); + // Hover over the image + registerMouseover(background); + if (test_driver) { + await new test_driver.Actions().pointerMove(0, 0, {origin: image}).send(); + } + if (!background) { + await wait_for_element_timing_entry("zoom1"); + } else { + await wait_for_resource_timing_entry("png?1"); + await new Promise(r => requestAnimationFrame(r)); + } + // There's only a single LCP entry, because the zoom was skipped. + await wait_for_lcp_entries(1); + + // Wait 600 ms as the heuristic is 500 ms. + // This will no longer be necessary once the heuristic relies on Task + // Attribution. + await new Promise(r => step_timeout(r, 600)); + + // Hover over the span. + if (test_driver) { + await new test_driver.Actions().pointerMove(0, 0, {origin: span}).send(); + } + if (!background) { + await wait_for_element_timing_entry("zoom2"); + } else { + await wait_for_resource_timing_entry("png?2"); + await new Promise(r => requestAnimationFrame(r)); + } + // There are 2 LCP entries, as the image loaded due to span hover is a + // valid LCP candidate. + await wait_for_lcp_entries(2); + }, `LCP mouseover heuristics ignore ${background ? + "background" : "element"}-based zoom widgets`); +} diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py b/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py new file mode 100644 index 0000000000..780d5736c4 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py @@ -0,0 +1,9 @@ +import time + +def main(request, response): + time.sleep(1) + return [ ("Content-Type", "text/css")], """ + #text { + font-size: 4em; + } + """ 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..a26ddd1420 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html @@ -0,0 +1,148 @@ +<!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/mp4;base64,T2dnUwAC')); + }, "Video of data URI src should trigger an LCP entry to be emitted.") + </script> + <!-- + This is the base64 encoding of the images/pattern.ogv video file. + --> + <video autoplay muted src="data:video/mp4;base64,T2dnUwACAAAAAAAAAADmWaI+AAAAAKRLSUwBKoB0aGVvcmEDAgEAAgACAAAUAAAUAAwAAAAeAAAA + AQAAAQAAAQAAAACw2E9nZ1MAAAAAAAAAAAAA5lmiPgEAAADp90reDj////////////////+QgXRo + ZW9yYQ0AAABMYXZmNTcuMjYuMTAwAQAAAB8AAABlbmNvZGVyPUxhdmM1Ny4yNC4xMDUgbGlidGhl + b3JhgnRoZW9yYb7NKPe5zWsYtalJShBznOYxjFKUpCEIMYxiEIQhCEAAAAAAAAAAAAARba5TZ5LI + /FYS/Hg5W2zmKvVoq1QoEykkWhD+eTmbjWZTCXiyVSmTiSSCGQh8PB2OBqNBgLxWKhQJBGIhCHw8 + HAyGAsFAiDgVFtrlNnksj8VhL8eDlbbOYq9WirVCgTKSRaEP55OZuNZlMJeLJVKZOJJIIZCHw8HY + 4Go0GAvFYqFAkEYiEIfDwcDIYCwUCIOBQLDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw + 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8MDA8SFBQVDQ0OERIVFRQODg8SFBUVFQ + 4QERMUFRUVEBEUFRUVFRUSExQVFRUVFRQVFRUVFRUVFRUVFRUVFRUQDAsQFBkbHA0NDhIVHBwbDg + 0QFBkcHBwOEBMWGx0dHBETGRwcHh4dFBgbHB0eHh0bHB0dHh4eHh0dHR0eHh4dEAsKEBgoMz0MDA + 4TGjo8Nw4NEBgoOUU4DhEWHTNXUD4SFiU6RG1nTRgjN0BRaHFcMUBOV2d5eGVIXF9icGRnYxMTEx + MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx + MTExMSEhUZGhoaGhIUFhoaGhoaFRYZGhoaGhoZGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGh + oaGhoaGhoaGhoaERIWHyQkJCQSFBgiJCQkJBYYISQkJCQkHyIkJCQkJCQkJCQkJCQkJCQkJCQkJC + QkJCQkJCQkJCQkJCQkJCQkJBESGC9jY2NjEhUaQmNjY2MYGjhjY2NjYy9CY2NjY2NjY2NjY2NjY2 + NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFR + UVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVEhISFRcYGRsSEhUXGBkbHBIVFxgZGxwdFR + cYGRscHR0XGBkbHB0dHRgZGxwdHR0eGRscHR0dHh4bHB0dHR4eHhERERQXGhwgEREUFxocICIRFB + caHCAiJRQXGhwgIiUlFxocICIlJSUaHCAiJSUlKRwgIiUlJSkqICIlJSUpKioQEBAUGBwgKBAQFB + gcICgwEBQYHCAoMEAUGBwgKDBAQBgcICgwQEBAHCAoMEBAQGAgKDBAQEBggCgwQEBAYICAB8Xlx0 + fV7c7D8vrrAaZid8hRvB1RN7csxFuo43wH7lEkS9wbGS+tVSNMyuxdiECcjB7R1Ml85htasNjKpS + vPt3D8k7iGmZXYuxBC+RR4arUGxkvH5y7mJXR7R5Jwn3VUhBiuap91VIrsaCM5TSg9o867khwMrW + Y2+cP4rwvBLzt/wnHaYe0edSRMYC6tZmU1BrvhktIUf2gXoU8bHMuyNA7lB7R51ym213sFcFKowI + viT/i0Wscg+4RDubX+4haRsMxZWgN05K5FD3bzqS9VSVCPM4TpWs2C43ihFdgaSByeKHu3Xf/2TG + 8tgpB7PAtOs7jixWYw+Ayo5GjUTSybX/1KW52RxYfB8nBNLJtHgt4DPq6BZWBFpjyZX/1KW5Ca0e + vOwG1EX/A9j5fQm5hOz6W2CtcCaWTXTFAeZO71VIgCTX69y9TiaXag3Os2ES1DcLKw0/xR5HfnCq + kpQF0Z1kxKNfhZWLycml2keduHMQh3HubB/pbUUoCK5wxetZRZWPJF/bdyE21H2YjMOhP/pkthqK + UCOEWVm68+1J5n7ahES5sOhaZPdOC5j4kc91FVIsrF8ofe+A2on/16Z4RiKQZcMU3NouO9N4YAvr + WaiA6h4bfLqhTitbnnJ2iPSVRNJH+aZGE+YXzq7Ah/OncW2K59AKamlocOUYTSvaJPNcjDfMGrmG + 9pOV2MbgI9v3B3ECZ7RLJ51UpzMn0C1huA87Ngom9lkiaw3t5yvFZmDl1HpkuP+PiqlawgD69jAT + 5Nxr2i6cwiytcwHhK2KJvZI9C1m/4VUil8RvO/ydxmgsFdzdgGpMbUeyyRNOi1k5hMb6hVSMuTrO + E/xuDhGExQ219l07sV2kG5fOEnkWHwgqUkbvC0P2KTytY4nHLqJDc3DMGlDbX2aXK/4UuJxizaIk + ZITS7a3HN5374PrVlYKIcP9xl1BUKqQ7aAml2k1o5uGcN8A+tPz1HF1YVnmE7cyx4FIiUA2ml1k0 + hX9HB7l4tMO+R9YrMWcf5Anub1BZXUp3Ce4jBM21l0kyhcF/vg6FGeHa345MYv4BVSciTJhj5Abu + D2K0dfIXc4jKAbazaS53rv1lYqpIVr2fcgcPox4u/WVnRfJ25GGING2s2cqjKIVUtwGbRtrljLd9 + CQOHhewUTfiKxWk7Olr2dHyIKlLgejEbasmmdGF/dhuhVrU9xGi6Hksgm/+5Bw813T3mJyRNqIYG + dYspVZFzQ6dhNLJ7H+fYWh8Q+cMbzLc/O0evM4srXGjpECaXaT2jApqM4LRavgPnH7ecDRQSErab + X3zC4EcXfOVZZUpYs3UIfMsKVR+6hgFzHhvWWWl4EqZtrJpHnyeO0T2icPrqVRyyDRKmbayexv7w + dolGfh1hwtsK4G5jDOIHz/lTULUM47PaBmNJm2ssmTq+ssXeHBjgij3G5P+u5QVFIGQ21TNM5aGO + HbqKssQ/HiM9kvcWjdCtF6gZNMzbXFhNP2gV2FNQi+OpOR+S+3RvOBVSOr+E5hjyPrQho7/QDNEG + 2qRNLpHl6WVl3m4p3POFvwEWUN0ByvCQTSttdM48H7tjQWVk73qoUvhiSDbVK0mzyohbuHXofmEa + K/xXYJ+Vq7tBUN6lMAdrouC3p96IS8kMzbVK0myY4f+HKdRGsrG9SlDwEfQkXsGLIbapmmcv/sA5 + TrqC36t4sRdjylU4JC9KwG2plM0zxuT2iFFzAPXyj9ZWRu+tx5UpFv0jn0gQrKyMF5MyaZsDbXG7 + /qIdp0tHG4jOQumLzBliaZttaLfZFUBSOu7FaUn/+IXETfwUj2E0o6gJ2HB/l8N7jFnzWWBESEra + bWPvy9bUKqS4y78CME0rbXSTNFRf8H7r1wwxQbltish5nFVIRkhKaTNtc6L3LHAh8+B2yi/tHvXG + 4nusVwAKMb/0/MCmoWrvASDM0mbay5YRI+7CtC96OPtxudDEyTGmbbWVRgkvR8qaiA8+rLCft7cW + 8H8UI3E8nzmJVSQIT3+0srHfUbgKA21ZNM8WEy+W7wbj9OuBpm21MKGWN80kaA5PZfoSqkRPLa1h + 31wIEjiUhcnX/e5VSWVkQnPhtqoYXrjLFpn7M8tjB17xSqfWgoA21StJpM48eSG+5A/dsGUQn8sV + 7impA4dQjxPyrsBfHd8tUGBIJWkxtrnljE3eu/xTUO/nVsA9I4uVlZ5uQvy9IwYjbWUmaZ5XE9HA + WVkXUKmoI3y4vDKZpnKNtccJHK2iA83ej+fvgI3KR9P6qpG/kBCUdxHFisLkq8aZttTCZlj/b0G8 + XoLX/3fHhZWCVcMsWmZtqmYXz0cpOiBHCqpKUZu76iICRxYVuSULpmF/421MsWmfyhbP4ew1FVKA + jFlY437JXImUTm2r/4ZYtMy61hf16RPJIRA8tU1BDc5/JzAkEzTM21lyx7sK9wojRX/OHXoOv05I + DbUymaZyscL7qlMA8c/CiK3csceqzuOEU1EPpbz4QEahIShpm21MJmWN924f98WKyf51EEYBli0z + NtUzC+6X9P9ysrU1CHyA3RJFFr1w67HpyULT+YMsWmZtquYXz97oKil44sI1bpL8hRSDeMkhiIBw + OgxwZ5Fs6+5M+NdH+3Kjv0sreSqqRvGSQxEA4HQY4M8i2dfcmfGuj/blR36WVvJVVI3jJIYiAcDo + McGeRbOvuTPjXR/tyo79LK3kqqkVUnCfqAES8EzTM21lykY4Q+LKxby+9F3ZHR/uC2OGpS9cv6BZ + XAebhckMGIymaZm2st8/B38i6A/n58pVLKwfURet4UBwSF6UaZttSZljhd2jW9BZWcrX0/hG4Sdt + /SBCdH6UMJmWK80zba3URKaik8iB9PR2459CuyOAbi0/GWLTMmYXm2t0vUkNQhRPVldKpAN5HgHy + ZfdOtGuj/YxwZ5S8u3CjqMgQoyQJRdawvJlE530/+sVg21c8GWLTPf3yJVSVUoCMWVjjfslciZRO + bav/hli0zLrWF/XpE8khT2dnUwAAQAAAAAAAAADmWaI+AgAAAA6lVqcC/yoswcgPXVDgwIQQYBK8 + JHGwwhNHk7ANjkOfhcKbwVnSYLxZeR6bjpWsA1Y6R+wZcHLxjMJmal1gZAIcaRD6tAh8GgJhGSjo + RpMND9CVoehCgT8+hDwjEFfgrNOcG5ocU5t9Rh4kcJd9R5dXmYNMXqo9IPXZaceU+4mxu3IDOM1n + B9miEvZlRdJVqYnBiCi1YTjCdVTR09SWSpdB0VbJ7X5K4yFui7Z1Jd5eyf7J8/JTx+2ji97qbeSC + 43FWTwEqySHBdRDnj5dGfPoc5Z1yIDxOmzKKkkTEqU7vDvwgj0xCs82zZqaO10ruMxgqZ/lTOZZm + VXKlRKlXdtEJ3duzrZ6ubMGIlwLV6+zHVrI8HdzncTtng2MaugcSSKFo5x4tqwQh7fGnvW140wBP + Z2dTAABLAAAAAAAAAOZZoj4DAAAAwc+ctgsAAAAAAAAAAAAAAE9nZ1MAAEADAAAAAAAA5lmiPgQA + AADHLitMAv8qLMHID11Q4MCEEGASvCRxsMITR5OwDY5Dn4XCm8FZ0mC8WXkem46VrANWOkfsGXBy + 8YzCZmpdYGQCHGkQ+rQIfBoCYRko6EaTDQ/QlaHoQoE/PoQ8IxBX4KzTnBuaHFObfUYeJHCXfUeX + V5mDTF6qPSD12WnHlPuJsbtyAzjNZwfZohL2ZUXSVamJwYgotWE4wnVU0dPUlkqXQdFWye1+SuMh + bou2dSXeXsn+yfPyU8fto4ve6m3kguNxVk8BKskhwXUQ54+XRnz6HOWdciA8TpsyipJExKlO7w78 + II9MQrPNs2amjtdK7jMYKmf5UzmWZlVypUSpV3bRCd3bs62ermzBiJcC1evsx1ayPB3c53E7Z4Nj + GroHEkihaOceLasEIe3xp71teNMAT2dnUwAASwMAAAAAAADmWaI+BQAAAL37AWwLAAAAAAAAAAAA + AABPZ2dTAABABgAAAAAAAOZZoj4GAAAA5hY3pwL/KizByA9dUODAhBBgErwkcbDCE0eTsA2OQ5+F + wpvBWdJgvFl5HpuOlawDVjpH7BlwcvGMwmZqXWBkAhxpEPq0CHwaAmEZKOhGkw0P0JWh6EKBPz6E + PCMQV+Cs05wbmhxTm31GHiRwl31Hl1eZg0xeqj0g9dlpx5T7ibG7cgM4zWcH2aIS9mVF0lWpicGI + KLVhOMJ1VNHT1JZKl0HRVsntfkrjIW6LtnUl3l7J/snz8lPH7aOL3upt5ILjcVZPASrJIcF1EOeP + l0Z8+hzlnXIgPE6bMoqSRMSpTu8O/CCPTEKzzbNmpo7XSu4zGCpn+VM5lmZVcqVEqVd20Qnd27Ot + nq5swYiXAtXr7MdWsjwd3OdxO2eDYxq6BxJIoWjnHi2rBCHt8ae9bXjTAE9nZ1MAAEsGAAAAAAAA + 5lmiPgcAAACJ2bKjCwAAAAAAAAAAAAAAT2dnUwAAQAkAAAAAAADmWaI+CAAAAP/6iUwC/yoswcgP + XVDgwIQQYBK8JHGwwhNHk7ANjkOfhcKbwVnSYLxZeR6bjpWsA1Y6R+wZcHLxjMJmal1gZAIcaRD6 + tAh8GgJhGSjoRpMND9CVoehCgT8+hDwjEFfgrNOcG5ocU5t9Rh4kcJd9R5dXmYNMXqo9IPXZaceU + +4mxu3IDOM1nB9miEvZlRdJVqYnBiCi1YTjCdVTR09SWSpdB0VbJ7X5K4yFui7Z1Jd5eyf7J8/JT + x+2ji97qbeSC43FWTwEqySHBdRDnj5dGfPoc5Z1yIDxOmzKKkkTEqU7vDvwgj0xCs82zZqaO10ru + MxgqZ/lTOZZmVXKlRKlXdtEJ3duzrZ6ubMGIlwLV6+zHVrI8HdzncTtng2MaugcSSKFo5x4tqwQh + 7fGnvW140wBPZ2dTAABLCQAAAAAAAOZZoj4JAAAAZcFzUwsAAAAAAAAAAAAAAE9nZ1MAAEAMAAAA + AAAA5lmiPgoAAADewpWnAv8qLMHID11Q4MCEEGASvCRxsMITR5OwDY5Dn4XCm8FZ0mC8WXkem46V + rANWOkfsGXBy8YzCZmpdYGQCHGkQ+rQIfBoCYRko6EaTDQ/QlaHoQoE/PoQ8IxBX4KzTnBuaHFOb + fUYeJHCXfUeXV5mDTF6qPSD12WnHlPuJsbtyAzjNZwfZohL2ZUXSVamJwYgotWE4wnVU0dPUlkqX + QdFWye1+SuMhbou2dSXeXsn+yfPyU8fto4ve6m3kguNxVk8BKskhwXUQ54+XRnz6HOWdciA8Tpsy + ipJExKlO7w78II9MQrPNs2amjtdK7jMYKmf5UzmWZlVypUSpV3bRCd3bs62ermzBiJcC1evsx1ay + PB3c53E7Z4NjGroHEkihaOceLasEIe3xp71teNMAT2dnUwAASwwAAAAAAADmWaI+CwAAAFHjwJwL + AAAAAAAAAAAAAABPZ2dTAABADwAAAAAAAOZZoj4MAAAAF0noTAL/KizByA9dUODAhBBgErwkcbDC + E0eTsA2OQ5+FwpvBWdJgvFl5HpuOlawDVjpH7BlwcvGMwmZqXWBkAhxpEPq0CHwaAmEZKOhGkw0P + 0JWh6EKBPz6EPCMQV+Cs05wbmhxTm31GHiRwl31Hl1eZg0xeqj0g9dlpx5T7ibG7cgM4zWcH2aIS + 9mVF0lWpicGIKLVhOMJ1VNHT1JZKl0HRVsntfkrjIW6LtnUl3l7J/snz8lPH7aOL3upt5ILjcVZP + ASrJIcF1EOePl0Z8+hzlnXIgPE6bMoqSRMSpTu8O/CCPTEKzzbNmpo7XSu4zGCpn+VM5lmZVcqVE + qVd20Qnd27Otnq5swYiXAtXr7MdWsjwd3OdxO2eDYxq6BxJIoWjnHi2rBCHt8ae9bXjTAE9nZ1MA + AEsPAAAAAAAA5lmiPg0AAAAt111GCwAAAAAAAAAAAAAAT2dnUwAAQBIAAAAAAADmWaI+DgAAALvQ + BAYC/yoswcgPXVDgwIQQYBK8JHGwwhNHk7ANjkOfhcKbwVnSYLxZeR6bjpWsA1Y6R+wZcHLxjMJm + al1gZAIcaRD6tAh8GgJhGSjoRpMND9CVoehCgT8+hDwjEFfgrNOcG5ocU5t9Rh4kcJd9R5dXmYNM + Xqo9IPXZaceU+4mxu3IDOM1nB9miEvZlRdJVqYnBiCi1YTjCdVTR09SWSpdB0VbJ7X5K4yFui7Z1 + Jd5eyf7J8/JTx+2ji97qbeSC43FWTwEqySHBdRDnj5dGfPoc5Z1yIDxOmzKKkkTEqU7vDvwgj0xC + s82zZqaO10ruMxgqZ/lTOZZmVXKlRKlXdtEJ3duzrZ6ubMGIlwLV6+zHVrI8HdzncTtng2MaugcS + SKFo5x4tqwQh7fGnvW140wBPZ2dTAABLEgAAAAAAAOZZoj4PAAAAgHc9kAsAAAAAAAAAAAAAAE9n + Z1MAAEAVAAAAAAAA5lmiPhAAAAD0zWMtAv8qLMHID11Q4MCEEGASvCRxsMITR5OwDY5Dn4XCm8FZ + 0mC8WXkem46VrANWOkfsGXBy8YzCZmpdYGQCHGkQ+rQIfBoCYRko6EaTDQ/QlaHoQoE/PoQ8IxBX + 4KzTnBuaHFObfUYeJHCXfUeXV5mDTF6qPSD12WnHlPuJsbtyAzjNZwfZohL2ZUXSVamJwYgotWE4 + wnVU0dPUlkqXQdFWye1+SuMhbou2dSXeXsn+yfPyU8fto4ve6m3kguNxVk8BKskhwXUQ54+XRnz6 + HOWdciA8TpsyipJExKlO7w78II9MQrPNs2amjtdK7jMYKmf5UzmWZlVypUSpV3bRCd3bs62ermzB + iJcC1evsx1ayPB3c53E7Z4NjGroHEkihaOceLasEIe3xp71teNMAT2dnUwAASxUAAAAAAADmWaI+ + EQAAAKLKJiULAAAAAAAAAAAAAABPZ2dTAARAGAAAAAAAAOZZoj4SAAAAmbxPPwL/KizByA9dUODA + hBBgErwkcbDCE0eTsA2OQ5+FwpvBWdJgvFl5HpuOlawDVjpH7BlwcvGMwmZqXWBkAhxpEPq0CHwa + AmEZKOhGkw0P0JWh6EKBPz6EPCMQV+Cs05wbmhxTm31GHiRwl31Hl1eZg0xeqj0g9dlpx5T7ibG7 + cgM4zWcH2aIS9mVF0lWpicGIKLVhOMJ1VNHT1JZKl0HRVsntfkrjIW6LtnUl3l7J/snz8lPH7aOL + 3upt5ILjcVZPASrJIcF1EOePl0Z8+hzlnXIgPE6bMoqSRMSpTu8O/CCPTEKzzbNmpo7XSu4zGCpn + +VM5lmZVcqVEqVd20Qnd27Otnq5swYiXAtXr7MdWsjwd3OdxO2eDYxq6BxJIoWjnHi2rBCHt8ae9 + bXjTAA==" 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..fdc691819b --- /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'/> 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> |