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