summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/largest-contentful-paint
diff options
context:
space:
mode:
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/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.html55
-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.html107
-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.html58
-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-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-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.html31
-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/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.html148
-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
69 files changed, 3080 insertions, 0 deletions
diff --git a/testing/web-platform/tests/largest-contentful-paint/META.yml b/testing/web-platform/tests/largest-contentful-paint/META.yml
new file mode 100644
index 0000000000..e11810cc10
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/largest-contentful-paint/
+suggested_reviewers:
+ - npm1
+ - yoavweiss
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
new file mode 100644
index 0000000000..a2c0d7975a
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 136 is the size of the animated GIF up until the first frame.
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/images/anim-gr.gif?pipe=trickle(136:d${delay_pipe_value})`;
+ const entry = await load_and_observe(url);
+ // anim-gr.gif is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
new file mode 100644
index 0000000000..de59d5c5f7
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 142 is the size of the animated WebP up until the first frame.
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/images/webp-animated.webp?pipe=trickle(142:d${delay_pipe_value})`;
+ const entry = await load_and_observe(url);
+ // webp-animated.webp is 11 by 29.
+ const size = 11 * 29;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html
new file mode 100644
index 0000000000..cf7d262b0f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ // the first frame data is done).
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/images/anim-gr.png?pipe=trickle(262:d${delay_pipe_value})`;
+ const entry = await load_and_observe(url);
+ // anim-gr.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
new file mode 100644
index 0000000000..993883c607
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ //the first frame data is done).
+ const {REMOTE_ORIGIN} = get_host_info();
+ const url = REMOTE_ORIGIN +
+ '/images/anim-gr.png?pipe=trickle(262:d1)';
+ const entry = await load_and_observe(url);
+ // anim-gr.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["renderTimeIs0", "animated-zero"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
new file mode 100644
index 0000000000..137dde6638
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ //the first frame data is done).
+ const {REMOTE_ORIGIN} = get_host_info();
+ const url = REMOTE_ORIGIN +
+ '/images/anim-tao.png?pipe=trickle(262:d1)';
+ const entry = await load_and_observe(url);
+ // anim-gr.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html
new file mode 100644
index 0000000000..6bbc0958b1
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe image.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 262 is the size of the animated PNG up until the first frame,
+ // including the chunk that starts the second frame (indicating that
+ //the first frame data is done).
+ const url = window.location.origin + '/images/blue.png';
+ const entry = await load_and_observe(url);
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ["animated-zero"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-video.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-video.tentative.html
new file mode 100644
index 0000000000..49bdd986f6
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-video.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>Largest Contentful Paint: observe video.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async () => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ // 136 is the size of the animated GIF up until the first frame.
+ // The trickle pipe delays the response after the first frame by 1 second.
+ const url = window.location.origin +
+ `/media/test-1s.webm?pipe=trickle(1500:d${delay_pipe_value})`;
+ const entry = await load_video_and_observe(url);
+ // Video is 320 x 184.
+ const size = 320 * 184;
+ // TODO(yoav): Validate size as well as load and render times. "skip" is
+ // currently causing those checks to be skipped.
+ checkImage(entry, url, 'video_id', size, beforeLoad, ["skip"]);
+ }, "Same origin animated image is observable and has a first frame.");
+ </script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/contracted-image.html b/testing/web-platform/tests/largest-contentful-paint/contracted-image.html
new file mode 100644
index 0000000000..8816bf4ba9
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/contracted-image.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: contracted image bounded by display size.</title>
+<style type="text/css">
+ #image_id {
+ width: 50px;
+ height: 50px;
+ }
+</style>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/black-rectangle.png';
+ // black-rectangle.png is 100 x 50. It occupies 50 x 50 so size will be bounded by the displayed size.
+ const size = 50 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Largest Contentful Paint: |size| attribute is bounded by display size.');
+</script>
+<img src='/images/black-rectangle.png' id='image_id'/>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/cross-origin-image.sub.html b/testing/web-platform/tests/largest-contentful-paint/cross-origin-image.sub.html
new file mode 100644
index 0000000000..0cfdd1791b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/cross-origin-image.sub.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe cross-origin images but without renderTime.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = 'http://{{domains[www]}}:{{ports[http][1]}}/images/blue.png';
+ // blue.png is 133 x 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad, ['renderTimeIs0']);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Cross-origin image is observable, with renderTime equal to 0.');
+</script>
+
+<img src='http://{{domains[www]}}:{{ports[http][1]}}/images/blue.png' id='image_id'/>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/element-only-when-fully-active.html b/testing/web-platform/tests/largest-contentful-paint/element-only-when-fully-active.html
new file mode 100644
index 0000000000..519b249196
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/element-only-when-fully-active.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: element is only exposed for fully active documents.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe src="resources/iframe-stores-entry.html" id="ifr"></iframe>
+<script>
+ setup({"hide_test_state": true});
+ let t = async_test('Only expose element attribute for fully active documents');
+ window.triggerTest = t.step_func_done(entry => {
+ assert_not_equals(entry.element, null);
+ const iframe = document.getElementById('ifr');
+ iframe.remove();
+ assert_equals(entry.element, null);
+ });
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/expanded-image.html b/testing/web-platform/tests/largest-contentful-paint/expanded-image.html
new file mode 100644
index 0000000000..90f803930c
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/expanded-image.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: expanded image bounded by intrinsic size.</title>
+<style type="text/css">
+ #image_id {
+ width: 300px;
+ height: 300px;
+ }
+</style>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/black-rectangle.png';
+ // black-rectangle.png is 100 x 50. It occupies 300 x 300 so size will be bounded by the intrinsic size.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Largest Contentful Paint: |size| attribute is bounded by intrinsic size.');
+</script>
+<img src='/images/black-rectangle.png' id='image_id'/>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/first-letter-background.html b/testing/web-platform/tests/largest-contentful-paint/first-letter-background.html
new file mode 100644
index 0000000000..56ac105677
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/first-letter-background.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe element with background image in its first letter</title>
+<body>
+<style>
+div::first-letter {
+ background-image: url('/images/black-rectangle.png');
+}
+div {
+ font-size: 12px;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeLoad = performance.now();
+ let observedFirstLetter = false;
+ const observer = new PerformanceObserver(
+ t.step_func(function(entryList) {
+ const entry = entryList.getEntries()[entryList.getEntries().length -1];
+ if (!observedFirstLetter) {
+ // When we haven't observed first-letter as LCP...
+ // If we happen to get a text entry due to text happening before the image, return.
+ if (entry.url === '') {
+ assert_equals(entry.entryType, 'largest-contentful-paint');
+ assert_greater_than_equal(entry.renderTime, beforeLoad);
+ assert_greater_than_equal(performance.now(), entry.renderTime);
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ assert_equals(entry.duration, 0);
+ assert_equals(entry.loadTime, 0);
+ assert_equals(entry.id, 'target');
+ assert_equals(entry.element, document.getElementById('target'));
+ } else {
+ const url = window.location.origin + '/images/black-rectangle.png';
+ checkImage(entry, url, 'target', 0, beforeLoad, ['sizeLowerBound']);
+ }
+
+ // Now change the div content to proceed to the second part of the test.
+ beforeLoad = performance.now();
+ const div = document.createElement('div');
+ div.id = 'target2';
+ div.innerHTML = 'long text will now be LCP';
+ document.body.appendChild(div);
+ observedFirstLetter = true;
+ } else {
+ // Ignore entries that are caused by the initial 'target'.
+ if (entry.id === 'target')
+ return;
+ // The LCP must now be text.
+ if (entry.url !== '')
+ assert_unreached('First-letter background should not be LCP!');
+
+ assert_equals(entry.entryType, 'largest-contentful-paint');
+ assert_greater_than_equal(entry.renderTime, beforeLoad, 'blaaa');
+ assert_greater_than_equal(performance.now(), entry.renderTime, 'bleee');
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ assert_equals(entry.duration, 0);
+ assert_equals(entry.id, 'target2');
+ const div = document.getElementById('target2');
+ // Estimate the text size: 12 * 100
+ assert_greater_than_equal(entry.size, 1200);
+ assert_equals(entry.loadTime, 0);
+ assert_equals(entry.element, div);
+ t.done();
+ }
+ }));
+ observer.observe({entryTypes: ['largest-contentful-paint']});
+ }, 'Largest Contentful Paint: first-letter is observable.');
+</script>
+<div id='target'>A</div>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/first-paint-equals-lcp-text.html b/testing/web-platform/tests/largest-contentful-paint/first-paint-equals-lcp-text.html
new file mode 100644
index 0000000000..50bccd072e
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/first-paint-equals-lcp-text.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>LargestContentfulPaint compared with FirstPaint and FirstContentfulPaint on single text page.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ async_test(function (t) {
+ assert_implements(window.PerformancePaintTiming, "PerformancePaintTiming is not implemented");
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let firstPaintTime = 0;
+ let firstContentfulPaintTime = 0;
+ let largestContentfulPaintTime = 0;
+ const observer = new PerformanceObserver(
+ t.step_func(function(entryList) {
+ entryList.getEntries().forEach(entry => {
+ if (entry.name === 'first-paint') {
+ assert_equals(firstPaintTime, 0, 'Only one first-paint entry.');
+ assert_equals(entry.entryType, 'paint');
+ firstPaintTime = entry.startTime;
+ } else if (entry.name === 'first-contentful-paint') {
+ assert_equals(firstContentfulPaintTime, 0, 'Only one first-contentful-paint entry.');
+ assert_equals(entry.entryType, 'paint');
+ firstContentfulPaintTime = entry.startTime;
+ } else {
+ assert_equals(largestContentfulPaintTime, 0, 'Only one largest-contentful-paint entry.');
+ assert_equals(entry.entryType, 'largest-contentful-paint');
+ largestContentfulPaintTime = entry.renderTime;
+ }
+ // LCP fires necessarily after first-paint and first-contentful-paint.
+ if (largestContentfulPaintTime) {
+ assert_equals(firstContentfulPaintTime, largestContentfulPaintTime, 'FCP should equal LCP.');
+ // In PaintTiming spec, first-paint isn't a hard requirement, browsers can support
+ // first-contentful-paint only.
+ if (firstPaintTime) {
+ assert_less_than_equal(firstPaintTime, firstContentfulPaintTime, 'FP should be less than or equal to FCP.');
+ }
+ t.done();
+ }
+ });
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ observer.observe({type: 'paint', buffered: true});
+ }, 'FCP and LCP are the same when there is a single text element in the page.');
+</script>
+<p>Text</p>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/idlharness.html b/testing/web-platform/tests/largest-contentful-paint/idlharness.html
new file mode 100644
index 0000000000..84d1c7ff9a
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/idlharness.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>Largest Contentful Paint IDL tests</title>
+<link rel="help" href="https://wicg.github.io/largest-contentful-paint/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script>
+'use strict';
+
+idl_test(
+ ['largest-contentful-paint'],
+ ['performance-timeline', 'dom', 'hr-time'],
+ async (idl_array, t) => {
+ idl_array.add_objects({
+ LargestContentfulPaint: ['lcp']
+ });
+
+ window.lcp = await new Promise((resolve, reject) => {
+ const observer = new PerformanceObserver(entryList => {
+ resolve(entryList.getEntries()[0]);
+ });
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ t.step_timeout(() => reject('Timed out waiting for LargestContentfulPaint entry'), 3000);
+ });
+ }
+);
+</script>
+<!-- a contentful element to observe -->
+<img src=/images/green-100x50.png>
diff --git a/testing/web-platform/tests/largest-contentful-paint/iframe-content-not-observed.html b/testing/web-platform/tests/largest-contentful-paint/iframe-content-not-observed.html
new file mode 100644
index 0000000000..e605e9f21f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/iframe-content-not-observed.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<head>
+<title>Largest Contentful Paint: do NOT observe elements from same-origin iframes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+ async_test((t) => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const observer = new PerformanceObserver(
+ t.step_func_done(entryList => {
+ assert_unreached("Should not have received an entry!");
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ // After a delay, assume that no entry was produced.
+ t.step_timeout(() => {
+ t.done();
+ }, 200);
+ }, 'Element in child iframe is not observed, even if same-origin.');
+</script>
+<iframe src='resources/iframe-with-content.html'></iframe>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-TAO.sub.html b/testing/web-platform/tests/largest-contentful-paint/image-TAO.sub.html
new file mode 100644
index 0000000000..296fe5e65b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-TAO.sub.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe cross origin images with various Timing-Allow-Origin headers</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<div id='my_div'></div>
+<script>
+ setup({"hide_test_state": true});
+ async_test(t => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const remote_img = 'http://{{domains[www]}}:{{ports[http][1]}}/element-timing/resources/TAOImage.py?'
+ + 'origin=' + window.location.origin +'&tao=';
+ const valid_tao = ['wildcard', 'origin', 'multi', 'multi_wildcard', 'match_origin', 'match_wildcard'];
+ const invalid_tao = ['null', 'space', 'uppercase'];
+ const div = document.getElementById('my_div');
+ let img_length = 20;
+ function addImage(tao) {
+ const img = document.createElement('img');
+ img.src = remote_img + tao;
+ img.id = tao;
+ // Set increasing size so that largest-contentful-paint captures all of them.
+ img_length++;
+ img.height = img_length;
+ img.width = img_length;
+ div.appendChild(img);
+ }
+ let img_count = 0;
+ const total_images = valid_tao.length + invalid_tao.length;
+ let beforeLoad;
+ new PerformanceObserver(
+ t.step_func(entryList => {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const tao = entry.id;
+ const url = remote_img + tao;
+ const size = img_length * img_length;
+ let options = valid_tao.includes(tao) ? [] : ['renderTimeIs0'];
+ checkImage(entry, url, tao, size, beforeLoad, options);
+ img_count++;
+ beforeLoad = performance.now();
+ // Process valid TAO images first.
+ if (img_count < valid_tao.length)
+ addImage(valid_tao[img_count]);
+ // Then add invalid TAO images.
+ else if (img_count < total_images)
+ addImage(invalid_tao[img_count - valid_tao.length]);
+ // Once we've seen all the images, end the test.
+ else
+ t.done();
+ })
+ ).observe({type: 'largest-contentful-paint'});
+ // Add first image, the rest will be added on each observer callback.
+ addImage(valid_tao[0]);
+ beforeLoad = performance.now();
+ }, 'Cross-origin elements with valid TAO have correct renderTime, with invalid TAO have renderTime set to 0.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-full-viewport.html b/testing/web-platform/tests/largest-contentful-paint/image-full-viewport.html
new file mode 100644
index 0000000000..e67e21a17c
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-full-viewport.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: size when image overflows</title>
+<!-- In this test, an image with an intrinsic size of 100 x 50 is added, but
+ scaled up in order to overflow the viewport. It should not be reported. -->
+<body>
+<style>
+body {
+ margin: 0px;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ const viewportWidth = document.documentElement.clientWidth;
+ const viewportHeight = document.documentElement.clientHeight;
+ setup({"hide_test_state": true});
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1, 'Should have received only one entry!');
+ const entry = entryList.getEntries()[0];
+ if (entry.url)
+ assert_unreached('Should not have received an image entry!');
+ })
+ ).observe({type: 'largest-contentful-paint', buffered: true});
+ // Add an image, setting width and height equal to viewport.
+ img = document.createElement('img');
+ img.src = '/images/green-100x50.png';
+ img.id = 'image_id';
+ img.width = viewportWidth * 2;
+ img.height = viewportHeight * 2;
+ img.onload = () => {
+ const p = document.createElement('p');
+ p.innerHTML = 'a';
+ p.style = 'position: absolute; top: 10px; left: 10px;';
+ document.body.appendChild(p);
+ }
+ document.body.appendChild(img);
+ }, 'The intersectionRect of an img element overflowing is computed correctly');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-inside-svg.html b/testing/web-platform/tests/largest-contentful-paint/image-inside-svg.html
new file mode 100644
index 0000000000..77e42fcc6d
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-inside-svg.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe image inside SVG</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+setup({"hide_test_state": true});
+async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ new PerformanceObserver(
+ t.step_func_done(entryList => {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/blue.png';
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ ).observe({type: 'largest-contentful-paint', buffered: true});
+}, "Image inside SVG is observable.");
+</script>
+<svg width="300" height="300" id='svg_id'>
+ <image href='/images/blue.png' id='image_id'/>
+</svg>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html b/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html
new file mode 100644
index 0000000000..1aee495fe1
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-not-fully-visible.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: size when image overflows</title>
+<body>
+<style>
+body {
+ /* Preventing a scrollbar from showing and removing any margins simplifies
+ the calculations below. */
+ overflow: hidden;
+ margin: 0px;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ let beforeRender;
+ const viewportWidth = document.documentElement.clientWidth;
+ const viewportHeight = document.documentElement.clientHeight;
+ const imgWidth = 100;
+ const imgHeight = 50;
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/green-100x50.png';
+ // To compute the size, compute the percentage of the image visible and
+ // scale by its natural dimensions. In this test, the image is expanded to twice its size
+ // but place towards the bottom right corner of the viewport, so it is
+ // effectively clipped to 50% by 50% of its display size. Scaling by
+ // its natural width and height of 100px and 50px respectively, leads
+ // to a weighted size of 50 by 25.
+ const truncatedWidth = imgWidth / 2;
+ const truncatedHeight = imgHeight / 2;
+ const weightedSize = truncatedWidth * truncatedHeight;
+ checkImage(entry, url, 'image_id', weightedSize, beforeLoad);
+ })
+ ).observe({type: 'largest-contentful-paint', buffered: true});
+ // Add an image, setting width and height equal to viewport.
+ img = document.createElement('img');
+ img.src = '/images/green-100x50.png';
+ img.id = 'image_id';
+ img.width = imgWidth * 2;
+ img.height = imgHeight * 2;
+ img.style.position = 'absolute';
+ img.style.left = viewportWidth - imgWidth + 'px';
+ img.style.top = viewportHeight - imgHeight + 'px';
+ document.body.appendChild(img);
+ }, 'The intersectionRect of an img element overflowing is computed correctly');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-removed-before-load.html b/testing/web-platform/tests/largest-contentful-paint/image-removed-before-load.html
new file mode 100644
index 0000000000..3e557a4fdc
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-removed-before-load.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: largest image is reported.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<img id="target"/>
+<img id="target2"/>
+<script>
+ setup({"hide_test_state": true});
+ const numInitial = 100;
+ const sleep = 1000;
+ const small_img_src = '/images/green-16x16.png';
+ let beforeLoad;
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const img_src = '/element-timing/resources/progressive-image.py?name=square20.jpg&numInitial='
+ + numInitial + '&sleep=' + sleep;
+ const img1 = document.getElementById('target')
+ img1.src = img_src;
+ // After a brief wait, remove the image and add a smaller image to target2.
+ t.step_timeout(() => {
+ img1.parentNode.removeChild(img1);
+ document.getElementById('target2').src = small_img_src;
+ beforeLoad = performance.now();
+ }, 0);
+ new PerformanceObserver(
+ t.step_func(entryList => {
+ let images = entryList.getEntries().filter(e => e.id !== '');
+ if (!images.length)
+ return;
+ assert_equals(images.length, 1, 'Should only receive one entry');
+ const entry = images[0];
+ checkImage(images[0], window.location.origin + small_img_src, 'target2', 16 * 16,
+ beforeLoad);
+ t.done();
+ })
+ ).observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Largest Contentful Paint: image removed before loaded does not produce entry.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-src-change.html b/testing/web-platform/tests/largest-contentful-paint/image-src-change.html
new file mode 100644
index 0000000000..33213a570e
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-src-change.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: src change triggers new entry.</title>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/largest-contentful-paint-helpers.js"></script>
+ <img src='' id='image_id' />
+ <script>
+ setup({ "hide_test_state": true });
+
+ let first_image_src = '/images/black-rectangle.png';
+ let second_image_src = '/images/blue.png';
+ let image_id = 'image_id';
+
+ // Add listener for load event that is fired when image is loaded.
+ const image_load_promise = image_element => {
+ return new Promise(resolve => {
+ image_element.addEventListener('load', resolve);
+ });
+ }
+
+ // Create a promise that resolves when an LCP is observed.
+ const lcp_observation_promise = image_src => {
+ return new Promise(resolve => {
+ new PerformanceObserver((entryList) => {
+ let lcpEntry = entryList.getEntries().find(e => e.url.includes(image_src));
+
+ if (lcpEntry) {
+ resolve(lcpEntry);
+ }
+
+ }).observe({ type: 'largest-contentful-paint' });
+ });
+ }
+
+ const loadImageAndGetLCPEntry = async image_src => {
+ let LCPObserverPromise = lcp_observation_promise(image_src);
+
+ let image_element = document.getElementById(image_id);
+
+ let promise = image_load_promise(image_element);
+
+ image_element.src = image_src;
+
+ await promise;
+
+ return await LCPObserverPromise;
+ }
+
+ promise_test(async t => {
+
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+
+
+ // Load first image.
+ let beforeLoad = performance.now();
+
+ let first_LCP = await loadImageAndGetLCPEntry(first_image_src);
+
+ // Verify first LCP entry correctness. The black-rectangle.png is 100 x 50.
+ checkImage(first_LCP, window.location.origin + first_image_src, image_id, 100 * 50, beforeLoad);
+
+ // Load second image.
+ beforeLoad = performance.now();
+
+ let second_LCP = await loadImageAndGetLCPEntry(second_image_src);
+
+ // Verify second LCP entry correctness. The blue.png is 133 by 106.
+ checkImage(second_LCP, window.location.origin + second_image_src, image_id, 133 * 106, beforeLoad);
+
+ }, 'Largest Contentful Paint: changing src causes a new entry to be dispatched.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-sw-same-origin.https.html b/testing/web-platform/tests/largest-contentful-paint/image-sw-same-origin.https.html
new file mode 100644
index 0000000000..3f375008c5
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-sw-same-origin.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: same-origin service worker should not be treated as TAO-fail</title>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/largest-contentful-paint-helpers.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ <script>
+ setup(() => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ });
+
+ promise_test(async t => {
+ const scope = "resources/lcp-sw.https.html";
+
+ const registration =
+ await service_worker_unregister_and_register(t, "resources/lcp-sw-from-cache.js", "resources/");
+
+ await wait_for_state(t, registration.installing, "activated");
+ t.add_cleanup(() => registration.unregister());
+ const iframe = document.createElement("iframe");
+ iframe.src = scope;
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ const entry = await new Promise(resolve => window.addEventListener("message", e => resolve(e.data)));
+
+ assert_equals(entry.id, "theImage");
+ assert_not_equals(entry.renderTime, 0);
+ }, "Same-origin images served from a service-worker should have a correct renderTime");
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html b/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html
new file mode 100644
index 0000000000..b3ce16f03f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/image-upscaling.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: largest image is reported.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ setup(() =>
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented"));
+
+ const imageURL = `${window.location.origin}/images/blue.png`;
+ async function load_image_and_get_lcp_size(t, imageStyle = {}, containerStyle = {}) {
+ const popup = window.open();
+ t.add_cleanup(() => popup.close());
+ const image = popup.document.createElement('img');
+ image.src = imageURL;
+
+ // We decode the image to get the natural size (though it's a constant)
+ await image.decode();
+ const naturalSize = image.width * image.height;
+
+ const container = popup.document.createElement('div');
+ container.appendChild(image);
+ const applyStyle = (el, style = {}) =>
+ Object.entries(style).forEach(([k, v]) => el.style.setProperty(k, v));
+
+ applyStyle(image, imageStyle);
+ applyStyle(container, containerStyle);
+ image.id = token();
+ container.id = token();
+ const entryReported = new Promise(resolve => new popup.PerformanceObserver(entryList => {
+ entryList.getEntries().forEach(entry => {
+ if (entry.id === image.id || entry.id === container.id)
+ resolve(entry.size);
+ });
+ }).observe({type: 'largest-contentful-paint'}));
+ popup.document.body.appendChild(container);
+ return {
+ lcpSize: (await await_with_timeout(1000, 'not reported', entryReported)),
+ naturalSize
+ };
+ }
+
+ // We set the image to display: none when testing background, so that only the background is reported
+ // and not the image itself
+ const load_background_image_and_get_lcp_size = (t, style) =>
+ load_image_and_get_lcp_size(t, {display: 'none'},
+ {
+ position: 'absolute',
+ 'background-image': `url(${imageURL})`,
+ ...style,
+ });
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t);
+ assert_equals(lcpSize, naturalSize);
+ }, 'Non-scaled image should report the natural size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {width: '50px', height: '50px'});
+ assert_equals(lcpSize, 50 * 50);
+ }, 'A downscaled image (width/height) should report the displayed size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {transform: 'scale(0.5)'});
+ assert_equals(Math.floor(lcpSize), Math.floor(naturalSize / 4));
+ }, 'A downscaled image (using scale) should report the displayed size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {width: '500px', height: '500px'});
+ assert_equals(lcpSize, naturalSize);
+ }, 'An upscaled image (width/height) should report the natural size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {transform: 'scale(2)'});
+ assert_equals(Math.floor(lcpSize), Math.floor(naturalSize));
+ }, 'An upscaled image (using scale) should report the natural size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {'object-size': '300px 300px'});
+ assert_equals(Math.floor(lcpSize), Math.floor(naturalSize));
+ }, 'An upscaled image (using object-size) should report the natural size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_image_and_get_lcp_size(t, {'object-position': '-100px 0'});
+ assert_equals(lcpSize, 3498);
+ }, 'An intersecting element with a partial-intersecting image (object-position) should report the image intersection');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_background_image_and_get_lcp_size(t, {width: '50px', height: '50px'});
+ assert_equals(lcpSize, 50 * 50);
+ }, 'A background image larger than the container should report the container size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_background_image_and_get_lcp_size(t, {width: '300px', height: '300px'});
+ assert_equals(lcpSize, naturalSize);
+ }, 'A background image smaller than the container should report the natural size');
+
+ promise_test(async t => {
+ const {naturalSize, lcpSize} = await load_background_image_and_get_lcp_size(t, {width: '300px', height: '300px', 'background-size': '10px 10x'});
+ assert_equals(lcpSize, 100);
+ }, 'A scaled-down background image should report the background size');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/initially-invisible-images.html b/testing/web-platform/tests/largest-contentful-paint/initially-invisible-images.html
new file mode 100644
index 0000000000..b4d68a5cb9
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/initially-invisible-images.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<head>
+<title>Largest Contentful Paint: initially out-of-viewport image gets an LCP entry once they are visible.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ .flex-container {
+ display: flex;
+ flex-direction: row;
+ width: 1000px;
+ overflow-x: hidden;
+ scroll-behavior: auto;
+ }
+</style>
+</head>
+<body>
+<div>
+<div class='flex-container' id="container">
+ <img src='/images/yellow.png?pipe=trickle(d1)' width="1000" height="1000"/>
+ <img src='/images/green-1x1.png?1' width="1000" height="1000"/>
+ <img src='/images/green-1x1.png?2' width="1000" height="1000"/>
+ <img src='/images/green-1x1.png?3' width="1000" height="1000"/>
+</div>
+</div>
+<script>
+// Spin the carousel
+setup({"hide_test_state": true});
+const images = document.querySelectorAll('img');
+
+let selected = 0;
+const container = document.getElementById("container");
+const transition = () => {
+ container.scrollLeft = selected * 1000;
+ selected = (selected + 1) % images.length;
+}
+
+container.scrollLeft=1000;
+setInterval(transition, 1000);
+
+promise_test(async t => {
+
+ return new Promise(resolve => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const observer = new PerformanceObserver(entryList => {
+ entryList.getEntries().forEach(entry => {
+ // May receive a text entry. Ignore that entry.
+ if (!entry.url) {
+ return;
+ }
+ assert_true(entry.url.includes("yellow.png"), "Re-visible image has an entry");
+ resolve();
+ });
+ });
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ t.step_timeout(() => {
+ assert_unreached("The image should have become visible by now, which should have triggered an LCP entry.");
+ t.done();
+ }, 2000);
+ });
+}, 'Image visibility: out-of-viewport images are observable by LargestContentfulPaint once they become visible.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-1.html b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-1.html
new file mode 100644
index 0000000000..6b33c425b7
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-1.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<title>Largest Contentful Paint: invisible images are not observable</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/invisible-images.js"></script>
+<style>
+ .opacity0 {
+ opacity: 0;
+ }
+ .visibilityHidden {
+ visibility: hidden;
+ }
+ .displayNone {
+ display: none;
+ }
+ .willChangeTransform {
+ will-change: transform;
+ }
+ .willChangeOpacity {
+ will-change: opacity;
+ }
+</style>
+<script>
+setup({"hide_test_state": true});
+</script>
+<img src='/images/blue.png' class='opacity0 willChangeTransform' id='opacity0-willChangeTransform'/>
+<img src='/images/green.png' class='visibilityHidden willChangeTransform' id='visibilityHidden'/>
+<img src='/images/red.png' class='displayNone willChangeTransform' id='displayNone'/>
+<img src='/images/blue.png' class='opacity0 willChangeOpacity' id='opacity0-willChangeOpacity'/>
+<div class='opacity0 composited'><img src='/images/yellow.png' id='divOpacity0'/></div>
+<div class='visibilityHidden composited'><img src='/images/yellow.png' id='divVisibilityHidden'/></div>
+<div class='displayNone composited'><img src='/images/yellow.png' id='divDisplayNone'/></div>
diff --git a/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-2.html b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-2.html
new file mode 100644
index 0000000000..8ab32ebb1f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/invisible-images-composited-2.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>Largest Contentful Paint: invisible images are not observable</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/invisible-images.js"></script>
+<style>
+ .opacity0 {
+ opacity: 0;
+ }
+ .visibilityHidden {
+ visibility: hidden;
+ }
+ .displayNone {
+ display: none;
+ }
+ .composited {
+ will-change: transform;
+ }
+ img {
+ border: 2px solid black;
+ will-change: transform;
+ }
+</style>
+<div class='opacity0 composited'><img src='/images/yellow.png' id='divOpacity0'/></div>
+<div class='visibilityHidden composited'><img src='/images/yellow.png' id='divVisibilityHidden'/></div>
+<div class='displayNone composited'><img src='/images/yellow.png' id='divDisplayNone'/></div>
diff --git a/testing/web-platform/tests/largest-contentful-paint/invisible-images.html b/testing/web-platform/tests/largest-contentful-paint/invisible-images.html
new file mode 100644
index 0000000000..997d70f777
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/invisible-images.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<title>Largest Contentful Paint: invisible images are not observable</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/invisible-images.js"></script>
+<style>
+ .opacity0 {
+ opacity: 0;
+ }
+ .visibilityHidden {
+ visibility: hidden;
+ }
+ .displayNone {
+ display: none;
+ }
+</style>
+<img src='/images/blue.png' class='opacity0' id='opacity0'/>
+<img src='/images/green.png' class='visibilityHidden' id='visibilityHidden'/>
+<img src='/images/red.png' class='displayNone' id='displayNone'/>
+<div class='opacity0'><img src='/images/yellow.png' id='divOpacity0'/></div>
+<div class='visibilityHidden'><img src='/images/yellow.png' id='divVisibilityHidden'/></div>
+<div class='displayNone'><img src='/images/yellow.png' id='divDisplayNone'/></div>
diff --git a/testing/web-platform/tests/largest-contentful-paint/larger-image.html b/testing/web-platform/tests/largest-contentful-paint/larger-image.html
new file mode 100644
index 0000000000..948f00d0c6
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/larger-image.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: largest image is reported.</title>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/largest-contentful-paint-helpers.js"></script>
+ <!-- There is some text and some images. We care about blue.png being reported, as it is the largest. -->
+ <p>This is some text! :)</p>
+ <img src='' id='red' />
+ <img src='' id='blue' />
+ <img src='' id='black' />
+ <p>More text!</p>
+ <script>
+ // Add listener for load event that is fired when image is loaded.
+ function image_load_promise(image_element) {
+ return new Promise(resolve => {
+ image_element.addEventListener('load', resolve);
+ });
+ }
+
+ promise_test(async (t) => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+
+ let promise = image_load_promise(document.getElementById('red'));
+ document.getElementById('red').src = '/images/red.png';
+ await promise;
+
+ const beforeLoad = performance.now();
+
+ promise = image_load_promise(document.getElementById('blue'));
+ document.getElementById('blue').src = '/images/blue.png';
+ await promise;
+
+ promise = image_load_promise(document.getElementById('black'));
+ document.getElementById('black').src = '/images/black-rectangle.png';
+ await promise;
+
+ const observer = new PerformanceObserver(
+ t.step_func(entryList => {
+ entryList.getEntries().forEach(entry => {
+ // The text or other image could be reported as LCP if it is rendered before the blue image.
+ if (entry.id !== 'blue')
+ return;
+
+ const url = window.location.origin + '/images/blue.png';
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'blue', size, beforeLoad);
+ t.done();
+ })
+ })
+ );
+ observer.observe({ type: 'largest-contentful-paint', buffered: true });
+ }, 'Largest Contentful Paint: largest image is reported.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/larger-text.html b/testing/web-platform/tests/largest-contentful-paint/larger-text.html
new file mode 100644
index 0000000000..8758c1c839
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/larger-text.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: largest text is reported.</title>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style type="text/css">
+ #text2 {
+ position: absolute;
+ width: auto;
+ white-space: nowrap;
+ }
+
+ </style>
+ <!-- These are some text and some tiny images. We care about the largest text. -->
+ <img id='green1' />
+ <div id='text1'></div>
+ <div id='text2'></div>
+ <img id='green2' />
+ <script>
+ const load_image = async (id, url) => {
+ await new Promise(resolve => {
+ const image = document.getElementById(id);
+ image.addEventListener('load', resolve);
+ image.src = url;
+ });
+ }
+
+ const load_text = (id, text) => {
+ let div = document.getElementById(id);
+ div.innerHTML = text;
+ }
+
+ promise_test(async (t) => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeRender = performance.now();
+
+ // Load images and add texts.
+ await load_image('green1', '/images/green-1x1.png');
+
+ load_text('text1', 'This is some text.');
+
+ load_text('text2', 'This is more text so it will be the Largest Contentful Paint!');
+
+ await load_image('green2', '/images/green-2x2.png');
+
+ await new Promise(resolve => {
+ new PerformanceObserver(
+ (entryList, observer) => {
+ entryList.getEntries().forEach(entry => {
+ // The tiny images or text1 could be reported as LCP if it is rendered before text2.
+ if (entry.id !== 'text2')
+ return;
+
+ assert_equals(entry.entryType, 'largest-contentful-paint',
+ 'The entry entryType should be largest-contentful-paint.');
+
+ assert_greater_than_equal(entry.renderTime, beforeRender,
+ 'The entry renderTime should be greater than or equal to the beforeRender.');
+
+ assert_greater_than_equal(performance.now(), entry.renderTime,
+ 'The performance.now() timestamp should be greater than or equal to the entry renderTime.');
+
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'The entry startTime should be equal to renderTime to the precision of 1 millisecond.');
+
+ assert_equals(entry.duration, 0, 'The entry duration should be 0.');
+
+ const div = document.getElementById('text2');
+
+ // The div styling makes it approximate the text size.
+ assert_greater_than_equal(entry.size, (div.clientHeight - 5) * (div.clientWidth - 5),
+ 'Reported LCP size should not be significantly smaller than the text2 div.');
+
+ assert_less_than_equal(entry.size, (div.clientHeight + 1) * (div.clientWidth + 1),
+ 'Reported LCP size should not be larger than the text2 div.');
+
+ assert_equals(entry.loadTime, 0, 'The entry loadTime should be 0.');
+
+ assert_equals(entry.url, '', 'The entry url should be empty.');
+
+ assert_equals(entry.element, div, 'The entry element should be test2 div.');
+
+ observer.disconnect();
+
+ resolve();
+ })
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+ }, 'Largest Contentful Paint: largest text is reported.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/loadTime-after-appendChild.html b/testing/web-platform/tests/largest-contentful-paint/loadTime-after-appendChild.html
new file mode 100644
index 0000000000..52d8f0663b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/loadTime-after-appendChild.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: delayed appended image.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeLoad;
+ const observer = new PerformanceObserver(
+ t.step_func_done(entryList => {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/black-rectangle.png';
+ // blue.png is 100 by 50.
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ const img = document.createElement('img');
+ img.src = '/images/black-rectangle.png';
+ img.id = 'image_id';
+ t.step_timeout(() => {
+ beforeLoad = performance.now();
+ document.getElementById('image_div').appendChild(img);
+ }, 200)
+ }, 'Image loadTime occurs after appendChild is called.');
+</script>
+<div id='image_div'></div>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-background.tentative.html b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-background.tentative.html
new file mode 100644
index 0000000000..16cbd0f0cb
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-background.tentative.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>LCP mouseover heuristics ignore background-based zoom widgets</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="resources/mouseover-utils.js"></script>
+</head>
+<body>
+ <img src="/images/green-16x16.png" id=image>
+ <span id=span style="display: inline-block;width: 15px; height: 15px"></span>
+ <script>
+ run_mouseover_test(/*background=*/true);
+ </script>
+</body>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-element.tentative.html b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-element.tentative.html
new file mode 100644
index 0000000000..bbd87235e8
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/mouseover-heuristics-element.tentative.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>LCP mouseover heuristics ignore element-based zoom widgets</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="resources/mouseover-utils.js"></script>
+</head>
+<body>
+ <img src="/images/green-16x16.png" id=image>
+ <span id=span style="display: inline-block;width: 15px; height: 15px"></span>
+ <script>
+ run_mouseover_test(/*background=*/false);
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/multiple-image-same-src.html b/testing/web-platform/tests/largest-contentful-paint/multiple-image-same-src.html
new file mode 100644
index 0000000000..192a7a1dec
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/multiple-image-same-src.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint:dynamically appended image with different
+ dimensions but same src triggers new entry.</title>
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/largest-contentful-paint-helpers.js"></script>
+ <img src='/images/black-rectangle.png' id='image_id' width="50" />
+ <script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeLoad = performance.now();
+ let beforeSecondImageLoad = performance.now();
+ let firstCallback = true;
+ // black-rectangle.png is 50 x 25.
+ const original_rect_size = 50 * 25;
+ // The bigger black rectangle is 100 x 50, but defined with width="50".
+ const bigger_rect_size = 100 * 50;
+ const observer = new PerformanceObserver(
+ t.step_func(function (entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/black-rectangle.png';
+ if (firstCallback) {
+ // Checks the original black rectangle.
+ // TODO(https://crbug.com/1411616): we're testing approximated values.
+ checkImage(entry, url, 'image_id', original_rect_size,
+ beforeLoad, ["approximateSize"]);
+ // Creates a new bigger black rectangle.
+ const img = document.createElement('img');
+ img.id = 'new_image_id';
+ img.src = url;
+ img.width = 100;
+ beforeSecondImageLoad = performance.now();
+ firstCallback = false;
+ document.body.appendChild(img);
+ } else {
+ // Checks the new black rectangle.
+ checkImage(entry, url, 'new_image_id', bigger_rect_size, beforeSecondImageLoad);
+ t.done();
+ }
+ })
+ );
+ observer.observe({ type: 'largest-contentful-paint', buffered: true });
+ }, 'Largest Contentful Paint:dynamically appended image with different ' +
+ 'dimensions but same src triggers new entry.');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/multiple-redirects-TAO.html b/testing/web-platform/tests/largest-contentful-paint/multiple-redirects-TAO.html
new file mode 100644
index 0000000000..b9745176bd
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/multiple-redirects-TAO.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates some Timing-Allow-Origin header usage in multiple redirects.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+<img id='image'></img>
+<body>
+<script>
+setup({"hide_test_state": true});
+async_test(t => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let destUrl = get_host_info().HTTP_REMOTE_ORIGIN
+ + '/element-timing/resources/multiple-redirects.py?';
+ destUrl += 'redirect_count=2';
+ // The final resource has '*' in TAO header, so will not affect the result.
+ destUrl += '&final_resource=/element-timing/resources/circle-tao.svg';
+ destUrl += '&origin1=' + get_host_info().UNAUTHENTICATED_ORIGIN;
+ destUrl += '&origin2=' + get_host_info().HTTP_REMOTE_ORIGIN;
+ const taoCombinations = [
+ {tao1: location.origin, tao2: location.origin, passes: false},
+ {tao1: location.origin, tao2: get_host_info().HTTP_REMOTE_ORIGIN, passes: false},
+ {tao1: location.origin, tao2: 'null', passes: true},
+ {tao1: location.origin, tao2: '*', passes: true},
+ {tao1: location.origin, tao2: location.origin, passes: false},
+ {tao1: 'null', tao2: '*', passes: false},
+ {tao1: '*', tao2: 'null', passes: true},
+ ];
+ const sizes = [28*28, 33*33, 40*40, 50*50, 66*66, 100*100, 200*200];
+ function getURL(item) {
+ return destUrl + '&tao1=' + item.tao1 + '&tao2=' + item.tao2;
+ }
+ function setImage(index) {
+ const image = document.getElementById('image');
+ const item = taoCombinations[index];
+ image.src = getURL(item);
+ // Use monotonic sizes to get all the images!
+ image.width = 200 / (7 - index);
+ }
+ let observedCount = 0;
+ let beforeLoad = performance.now();
+ new PerformanceObserver(t.step_func(entries => {
+ assert_equals(entries.getEntries().length, 1, 'There should be a single entry.');
+ const e = entries.getEntries()[0];
+ const item = taoCombinations[observedCount];
+ const url = getURL(item);
+ const options = item.passes ? [] : ['renderTimeIs0'];
+ checkImage(e, url, 'image', sizes[observedCount], beforeLoad, options);
+ observedCount++;
+ if (observedCount === taoCombinations.length) {
+ t.done();
+ } else {
+ beforeLoad = performance.now();
+ setImage(observedCount);
+ }
+ })).observe({entryTypes: ['largest-contentful-paint']});
+ setImage(0);
+}, 'Cross-origin images with passing/failing TAO should/shouldn\'t have its renderTime set.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-after-fcp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-after-fcp.tentative.html
new file mode 100644
index 0000000000..06b065be3a
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-after-fcp.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Non-Tao Image Load and Render After FCP.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+ <script>
+ promise_test(async t => {
+ // Add text so that FCP is set and image rendering time would be larger than FCP.
+ add_text('Add to set FCP');
+
+ // wait for 1 animation frame so that FCP is set.
+ await raf();
+
+ const non_tao_image_url = get_host_info().OTHER_ORIGIN + '/images/blue.png';
+
+ await loadImage(non_tao_image_url);
+
+ lcp = await getLCPStartTime('blue.png');
+
+ fcp = getFCPStartTime();
+
+ checkLCPEntryForNonTaoImages({'lcp':lcp, 'fcp':fcp});
+ }, 'Non-Tao Image Load and Render After FCP.')
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-after.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-after.tentative.html
new file mode 100644
index 0000000000..57f29c3535
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-after.tentative.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Non-Tao Image Load Before FCP and Render After FCP.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+ <script>
+ promise_test(async t => {
+ const non_tao_image_url = get_host_info().OTHER_ORIGIN + '/images/blue.png';
+
+ img = await loadImage(non_tao_image_url, true);
+
+ // Add text so that FCP is set and image rendering time would be larger than FCP.
+ add_text('Add to set FCP');
+
+ // wait for 1 animation frame so that FCP is set.
+ await raf();
+
+ // Pass empty string to select the LCP entry corresponding to the text as LCP.url is
+ // empty for text elements.
+ lcp = await getLCPStartTime('');
+
+ img.style.opacity = 1;
+
+ lcp = await getLCPStartTime('blue.png');
+
+ fcp = getFCPStartTime();
+
+ checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp });
+ }, 'Non-Tao Image Load Before FCP and Render After FCP.')
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-at-fcp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-at-fcp.tentative.html
new file mode 100644
index 0000000000..b209b50c8b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-load-before-fcp-render-at-fcp.tentative.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Non-Tao Image Load Before LCP and Render at the Same Time of FCP.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+ <script>
+ promise_test(async t => {
+ const non_tao_image_url = get_host_info().OTHER_ORIGIN + '/images/blue.png';
+
+ await loadImage(non_tao_image_url);
+
+ lcp = await getLCPStartTime('blue.png');
+
+ fcp = getFCPStartTime();
+
+ checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp });
+ }, 'Non-Tao Image Load Before LCP and Render at the Same Time of FCP.')
+ </script>
+ <!-- <img src='{{location[scheme]}}://{{hosts[alt][www]}}:{{ports[http][0]}}/images/blue.png'> -->
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/non-tao-image-subsequent-lcp-candidate.tentative.html b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-subsequent-lcp-candidate.tentative.html
new file mode 100644
index 0000000000..50f9a229ea
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/non-tao-image-subsequent-lcp-candidate.tentative.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Non-Tao Image Subsequent LCP candidates.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+ <script>
+ promise_test(async t => {
+ let small_image_path = '/images/green-100x50.png';
+ let big_image_path = '/images/green-256x256.png';
+ let non_tao_scheme = get_host_info().OTHER_ORIGIN;
+
+ // Load non-tao image with 0 opacity so it won't trigger an LCP entry.
+ big_image = await loadImage(non_tao_scheme + big_image_path, true);
+
+ // Load a smaller non-tao image with 0 opacity.
+ small_image = await loadImage(non_tao_scheme + small_image_path, true);
+
+ // Add text so that FCP is set.
+ add_text('text');
+
+ // wait for 1 animation frame so that FCP is set.
+ await raf();
+
+ // The url of text LCP element is empty.
+ lcp = await getLCPStartTime('');
+
+ // Rendered loaded image after FCP.
+ small_image.style.opacity = 1;
+ lcp = await getLCPStartTime(small_image_path);
+
+ fcp = getFCPStartTime();
+
+ checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp });
+
+ // This is to verify subsequent LCP candidates also have the start time set correctly.
+ big_image.style.opacity = 1;
+
+ lcp = await getLCPStartTime(big_image_path);
+
+ checkLCPEntryForNonTaoImages({ 'lcp': lcp, 'fcp': fcp });
+
+ }, 'Non-Tao Image Subsequent LCP candidates.')
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-after-untrusted-scroll.html b/testing/web-platform/tests/largest-contentful-paint/observe-after-untrusted-scroll.html
new file mode 100644
index 0000000000..c84f922e5e
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-after-untrusted-scroll.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe image.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+
+ const url = window.location.origin + '/images/blue.png';
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Same-origin image after a JS initiated scroll event is observable.');
+ document.body.dispatchEvent(new Event('scroll'));
+ const image = new Image();
+ image.id = 'image_id';
+ image.src = '/images/blue.png';
+ document.body.appendChild(image);
+</script>
+
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-text.html b/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-text.html
new file mode 100644
index 0000000000..21ae68585b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-css-generated-text.html
@@ -0,0 +1,88 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #css-generated-text::before {
+ content: 'This is some text generated via css';
+ font-size: 12px;
+ }
+
+ #css-generated-text-attr::before {
+ content: attr(data-text);
+ font-size: 12px;
+ }
+
+ #css-generated-text-inline-elem::before {
+ content: 'This is some more text generated via css that should be displayed via a span tag';
+ font-size: 12px;
+ }
+</style>
+<body>
+ <script>
+ setup({"hide_test_state": true});
+ const checkText = (entry, expectedSize, expectedID, beforeRender) => {
+ assert_equals(entry.entryType, 'largest-contentful-paint',
+ 'Entry should be of type largest-contentful-paint');
+ assert_greater_than_equal(entry.renderTime, beforeRender,
+ 'Render time should be greater than time just before rendering');
+ assert_greater_than_equal(performance.now(), entry.renderTime,
+ 'renderTime should be less than current time');
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ assert_equals(entry.duration, 0, 'duration should be 0');
+ assert_greater_than_equal(entry.size, expectedSize,
+ 'Size should match expected size');
+ assert_equals(entry.loadTime, 0, 'loadTime should be zero');
+ assert_equals(entry.id, expectedID, 'ID should match expected ID');
+ assert_equals(entry.url, '', 'URL should be empty');
+ assert_equals(entry.element, document.getElementById(expectedID),
+ 'Entry element is expected element');
+ }
+
+ const runTest = (element, testName) => {
+ const elementId = element.id;
+ // The element should be atleast 12px in width
+ // and 100px across based on font size and text length.
+ const elemSizeLowerBound = 1200;
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ assert_implements(window.LargestContentfulPaint,
+ "LargestContentfulPaint is not implemented");
+ const observer = new PerformanceObserver(resolve);
+ observer.observe({ type: 'largest-contentful-paint' });
+ beforeRender = performance.now();
+ document.body.appendChild(element);
+
+ step_timeout(() => {
+ reject(new Error('timeout, LCP candidate not detected'));
+ }, 1000)
+ }).then(entryList => {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ checkText(entry, elemSizeLowerBound, elementId, beforeRender);
+ });
+ }, testName);
+ }
+
+ const cssGeneratedTextElem = document.createElement('p');
+ cssGeneratedTextElem.id = 'css-generated-text';
+ runTest(cssGeneratedTextElem,
+ "CSS generated text is observable as a LargestContentfulPaint candidate");
+
+ const cssGeneratedTextAttrElem = document.createElement('p');
+ cssGeneratedTextAttrElem.id = 'css-generated-text-attr';
+ cssGeneratedTextAttrElem.setAttribute('data-text',
+ 'This is some text generated using content:attr() via css');
+ runTest(cssGeneratedTextAttrElem,
+ "Text generated with CSS using content:attr() is observable as a LargestContentfulPaint candidate");
+
+ const cssGeneratedTextAttrInlineElemBlockWrapper = document.createElement('div');
+ cssGeneratedTextAttrInlineElemBlockWrapper.id = 'css-generated-text-inline-elem-block-wrapper';
+ const cssGeneratedTextInlineElem = document.createElement('span');
+ cssGeneratedTextInlineElem.id = 'css-generated-text-inline-elem';
+ cssGeneratedTextAttrInlineElemBlockWrapper.appendChild(cssGeneratedTextInlineElem);
+ runTest(cssGeneratedTextAttrInlineElemBlockWrapper,
+ "CSS generated text on a inline element is observable as a LargestContentfulPaint candidate");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-image.html
new file mode 100644
index 0000000000..707840671b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-image.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe image.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/blue.png';
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Same-origin image is observable.');
+</script>
+
+<img src='/images/blue.png' id='image_id'/>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-background-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-background-image.html
new file mode 100644
index 0000000000..bc0b399a18
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-background-image.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe image.</title>
+<style>
+ #target {
+ background-image: url('/images/green.svg');
+ width: 100px;
+ height: 50px;
+ }
+</style>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ let url = window.location.origin + '/images/green.svg';
+ // green.svg is 100 by 50
+ const size = 100 * 50;
+ checkImage(entry, url, 'target', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ t.step_timeout(() => {
+ assert_unreached("The image should have triggered an LCP entry.");
+ t.done();
+ }, 1000);
+ }, 'Same-origin SVG background image is observable.');
+</script>
+
+<div id="target" width="100" height="50"></div>
+</body>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-background-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-background-image.html
new file mode 100644
index 0000000000..53fd1b7273
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-background-image.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe image.</title>
+<style>
+ #target {
+ background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><rect fill="lime" width="100" height="50"/></svg>');
+ width: 100px;
+ height: 50px;
+ }
+</style>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ let url = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"';
+ url += ' width="100" height="50"><rect fill="lime" width="100"';
+ url += ' height="50"/></svg>';
+ // green.svg is 100 by 50
+ const size = 100 * 50;
+ checkImage(entry, url, 'target', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ t.step_timeout(() => {
+ assert_unreached("The image should have triggered an LCP entry.");
+ t.done();
+ }, 1000);
+ }, 'Data-URI background SVG image is observable.');
+</script>
+
+<div id="target" width="100" height="50"></div>
+</body>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-image.html
new file mode 100644
index 0000000000..00ea314e14
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-data-uri-image.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe image.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ let url = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"';
+ url += ' width="100" height="50"><rect fill="lime" width="100"';
+ url += ' height="50"/></svg>';
+ // green.svg is 100 by 50
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Same-origin image is observable.');
+</script>
+
+<img src='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><rect fill="lime" width="100" height="50"/></svg>' id='image_id'/>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-svg-image.html b/testing/web-platform/tests/largest-contentful-paint/observe-svg-image.html
new file mode 100644
index 0000000000..3a6e0f6f23
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-svg-image.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe SVG image.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/green.svg';
+ // green.svg is 100 by 50
+ const size = 100 * 50;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Same-origin image is observable.');
+</script>
+
+<img src='/images/green.svg' id='image_id'/>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/observe-text.html b/testing/web-platform/tests/largest-contentful-paint/observe-text.html
new file mode 100644
index 0000000000..5d0244b7e3
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/observe-text.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe text.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+p {
+ font-size: 12px;
+}
+</style>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeRender;
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ assert_equals(entry.entryType, 'largest-contentful-paint');
+ assert_greater_than_equal(entry.renderTime, beforeRender);
+ assert_greater_than_equal(performance.now(), entry.renderTime);
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ assert_equals(entry.duration, 0);
+ // Some lower bound: height of at least 12 px.
+ // Width of at least 100 px.
+ // TODO: find a good way to bound text width.
+ assert_greater_than_equal(entry.size, 1200);
+ assert_equals(entry.loadTime, 0);
+ assert_equals(entry.id, 'my_text');
+ assert_equals(entry.url, '');
+ assert_equals(entry.element, document.getElementById('my_text'));
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ beforeRender = performance.now();
+ }, 'Text element is observable as a LargestContentfulPaint candidate.');
+</script>
+
+<p id='my_text'>This is important text! :)</p>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html b/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html
new file mode 100644
index 0000000000..6a2ce5c7c6
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/placeholder-image.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: src change triggers new entry.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<img src='/images/green-1x1.png' id='image_id' width="133" height="106"/>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeLoad = performance.now();
+ document.getElementById('image_id').src = '/images/blue.png';
+ const url = window.location.origin + '/images/blue.png';
+ const observer = new PerformanceObserver(
+ t.step_func(function(entryList) {
+ let entries = entryList.getEntries().filter(e => e.url === url);
+ if (entries.length === 0)
+ return;
+ assert_equals(entries.length, 1);
+ const entry = entries[0];
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'image_id', size, beforeLoad);
+ t.done();
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Largest Contentful Paint: changing src causes a new entry to be dispatched.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/redirects-tao-star.html b/testing/web-platform/tests/largest-contentful-paint/redirects-tao-star.html
new file mode 100644
index 0000000000..5607ed792e
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/redirects-tao-star.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates LargestContentfulPaint information for cross-origin redirect chain images.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script src=/common/get-host-info.sub.js></script>
+</head>
+<body>
+<script>
+setup({"hide_test_state": true});
+async_test(t => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let destUrl = get_host_info().HTTP_REMOTE_ORIGIN
+ + '/resource-timing/resources/multi_redirect.py?';
+ destUrl += 'page_origin=' + get_host_info().HTTP_ORIGIN;
+ destUrl += '&cross_origin=' + get_host_info().HTTP_REMOTE_ORIGIN;
+ destUrl += '&final_resource=/element-timing/resources/circle-tao.svg';
+ destUrl += '&tao_steps=';
+ const sizes = [50*50, 66*66, 100*100, 200*200];
+
+ const image = document.createElement('img');
+ image.src = destUrl + '0';
+ image.setAttribute('id', 'id');
+ image.width = 200 / 4;
+ document.body.appendChild(image);
+
+ let numObserved = 0;
+ let beforeLoad = performance.now();
+ new PerformanceObserver(t.step_func(entries => {
+ assert_equals(entries.getEntries().length, 1);
+ const entry = entries.getEntries()[0];
+ const options = numObserved === 3 ? [] : ['renderTimeIs0'];
+ checkImage(entry, destUrl + numObserved, 'id', sizes[numObserved], beforeLoad, options);
+ numObserved++;
+ if (numObserved === 4)
+ t.done();
+ else {
+ // Change the image to trigger a new LCP entry.
+ const img = document.getElementById('id');
+ image.src = destUrl + numObserved;
+ // Use monotonically increasing image sizes to trigger LCP every time.
+ image.width = 200 / (4 - numObserved);
+ beforeLoad = performance.now();
+ }
+ })).observe({type: 'largest-contentful-paint'});
+}, 'Cross-origin image without TAO should not have its renderTime set, with full TAO it should.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/repeated-image.html b/testing/web-platform/tests/largest-contentful-paint/repeated-image.html
new file mode 100644
index 0000000000..c69cc5b615
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/repeated-image.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: repeated image.</title>
+<style>
+ #image_id {
+ width: 10px;
+ height: 10px;
+ }
+</style>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+ setup({"hide_test_state": true});
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeLoad = performance.now();
+ let firstCallback = true;
+ const url = window.location.origin + '/images/black-rectangle.png';
+ const observer = new PerformanceObserver(
+ t.step_func(entryList => {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+
+ // First image is shrunk to be 10 x 10. The second image is added at its natural size: 100 x 50.
+ const size = firstCallback ? 10 * 10 : 100 * 50;
+ const id = firstCallback ? 'image_id' : 'second_id';
+ checkImage(entry, url, id, size, beforeLoad);
+ if (firstCallback) {
+ const img = document.createElement('img');
+ img.src = '/images/black-rectangle.png';
+ img.id = 'second_id';
+ beforeLoad = performance.now();
+ document.getElementById('image_div').appendChild(img);
+ firstCallback = false;
+ return;
+ } else {
+ t.done();
+ }
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Repeated image produces different timestamps.');
+</script>
+<img src='/images/black-rectangle.png' id='image_id'/>
+<div id='image_div'></div>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html b/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html
new file mode 100644
index 0000000000..cd60025480
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/iframe-stores-entry.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Text</p>
+<script>
+ const observer = new PerformanceObserver(entryList => {
+ window.parent.triggerTest(entryList.getEntries()[0]);
+ });
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js b/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js
new file mode 100644
index 0000000000..bad078e35f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/invisible-images.js
@@ -0,0 +1,22 @@
+async_test(t => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const observer = new PerformanceObserver(
+ t.step_func(entryList => {
+ entryList.getEntries().forEach(entry => {
+ // May receive a text entry. Ignore that entry.
+ if (!entry.url) {
+ return;
+ }
+ // The images should not have caused an entry, so fail test.
+ assert_unreached('Should not have received an entry! Received one with id '
+ + entryList.getEntries()[0].id);
+ });
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ // Images have been added but should not cause entries to be dispatched.
+ // Wait for 500ms and end test, ensuring no entry was created.
+ t.step_timeout(() => {
+ t.done();
+ }, 500);
+}, 'Images with opacity: 0, visibility: hidden, or display: none are not observable by LargestContentfulPaint.');
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
new file mode 100644
index 0000000000..3ac3705e8e
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
@@ -0,0 +1,173 @@
+const image_delay = 1000;
+const delay_pipe_value = image_delay / 1000;
+
+const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => {
+ let timeout_id;
+ const timeout = new Promise((_, reject) => {
+ timeout_id = step_timeout(() =>
+ reject(new DOMException(message, "TimeoutError")), delay)
+ });
+ let result = null;
+ try {
+ result = await Promise.race([promise, timeout]);
+ clearTimeout(timeout_id);
+ } finally {
+ cleanup();
+ }
+ return result;
+};
+
+// Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values.
+// The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry.
+// The |options| parameter may contain some string values specifying the following:
+// * 'renderTimeIs0': the renderTime should be 0 (image does not pass Timing-Allow-Origin checks).
+// When not present, the renderTime should not be 0 (image passes the checks).
+// * 'sizeLowerBound': the |expectedSize| is only a lower bound on the size attribute value.
+// When not present, |expectedSize| must be exactly equal to the size attribute value.
+// * 'approximateSize': the |expectedSize| is only approximate to the size attribute value.
+// This option is mutually exclusive to 'sizeLowerBound'.
+function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound, options = []) {
+ assert_equals(entry.name, '', "Entry name should be the empty string");
+ assert_equals(entry.entryType, 'largest-contentful-paint',
+ "Entry type should be largest-contentful-paint");
+ assert_equals(entry.duration, 0, "Entry duration should be 0");
+ // The entry's url can be truncated.
+ assert_equals(expectedUrl.substr(0, 100), entry.url.substr(0, 100),
+ `Expected URL ${expectedUrl} should at least start with the entry's URL ${entry.url}`);
+ assert_equals(entry.id, expectedID, "Entry ID matches expected one");
+ assert_equals(entry.element, document.getElementById(expectedID),
+ "Entry element is expected one");
+ if (options.includes('skip')) {
+ return;
+ }
+ if (options.includes('renderTimeIs0')) {
+ assert_equals(entry.renderTime, 0, 'renderTime should be 0');
+ assert_between_exclusive(entry.loadTime, timeLowerBound, performance.now(),
+ 'loadTime should be between the lower bound and the current time');
+ assert_approx_equals(entry.startTime, entry.loadTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ } else {
+ assert_between_exclusive(entry.loadTime, timeLowerBound, entry.renderTime,
+ 'loadTime should occur between the lower bound and the renderTime');
+ assert_greater_than_equal(performance.now(), entry.renderTime,
+ 'renderTime should occur before the entry is dispatched to the observer.');
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ }
+ if (options.includes('sizeLowerBound')) {
+ assert_greater_than(entry.size, expectedSize);
+ } else if (options.includes('approximateSize')) {
+ assert_approx_equals(entry.size, expectedSize, 1);
+ } else{
+ assert_equals(entry.size, expectedSize);
+ }
+
+ if (options.includes('animated')) {
+ assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime,
+ 'firstAnimatedFrameTime should be smaller than loadTime');
+ assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime,
+ 'firstAnimatedFrameTime should be smaller than renderTime');
+ assert_less_than(entry.firstAnimatedFrameTime, image_delay,
+ 'firstAnimatedFrameTime should be smaller than the delay applied to the second frame');
+ assert_greater_than(entry.firstAnimatedFrameTime, 0,
+ 'firstAnimatedFrameTime should be larger than 0');
+ }
+ if (options.includes('animated-zero')) {
+ assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0');
+ }
+}
+
+const load_and_observe = url => {
+ return new Promise(resolve => {
+ (new PerformanceObserver(entryList => {
+ for (let entry of entryList.getEntries()) {
+ if (entry.url == url) {
+ resolve(entryList.getEntries()[0]);
+ }
+ }
+ })).observe({ type: 'largest-contentful-paint', buffered: true });
+ const img = new Image();
+ img.id = 'image_id';
+ img.src = url;
+ document.body.appendChild(img);
+ });
+};
+
+const load_video_and_observe = url => {
+ return new Promise(resolve => {
+ (new PerformanceObserver(entryList => {
+ for (let entry of entryList.getEntries()) {
+ if (entry.url == url) {
+ resolve(entryList.getEntries()[0]);
+ }
+ }
+ })).observe({ type: 'largest-contentful-paint', buffered: true });
+ const video = document.createElement("video");
+ video.id = 'video_id';
+ video.src = url;
+ video.autoplay = true;
+ video.muted = true;
+ video.loop = true;
+ document.body.appendChild(video);
+ });
+};
+
+const getLCPStartTime = (identifier) => {
+ return new Promise(resolve => {
+ new PerformanceObserver((entryList, observer) => {
+ entryList.getEntries().forEach(e => {
+ if (e.url.includes(identifier)) {
+ resolve(e);
+ observer.disconnect();
+ }
+ });
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+}
+
+const getFCPStartTime = () => {
+ return performance.getEntriesByName('first-contentful-paint')[0];
+}
+
+const add_text = (text) => {
+ const paragraph = document.createElement('p');
+ paragraph.innerHTML = text;
+ document.body.appendChild(paragraph);
+}
+
+const loadImage = (url, shouldBeIgnoredForLCP = false) => {
+ return new Promise(function (resolve, reject) {
+ let image = document.createElement('img');
+ image.addEventListener('load', () => { resolve(image); });
+ image.addEventListener('error', reject);
+ image.src = url;
+ if (shouldBeIgnoredForLCP)
+ image.style.opacity = 0;
+ document.body.appendChild(image);
+ });
+}
+
+const checkLCPEntryForNonTaoImages = (times = {}) => {
+ const lcp = times['lcp'];
+ const fcp = times['fcp'];
+ const lcp_url_components = lcp.url.split('/');
+
+ if (lcp.loadTime <= fcp.startTime) {
+ assert_approx_equals(lcp.startTime, fcp.startTime, 0.001,
+ 'LCP start time should be the same as FCP for ' +
+ lcp_url_components[lcp_url_components.length - 1]) +
+ ' when LCP load time is less than FCP.';
+ } else {
+ assert_approx_equals(lcp.startTime, lcp.loadTime, 0.001,
+ 'LCP start time should be the same as LCP load time for ' +
+ lcp_url_components[lcp_url_components.length - 1]) +
+ ' when LCP load time is no less than FCP.';
+ }
+
+ assert_equals(lcp.renderTime, 0,
+ 'The LCP render time of Non-Tao image should always be 0.');
+}
+
+const raf = () => {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js
new file mode 100644
index 0000000000..c650a0b747
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw-from-cache.js
@@ -0,0 +1,8 @@
+self.addEventListener("fetch", e => {
+ if (e.request.url.endsWith('green.svg')) {
+ e.respondWith(new Response(`<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50">
+ <rect fill="lime" width="100" height="50"/>
+ </svg>
+ `, { headers: { 'Content-Type': 'image/svg+xml' } }));
+ }
+});
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html
new file mode 100644
index 0000000000..069a50eae9
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/lcp-sw.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+
+<body>
+ <script>
+ new PerformanceObserver(entries => {
+ window.parent.postMessage(entries.getEntries()[0].toJSON());
+ }).observe({ entryTypes: ["largest-contentful-paint"] });
+
+ const image = document.createElement("img");
+ image.src = "/images/green.svg";
+ image.id = "theImage";
+ document.body.appendChild(image);
+ </script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js b/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js
new file mode 100644
index 0000000000..1836f2e4ad
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/mouseover-utils.js
@@ -0,0 +1,128 @@
+let counter = 0;
+const loadImage = size => {
+ return event => {
+ let zoom;
+ if (location.search.includes("replace")) {
+ zoom = document.getElementById("image");
+ } else {
+ zoom = new Image();
+ }
+ zoom.src=`/images/green-${size}.png`;
+ ++counter;
+ zoom.elementTiming = "zoom" + counter;
+ document.body.appendChild(zoom);
+ }
+};
+const loadBackgroundImage = size => {
+ return event => {
+ const div = document.createElement("div");
+ const [width, height] = size.split("x");
+ ++counter;
+ div.style = `background-image:
+ url(/images/green-${size}.png?${counter}); width: ${width}px; height: ${height}px`;
+ div.elementTiming = "zoom" + counter;
+ document.body.appendChild(div);
+ }
+};
+
+const registerMouseover = background => {
+ const image = document.getElementById("image");
+ const span = document.getElementById("span");
+ const func = background ? loadBackgroundImage : loadImage;
+ image.addEventListener("mouseover", func("100x50"));
+ span.addEventListener("mouseover", func("256x256"));
+}
+
+const dispatch_mouseover = () => {
+ span.dispatchEvent(new Event("mouseover"))
+};
+
+const wait_for_lcp_entries = async entries_expected => {
+ await new Promise(resolve => {
+ let entries_seen = 0;
+ const PO = new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (let entry of entries) {
+ if (entry.url) {
+ entries_seen++;
+ }
+ }
+ if (entries_seen == entries_expected) {
+ PO.disconnect();
+ resolve()
+ } else if (entries_seen > entries_expected) {
+ PO.disconnect();
+ reject();
+ }
+ });
+ PO.observe({type: "largest-contentful-paint", buffered: true});
+ });
+};
+const wait_for_element_timing_entry = async identifier => {
+ await new Promise(resolve => {
+ const PO = new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (let entry of entries) {
+ if (entry.identifier == identifier) {
+ PO.disconnect();
+ resolve()
+ }
+ }
+ });
+ PO.observe({type: "element", buffered: true});
+ });
+};
+const wait_for_resource_timing_entry = async name => {
+ await new Promise(resolve => {
+ const PO = new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (let entry of entries) {
+ if (entry.name.includes(name)) {
+ PO.disconnect();
+ resolve()
+ }
+ }
+ });
+ PO.observe({type: "resource", buffered: true});
+ });
+};
+
+const run_mouseover_test = background => {
+ promise_test(async t => {
+ // await the first LCP entry
+ await wait_for_lcp_entries(1);
+ // Hover over the image
+ registerMouseover(background);
+ if (test_driver) {
+ await new test_driver.Actions().pointerMove(0, 0, {origin: image}).send();
+ }
+ if (!background) {
+ await wait_for_element_timing_entry("zoom1");
+ } else {
+ await wait_for_resource_timing_entry("png?1");
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ // There's only a single LCP entry, because the zoom was skipped.
+ await wait_for_lcp_entries(1);
+
+ // Wait 600 ms as the heuristic is 500 ms.
+ // This will no longer be necessary once the heuristic relies on Task
+ // Attribution.
+ await new Promise(r => step_timeout(r, 600));
+
+ // Hover over the span.
+ if (test_driver) {
+ await new test_driver.Actions().pointerMove(0, 0, {origin: span}).send();
+ }
+ if (!background) {
+ await wait_for_element_timing_entry("zoom2");
+ } else {
+ await wait_for_resource_timing_entry("png?2");
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ // There are 2 LCP entries, as the image loaded due to span hover is a
+ // valid LCP candidate.
+ await wait_for_lcp_entries(2);
+ }, `LCP mouseover heuristics ignore ${background ?
+ "background" : "element"}-based zoom widgets`);
+}
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py b/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py
new file mode 100644
index 0000000000..780d5736c4
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/slow-style-change.py
@@ -0,0 +1,9 @@
+import time
+
+def main(request, response):
+ time.sleep(1)
+ return [ ("Content-Type", "text/css")], """
+ #text {
+ font-size: 4em;
+ }
+ """
diff --git a/testing/web-platform/tests/largest-contentful-paint/same-origin-redirects.html b/testing/web-platform/tests/largest-contentful-paint/same-origin-redirects.html
new file mode 100644
index 0000000000..b5cf9da2d1
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/same-origin-redirects.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates LargestContentfulPaint for same-origin redirect chain.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+<script>
+async_test(t => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ // First redirect
+ let destUrl = '/common/redirect.py?location='
+ // Second redirect
+ destUrl += '/common/redirect.py?location='
+ // Image without TAO headers.
+ destUrl += '/element-timing/resources/square20.png';
+ let beforeLoad;
+ new PerformanceObserver(t.step_func_done(entries => {
+ assert_equals(entries.getEntries().length, 1, 'There should be one entry');
+ const entry = entries.getEntries()[0];
+ checkImage(entry, location.origin + destUrl, 'id', 20*20, beforeLoad);
+ })).observe({entryTypes: ['largest-contentful-paint']});
+ const image = document.createElement('img');
+ image.src = destUrl;
+ image.setAttribute('id', 'id')
+ document.body.appendChild(image);
+ beforeLoad = performance.now();
+}, 'Same-origin image redirect without TAO should have its renderTime set.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/largest-contentful-paint/supported-lcp-type.html b/testing/web-platform/tests/largest-contentful-paint/supported-lcp-type.html
new file mode 100644
index 0000000000..25d4eaa036
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/supported-lcp-type.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<head>
+<title>PerformanceObserver.supportedEntryTypes contains "largest-contentful-paint"</title>
+</head>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ if (typeof PerformanceObserver.supportedEntryTypes === "undefined")
+ assert_unreached("supportedEntryTypes is not supported.");
+ assert_greater_than(PerformanceObserver.supportedEntryTypes.indexOf("largest-contentful-paint"), -1,
+ "There should be an entry 'largest-contentful-paint' in PerformanceObserver.supportedEntryTypes");
+}, "supportedEntryTypes contains 'largest-contentful-paint'.");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/text-with-display-style.html b/testing/web-platform/tests/largest-contentful-paint/text-with-display-style.html
new file mode 100644
index 0000000000..69edc168c5
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/text-with-display-style.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe text with display style.</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#title {
+ display: flex;
+}
+</style>
+<h1 id='title'>I am a title!</h1>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let beforeRender;
+ /* In this test, we first observe a header with style 'display: flex'.
+ * Once observed, we remove it and add a header with style 'display: grid'.
+ * And once that is observed, we remove it and add a header with style 'display: block'.
+ * At each step, we check the values of the entries received.
+ */
+ let observedFlex = false;
+ let observedGrid = false;
+ const observer = new PerformanceObserver(
+ t.step_func(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ assert_equals(entry.entryType, 'largest-contentful-paint');
+ assert_greater_than_equal(entry.renderTime, beforeRender);
+ assert_greater_than_equal(performance.now(), entry.renderTime);
+ assert_approx_equals(entry.startTime, entry.renderTime, 0.001,
+ 'startTime should be equal to renderTime to the precision of 1 millisecond.');
+ assert_equals(entry.duration, 0);
+ // TODO: find a good way to bound text size.
+ assert_greater_than_equal(entry.size, 500);
+ assert_equals(entry.url, '');
+ assert_equals(entry.loadTime, 0);
+ if (!observedFlex) {
+ observedFlex = true;
+ assert_equals(entry.id, 'title');
+ const title = document.getElementById('title');
+ assert_equals(entry.element, title);
+ // Remove 'display: flex' and add 'display: grid' text.
+ title.parentNode.removeChild(title);
+ const title2 = document.createElement('h1');
+ title2.id = 'title2';
+ title2.style = 'display: grid';
+ title2.innerHTML = 'I am a second title!';
+ document.body.appendChild(title2);
+ beforeRender = performance.now();
+ } else if (!observedGrid) {
+ observedGrid = true;
+ assert_equals(entry.id, 'title2');
+ const title2 = document.getElementById('title2');
+ assert_equals(entry.element, title2);
+ // Remove 'display: grid' and add 'display: block' text.
+ title2.parentNode.removeChild(title2);
+ const title3 = document.createElement('h1');
+ title3.id = 'title3';
+ title3.style = 'display: block';
+ title3.innerHTML = 'I am the third and last title!';
+ document.body.appendChild(title3);
+ beforeRender = performance.now();
+ } else {
+ assert_equals(entry.id, 'title3');
+ const title3 = document.getElementById('title3');
+ assert_equals(entry.element, title3);
+ t.done();
+ }
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ beforeRender = performance.now();
+ }, 'Text with display style is observable.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/toJSON.html b/testing/web-platform/tests/largest-contentful-paint/toJSON.html
new file mode 100644
index 0000000000..5ea84eeb2b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/toJSON.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: toJSON</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>Text!</p>
+<script>
+ async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ const entry = entryList.getEntries()[0];
+ assert_equals(typeof(entry.toJSON), 'function');
+ const json = entry.toJSON();
+ assert_equals(typeof(json), 'object');
+ const keys = [
+ // PerformanceEntry
+ 'name',
+ 'entryType',
+ 'startTime',
+ 'duration',
+ // LargestContentfulPaint
+ 'renderTime',
+ 'loadTime',
+ 'size',
+ 'id',
+ 'url',
+ ];
+ for (const key of keys) {
+ assert_equals(json[key], entry[key],
+ 'LargestContentfulPaint ${key} entry does not match its toJSON value');
+ }
+ assert_equals(json['element'], undefined, 'toJSON should not include element');
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+ }, 'Test toJSON() in LargestContentfulPaint.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/update-on-style-change.tentative.html b/testing/web-platform/tests/largest-contentful-paint/update-on-style-change.tentative.html
new file mode 100644
index 0000000000..dd34e61b0f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/update-on-style-change.tentative.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>LargestContentfulPaint entries should generate for updates to previous LargestContentfulPaint nodes.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <script>
+ promise_test(() => {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ let countLcp = 0;
+ let firstLcp = null;
+ const timeoutPromise = new Promise(resolve => step_timeout(() => {
+ resolve(new Error('Did not observe two LCP entries'))
+ }, 3 * 1000));
+ const testPromise = new Promise(resolve => {
+ new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ for (const entry of entries) {
+ ++countLcp;
+ assert_equals(entry.entryType, 'largest-contentful-paint');
+ assert_equals(entry.id, 'text');
+ if (countLcp == 1) {
+ firstLcp = entry;
+ } else if (countLcp == 2) {
+ assert_more_than(entry.startTime, firstLcp.startTime);
+ assert_more_than(entry.size, firstLcp.size);
+ resolve();
+ }
+ }
+ }).observe({ entryTypes: ['largest-contentful-paint'] });
+ });
+ return Promise.race([timeoutPromise, testPromise]);
+ })
+ </script>
+ <div id="text">text</div>
+ <link rel="stylesheet" href="/resources/slow-style-change.py">
+</body>
diff --git a/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html b/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html
new file mode 100644
index 0000000000..a26ddd1420
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/video-data-uri.html
@@ -0,0 +1,148 @@
+<!doctype html>
+<html>
+<title>This test verifies a video element of data uri src triggers an LCP entry
+ to be emitted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <script>
+ const get_lcp_entry = () => {
+ return new Promise(resolve => {
+ new PerformanceObserver((list, observer) => {
+ if (list.getEntries()) {
+ observer.disconnect();
+ resolve(list.getEntries()[0]);
+ }
+ }).observe({ type: "largest-contentful-paint", buffered: true });
+ });
+ };
+
+ promise_test(async t => {
+ lcpEntry = await get_lcp_entry();
+ assert_true(lcpEntry.url.startsWith('data:video/mp4;base64,T2dnUwAC'));
+ }, "Video of data URI src should trigger an LCP entry to be emitted.")
+ </script>
+ <!--
+ This is the base64 encoding of the images/pattern.ogv video file.
+ -->
+ <video autoplay muted src="data:video/mp4;base64,T2dnUwACAAAAAAAAAADmWaI+AAAAAKRLSUwBKoB0aGVvcmEDAgEAAgACAAAUAAAUAAwAAAAeAAAA
+ AQAAAQAAAQAAAACw2E9nZ1MAAAAAAAAAAAAA5lmiPgEAAADp90reDj////////////////+QgXRo
+ ZW9yYQ0AAABMYXZmNTcuMjYuMTAwAQAAAB8AAABlbmNvZGVyPUxhdmM1Ny4yNC4xMDUgbGlidGhl
+ b3JhgnRoZW9yYb7NKPe5zWsYtalJShBznOYxjFKUpCEIMYxiEIQhCEAAAAAAAAAAAAARba5TZ5LI
+ /FYS/Hg5W2zmKvVoq1QoEykkWhD+eTmbjWZTCXiyVSmTiSSCGQh8PB2OBqNBgLxWKhQJBGIhCHw8
+ HAyGAsFAiDgVFtrlNnksj8VhL8eDlbbOYq9WirVCgTKSRaEP55OZuNZlMJeLJVKZOJJIIZCHw8HY
+ 4Go0GAvFYqFAkEYiEIfDwcDIYCwUCIOBQLDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw
+ 8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8MDA8SFBQVDQ0OERIVFRQODg8SFBUVFQ
+ 4QERMUFRUVEBEUFRUVFRUSExQVFRUVFRQVFRUVFRUVFRUVFRUVFRUQDAsQFBkbHA0NDhIVHBwbDg
+ 0QFBkcHBwOEBMWGx0dHBETGRwcHh4dFBgbHB0eHh0bHB0dHh4eHh0dHR0eHh4dEAsKEBgoMz0MDA
+ 4TGjo8Nw4NEBgoOUU4DhEWHTNXUD4SFiU6RG1nTRgjN0BRaHFcMUBOV2d5eGVIXF9icGRnYxMTEx
+ MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx
+ MTExMSEhUZGhoaGhIUFhoaGhoaFRYZGhoaGhoZGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGh
+ oaGhoaGhoaGhoaERIWHyQkJCQSFBgiJCQkJBYYISQkJCQkHyIkJCQkJCQkJCQkJCQkJCQkJCQkJC
+ QkJCQkJCQkJCQkJCQkJCQkJBESGC9jY2NjEhUaQmNjY2MYGjhjY2NjYy9CY2NjY2NjY2NjY2NjY2
+ NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFR
+ UVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVEhISFRcYGRsSEhUXGBkbHBIVFxgZGxwdFR
+ cYGRscHR0XGBkbHB0dHRgZGxwdHR0eGRscHR0dHh4bHB0dHR4eHhERERQXGhwgEREUFxocICIRFB
+ caHCAiJRQXGhwgIiUlFxocICIlJSUaHCAiJSUlKRwgIiUlJSkqICIlJSUpKioQEBAUGBwgKBAQFB
+ gcICgwEBQYHCAoMEAUGBwgKDBAQBgcICgwQEBAHCAoMEBAQGAgKDBAQEBggCgwQEBAYICAB8Xlx0
+ fV7c7D8vrrAaZid8hRvB1RN7csxFuo43wH7lEkS9wbGS+tVSNMyuxdiECcjB7R1Ml85htasNjKpS
+ vPt3D8k7iGmZXYuxBC+RR4arUGxkvH5y7mJXR7R5Jwn3VUhBiuap91VIrsaCM5TSg9o867khwMrW
+ Y2+cP4rwvBLzt/wnHaYe0edSRMYC6tZmU1BrvhktIUf2gXoU8bHMuyNA7lB7R51ym213sFcFKowI
+ viT/i0Wscg+4RDubX+4haRsMxZWgN05K5FD3bzqS9VSVCPM4TpWs2C43ihFdgaSByeKHu3Xf/2TG
+ 8tgpB7PAtOs7jixWYw+Ayo5GjUTSybX/1KW52RxYfB8nBNLJtHgt4DPq6BZWBFpjyZX/1KW5Ca0e
+ vOwG1EX/A9j5fQm5hOz6W2CtcCaWTXTFAeZO71VIgCTX69y9TiaXag3Os2ES1DcLKw0/xR5HfnCq
+ kpQF0Z1kxKNfhZWLycml2keduHMQh3HubB/pbUUoCK5wxetZRZWPJF/bdyE21H2YjMOhP/pkthqK
+ UCOEWVm68+1J5n7ahES5sOhaZPdOC5j4kc91FVIsrF8ofe+A2on/16Z4RiKQZcMU3NouO9N4YAvr
+ WaiA6h4bfLqhTitbnnJ2iPSVRNJH+aZGE+YXzq7Ah/OncW2K59AKamlocOUYTSvaJPNcjDfMGrmG
+ 9pOV2MbgI9v3B3ECZ7RLJ51UpzMn0C1huA87Ngom9lkiaw3t5yvFZmDl1HpkuP+PiqlawgD69jAT
+ 5Nxr2i6cwiytcwHhK2KJvZI9C1m/4VUil8RvO/ydxmgsFdzdgGpMbUeyyRNOi1k5hMb6hVSMuTrO
+ E/xuDhGExQ219l07sV2kG5fOEnkWHwgqUkbvC0P2KTytY4nHLqJDc3DMGlDbX2aXK/4UuJxizaIk
+ ZITS7a3HN5374PrVlYKIcP9xl1BUKqQ7aAml2k1o5uGcN8A+tPz1HF1YVnmE7cyx4FIiUA2ml1k0
+ hX9HB7l4tMO+R9YrMWcf5Anub1BZXUp3Ce4jBM21l0kyhcF/vg6FGeHa345MYv4BVSciTJhj5Abu
+ D2K0dfIXc4jKAbazaS53rv1lYqpIVr2fcgcPox4u/WVnRfJ25GGING2s2cqjKIVUtwGbRtrljLd9
+ CQOHhewUTfiKxWk7Olr2dHyIKlLgejEbasmmdGF/dhuhVrU9xGi6Hksgm/+5Bw813T3mJyRNqIYG
+ dYspVZFzQ6dhNLJ7H+fYWh8Q+cMbzLc/O0evM4srXGjpECaXaT2jApqM4LRavgPnH7ecDRQSErab
+ X3zC4EcXfOVZZUpYs3UIfMsKVR+6hgFzHhvWWWl4EqZtrJpHnyeO0T2icPrqVRyyDRKmbayexv7w
+ dolGfh1hwtsK4G5jDOIHz/lTULUM47PaBmNJm2ssmTq+ssXeHBjgij3G5P+u5QVFIGQ21TNM5aGO
+ HbqKssQ/HiM9kvcWjdCtF6gZNMzbXFhNP2gV2FNQi+OpOR+S+3RvOBVSOr+E5hjyPrQho7/QDNEG
+ 2qRNLpHl6WVl3m4p3POFvwEWUN0ByvCQTSttdM48H7tjQWVk73qoUvhiSDbVK0mzyohbuHXofmEa
+ K/xXYJ+Vq7tBUN6lMAdrouC3p96IS8kMzbVK0myY4f+HKdRGsrG9SlDwEfQkXsGLIbapmmcv/sA5
+ TrqC36t4sRdjylU4JC9KwG2plM0zxuT2iFFzAPXyj9ZWRu+tx5UpFv0jn0gQrKyMF5MyaZsDbXG7
+ /qIdp0tHG4jOQumLzBliaZttaLfZFUBSOu7FaUn/+IXETfwUj2E0o6gJ2HB/l8N7jFnzWWBESEra
+ bWPvy9bUKqS4y78CME0rbXSTNFRf8H7r1wwxQbltish5nFVIRkhKaTNtc6L3LHAh8+B2yi/tHvXG
+ 4nusVwAKMb/0/MCmoWrvASDM0mbay5YRI+7CtC96OPtxudDEyTGmbbWVRgkvR8qaiA8+rLCft7cW
+ 8H8UI3E8nzmJVSQIT3+0srHfUbgKA21ZNM8WEy+W7wbj9OuBpm21MKGWN80kaA5PZfoSqkRPLa1h
+ 31wIEjiUhcnX/e5VSWVkQnPhtqoYXrjLFpn7M8tjB17xSqfWgoA21StJpM48eSG+5A/dsGUQn8sV
+ 7impA4dQjxPyrsBfHd8tUGBIJWkxtrnljE3eu/xTUO/nVsA9I4uVlZ5uQvy9IwYjbWUmaZ5XE9HA
+ WVkXUKmoI3y4vDKZpnKNtccJHK2iA83ej+fvgI3KR9P6qpG/kBCUdxHFisLkq8aZttTCZlj/b0G8
+ XoLX/3fHhZWCVcMsWmZtqmYXz0cpOiBHCqpKUZu76iICRxYVuSULpmF/421MsWmfyhbP4ew1FVKA
+ jFlY437JXImUTm2r/4ZYtMy61hf16RPJIRA8tU1BDc5/JzAkEzTM21lyx7sK9wojRX/OHXoOv05I
+ DbUymaZyscL7qlMA8c/CiK3csceqzuOEU1EPpbz4QEahIShpm21MJmWN924f98WKyf51EEYBli0z
+ NtUzC+6X9P9ysrU1CHyA3RJFFr1w67HpyULT+YMsWmZtquYXz97oKil44sI1bpL8hRSDeMkhiIBw
+ OgxwZ5Fs6+5M+NdH+3Kjv0sreSqqRvGSQxEA4HQY4M8i2dfcmfGuj/blR36WVvJVVI3jJIYiAcDo
+ McGeRbOvuTPjXR/tyo79LK3kqqkVUnCfqAES8EzTM21lykY4Q+LKxby+9F3ZHR/uC2OGpS9cv6BZ
+ XAebhckMGIymaZm2st8/B38i6A/n58pVLKwfURet4UBwSF6UaZttSZljhd2jW9BZWcrX0/hG4Sdt
+ /SBCdH6UMJmWK80zba3URKaik8iB9PR2459CuyOAbi0/GWLTMmYXm2t0vUkNQhRPVldKpAN5HgHy
+ ZfdOtGuj/YxwZ5S8u3CjqMgQoyQJRdawvJlE530/+sVg21c8GWLTPf3yJVSVUoCMWVjjfslciZRO
+ bav/hli0zLrWF/XpE8khT2dnUwAAQAAAAAAAAADmWaI+AgAAAA6lVqcC/yoswcgPXVDgwIQQYBK8
+ JHGwwhNHk7ANjkOfhcKbwVnSYLxZeR6bjpWsA1Y6R+wZcHLxjMJmal1gZAIcaRD6tAh8GgJhGSjo
+ RpMND9CVoehCgT8+hDwjEFfgrNOcG5ocU5t9Rh4kcJd9R5dXmYNMXqo9IPXZaceU+4mxu3IDOM1n
+ B9miEvZlRdJVqYnBiCi1YTjCdVTR09SWSpdB0VbJ7X5K4yFui7Z1Jd5eyf7J8/JTx+2ji97qbeSC
+ 43FWTwEqySHBdRDnj5dGfPoc5Z1yIDxOmzKKkkTEqU7vDvwgj0xCs82zZqaO10ruMxgqZ/lTOZZm
+ VXKlRKlXdtEJ3duzrZ6ubMGIlwLV6+zHVrI8HdzncTtng2MaugcSSKFo5x4tqwQh7fGnvW140wBP
+ Z2dTAABLAAAAAAAAAOZZoj4DAAAAwc+ctgsAAAAAAAAAAAAAAE9nZ1MAAEADAAAAAAAA5lmiPgQA
+ AADHLitMAv8qLMHID11Q4MCEEGASvCRxsMITR5OwDY5Dn4XCm8FZ0mC8WXkem46VrANWOkfsGXBy
+ 8YzCZmpdYGQCHGkQ+rQIfBoCYRko6EaTDQ/QlaHoQoE/PoQ8IxBX4KzTnBuaHFObfUYeJHCXfUeX
+ V5mDTF6qPSD12WnHlPuJsbtyAzjNZwfZohL2ZUXSVamJwYgotWE4wnVU0dPUlkqXQdFWye1+SuMh
+ bou2dSXeXsn+yfPyU8fto4ve6m3kguNxVk8BKskhwXUQ54+XRnz6HOWdciA8TpsyipJExKlO7w78
+ II9MQrPNs2amjtdK7jMYKmf5UzmWZlVypUSpV3bRCd3bs62ermzBiJcC1evsx1ayPB3c53E7Z4Nj
+ GroHEkihaOceLasEIe3xp71teNMAT2dnUwAASwMAAAAAAADmWaI+BQAAAL37AWwLAAAAAAAAAAAA
+ AABPZ2dTAABABgAAAAAAAOZZoj4GAAAA5hY3pwL/KizByA9dUODAhBBgErwkcbDCE0eTsA2OQ5+F
+ wpvBWdJgvFl5HpuOlawDVjpH7BlwcvGMwmZqXWBkAhxpEPq0CHwaAmEZKOhGkw0P0JWh6EKBPz6E
+ PCMQV+Cs05wbmhxTm31GHiRwl31Hl1eZg0xeqj0g9dlpx5T7ibG7cgM4zWcH2aIS9mVF0lWpicGI
+ KLVhOMJ1VNHT1JZKl0HRVsntfkrjIW6LtnUl3l7J/snz8lPH7aOL3upt5ILjcVZPASrJIcF1EOeP
+ l0Z8+hzlnXIgPE6bMoqSRMSpTu8O/CCPTEKzzbNmpo7XSu4zGCpn+VM5lmZVcqVEqVd20Qnd27Ot
+ nq5swYiXAtXr7MdWsjwd3OdxO2eDYxq6BxJIoWjnHi2rBCHt8ae9bXjTAE9nZ1MAAEsGAAAAAAAA
+ 5lmiPgcAAACJ2bKjCwAAAAAAAAAAAAAAT2dnUwAAQAkAAAAAAADmWaI+CAAAAP/6iUwC/yoswcgP
+ XVDgwIQQYBK8JHGwwhNHk7ANjkOfhcKbwVnSYLxZeR6bjpWsA1Y6R+wZcHLxjMJmal1gZAIcaRD6
+ tAh8GgJhGSjoRpMND9CVoehCgT8+hDwjEFfgrNOcG5ocU5t9Rh4kcJd9R5dXmYNMXqo9IPXZaceU
+ +4mxu3IDOM1nB9miEvZlRdJVqYnBiCi1YTjCdVTR09SWSpdB0VbJ7X5K4yFui7Z1Jd5eyf7J8/JT
+ x+2ji97qbeSC43FWTwEqySHBdRDnj5dGfPoc5Z1yIDxOmzKKkkTEqU7vDvwgj0xCs82zZqaO10ru
+ MxgqZ/lTOZZmVXKlRKlXdtEJ3duzrZ6ubMGIlwLV6+zHVrI8HdzncTtng2MaugcSSKFo5x4tqwQh
+ 7fGnvW140wBPZ2dTAABLCQAAAAAAAOZZoj4JAAAAZcFzUwsAAAAAAAAAAAAAAE9nZ1MAAEAMAAAA
+ AAAA5lmiPgoAAADewpWnAv8qLMHID11Q4MCEEGASvCRxsMITR5OwDY5Dn4XCm8FZ0mC8WXkem46V
+ rANWOkfsGXBy8YzCZmpdYGQCHGkQ+rQIfBoCYRko6EaTDQ/QlaHoQoE/PoQ8IxBX4KzTnBuaHFOb
+ fUYeJHCXfUeXV5mDTF6qPSD12WnHlPuJsbtyAzjNZwfZohL2ZUXSVamJwYgotWE4wnVU0dPUlkqX
+ QdFWye1+SuMhbou2dSXeXsn+yfPyU8fto4ve6m3kguNxVk8BKskhwXUQ54+XRnz6HOWdciA8Tpsy
+ ipJExKlO7w78II9MQrPNs2amjtdK7jMYKmf5UzmWZlVypUSpV3bRCd3bs62ermzBiJcC1evsx1ay
+ PB3c53E7Z4NjGroHEkihaOceLasEIe3xp71teNMAT2dnUwAASwwAAAAAAADmWaI+CwAAAFHjwJwL
+ AAAAAAAAAAAAAABPZ2dTAABADwAAAAAAAOZZoj4MAAAAF0noTAL/KizByA9dUODAhBBgErwkcbDC
+ E0eTsA2OQ5+FwpvBWdJgvFl5HpuOlawDVjpH7BlwcvGMwmZqXWBkAhxpEPq0CHwaAmEZKOhGkw0P
+ 0JWh6EKBPz6EPCMQV+Cs05wbmhxTm31GHiRwl31Hl1eZg0xeqj0g9dlpx5T7ibG7cgM4zWcH2aIS
+ 9mVF0lWpicGIKLVhOMJ1VNHT1JZKl0HRVsntfkrjIW6LtnUl3l7J/snz8lPH7aOL3upt5ILjcVZP
+ ASrJIcF1EOePl0Z8+hzlnXIgPE6bMoqSRMSpTu8O/CCPTEKzzbNmpo7XSu4zGCpn+VM5lmZVcqVE
+ qVd20Qnd27Otnq5swYiXAtXr7MdWsjwd3OdxO2eDYxq6BxJIoWjnHi2rBCHt8ae9bXjTAE9nZ1MA
+ AEsPAAAAAAAA5lmiPg0AAAAt111GCwAAAAAAAAAAAAAAT2dnUwAAQBIAAAAAAADmWaI+DgAAALvQ
+ BAYC/yoswcgPXVDgwIQQYBK8JHGwwhNHk7ANjkOfhcKbwVnSYLxZeR6bjpWsA1Y6R+wZcHLxjMJm
+ al1gZAIcaRD6tAh8GgJhGSjoRpMND9CVoehCgT8+hDwjEFfgrNOcG5ocU5t9Rh4kcJd9R5dXmYNM
+ Xqo9IPXZaceU+4mxu3IDOM1nB9miEvZlRdJVqYnBiCi1YTjCdVTR09SWSpdB0VbJ7X5K4yFui7Z1
+ Jd5eyf7J8/JTx+2ji97qbeSC43FWTwEqySHBdRDnj5dGfPoc5Z1yIDxOmzKKkkTEqU7vDvwgj0xC
+ s82zZqaO10ruMxgqZ/lTOZZmVXKlRKlXdtEJ3duzrZ6ubMGIlwLV6+zHVrI8HdzncTtng2MaugcS
+ SKFo5x4tqwQh7fGnvW140wBPZ2dTAABLEgAAAAAAAOZZoj4PAAAAgHc9kAsAAAAAAAAAAAAAAE9n
+ Z1MAAEAVAAAAAAAA5lmiPhAAAAD0zWMtAv8qLMHID11Q4MCEEGASvCRxsMITR5OwDY5Dn4XCm8FZ
+ 0mC8WXkem46VrANWOkfsGXBy8YzCZmpdYGQCHGkQ+rQIfBoCYRko6EaTDQ/QlaHoQoE/PoQ8IxBX
+ 4KzTnBuaHFObfUYeJHCXfUeXV5mDTF6qPSD12WnHlPuJsbtyAzjNZwfZohL2ZUXSVamJwYgotWE4
+ wnVU0dPUlkqXQdFWye1+SuMhbou2dSXeXsn+yfPyU8fto4ve6m3kguNxVk8BKskhwXUQ54+XRnz6
+ HOWdciA8TpsyipJExKlO7w78II9MQrPNs2amjtdK7jMYKmf5UzmWZlVypUSpV3bRCd3bs62ermzB
+ iJcC1evsx1ayPB3c53E7Z4NjGroHEkihaOceLasEIe3xp71teNMAT2dnUwAASxUAAAAAAADmWaI+
+ EQAAAKLKJiULAAAAAAAAAAAAAABPZ2dTAARAGAAAAAAAAOZZoj4SAAAAmbxPPwL/KizByA9dUODA
+ hBBgErwkcbDCE0eTsA2OQ5+FwpvBWdJgvFl5HpuOlawDVjpH7BlwcvGMwmZqXWBkAhxpEPq0CHwa
+ AmEZKOhGkw0P0JWh6EKBPz6EPCMQV+Cs05wbmhxTm31GHiRwl31Hl1eZg0xeqj0g9dlpx5T7ibG7
+ cgM4zWcH2aIS9mVF0lWpicGIKLVhOMJ1VNHT1JZKl0HRVsntfkrjIW6LtnUl3l7J/snz8lPH7aOL
+ 3upt5ILjcVZPASrJIcF1EOePl0Z8+hzlnXIgPE6bMoqSRMSpTu8O/CCPTEKzzbNmpo7XSu4zGCpn
+ +VM5lmZVcqVEqVd20Qnd27Otnq5swYiXAtXr7MdWsjwd3OdxO2eDYxq6BxJIoWjnHi2rBCHt8ae9
+ bXjTAA==" width="600"></video>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/video-poster.html b/testing/web-platform/tests/largest-contentful-paint/video-poster.html
new file mode 100644
index 0000000000..fdc691819b
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/video-poster.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Largest Contentful Paint: observe video poster image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/largest-contentful-paint-helpers.js"></script>
+<script>
+setup({"hide_test_state": true});
+async_test(function (t) {
+ assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+ const beforeLoad = performance.now();
+ const observer = new PerformanceObserver(
+ t.step_func_done(function(entryList) {
+ assert_equals(entryList.getEntries().length, 1);
+ const entry = entryList.getEntries()[0];
+ const url = window.location.origin + '/images/blue.png';
+ // blue.png is 133 by 106.
+ const size = 133 * 106;
+ checkImage(entry, url, 'the_poster', size, beforeLoad);
+ })
+ );
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
+}, "Able to observe a video's poster image.");
+</script>
+<video id='the_poster' src='/media/test.mp4' poster='/images/blue.png'/>
diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-block.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-block.html
new file mode 100644
index 0000000000..572442f2a2
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-block.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!--
+ Web-font styled text that gets resized during block period should not make a
+ LCP emission.
+-->
+<style>
+ @font-face {
+ font-family: 'ADTestFaceBlock';
+ src: url('/fonts/AD.woff');
+ font-display: block;
+ }
+
+ .test {
+ font-family: 'ADTestFaceBlock';
+ }
+
+</style>
+<div class="test">LCP: Web Font Styled Text Resize</div>
+<script>
+ function LCPEntryList(t) {
+ return new Promise(resolve => {
+ let = lcpEntries = [];
+ new PerformanceObserver((entryList, observer) => {
+ lcpEntries = lcpEntries.concat(entryList.getEntries());
+ if (lcpEntries) {
+ // Adding timeout to wait a bit more so that if there are more than
+ // expected LCP entries emitted, they can be observed.
+ t.step_timeout(() => {
+ resolve(lcpEntries);
+ observer.disconnect();
+ }, 200);
+ }
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+ }
+
+ promise_test(async t => {
+ await document.fonts.ready;
+
+ // Verify web font is downloaded.
+ assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported");
+ let url = '/fonts/AD.woff';
+ var absoluteURL = new URL(url, location.href).href;
+ assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font\
+ should be downloaded');
+
+ // Verify web font is available.
+ assert_true(document.fonts.check('16px ADTestFaceBlock'), 'Font should be the web font added');
+
+ // Verify there is only one LCP entry.
+ let entryList = await LCPEntryList(t);
+ assert_equals(entryList.length, 1, 'Web font styled text resize that occurs during block period should not make a new LCP emission.');
+
+ }, "LCP should be not updated if the web font styled text resize occurs during the block period.");
+</script>
diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-after-interaction.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-after-interaction.html
new file mode 100644
index 0000000000..3ba02bab5a
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-after-interaction.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-actions.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<style></style>
+<!--
+ Web-font styled text that gets resized after an interaction stops LCP
+ observation should not make a new LCP emission.
+-->
+<textarea id='input'></textarea>
+<div class="test">LCP: Web Font Styled Text Resize</div>
+<script>
+ function addCSSRules() {
+ styleSheet = document.styleSheets[0];
+ fontRuleSet = "@font-face {\
+ font-family: 'ADTestFaceInteraction';\
+ src: url('/fonts/AD.woff');\
+ font-display: swap;\
+ }";
+ fontAtRule = ".test {\
+ font-family: 'ADTestFaceInteraction';\
+ } ";
+ styleSheet.insertRule(fontRuleSet);
+ styleSheet.insertRule(fontAtRule);
+ }
+
+ function LCPEntryList(t) {
+ return new Promise(resolve => {
+ let = lcpEntries = [];
+ new PerformanceObserver((entryList, observer) => {
+ lcpEntries = lcpEntries.concat(entryList.getEntries());
+ if (lcpEntries) {
+ // Adding timeout to wait a bit more so that if there are more than
+ // expected LCP entries emitted, they can be observed.
+ t.step_timeout(() => {
+ resolve(lcpEntries);
+ observer.disconnect();
+ }, 200);
+ }
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+ }
+
+ promise_test(async t => {
+ await document.fonts.ready;
+ let system_font_size = document.getElementsByClassName('test')[0].offsetHeight;
+
+ // Verify an LCP entry is emitted.
+ let entryList = await LCPEntryList(t);
+ assert_equals(entryList.length, 1, 'Text with system font should make a LCP emission.');
+
+ let lcpEntryBeforeInteraction = entryList[0];
+
+ // Add event listener so that CSS rule would be added after there's an
+ // input.
+ let inputElement = document.getElementById('input');
+ inputElement.addEventListener('keydown', addCSSRules);
+
+ // Send key as input.
+ await test_driver.send_keys(inputElement, 'k');
+
+ // Wait for web font to load.
+ await document.fonts.ready;
+
+ // Verify web font is downloaded.
+ assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported");
+ let url = '/fonts/AD.woff';
+ var absoluteURL = new URL(url, location.href).href;
+ assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font should be downloaded.');
+
+ // Verify web font is available.
+ assert_true(document.fonts.check('16px ADTestFaceInteraction'), 'Font should be the web font added');
+
+ // Verify web font is applied.
+ let web_font_size = document.getElementsByClassName('test')[0].offsetHeight;
+ assert_not_equals(web_font_size, system_font_size, 'Web font swap should happen');
+
+ // Assert there is only 1 LCP entry, which verifies the added web font does
+ // not make a new LCP entry after an input.
+ entryList = await LCPEntryList(t);
+ assert_equals(entryList.length, 1, 'Text with system font should not make a LCP emission.');
+
+ // Verify the LCP entry is the same one emitted before interaction by
+ // asserting the size is the same.
+ assert_equals(lcpEntryBeforeInteraction.size, entryList[0].size, 'There should be only 1 LCP entry emitted.');
+
+ }, "LCP should be not updated if the web font styled text resize occurs after an interaction happens");
+</script>
diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-smaller.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-smaller.html
new file mode 100644
index 0000000000..253038eb8d
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-smaller.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @font-face {
+ font-family: 'TestFaceSmaller';
+ src: url('/fonts/CSSTest/csstest-basic-regular.ttf?pipe=trickle(d0.5)');
+ size-adjust: 30%;
+ font-display: swap;
+ }
+
+ .test {
+ font-family: 'TestFaceSmaller';
+ }
+
+</style>
+<!--
+ Web-font styled text that gets resized during swap period should not make a
+ LCP emission if the new size is smaller than the current LCP element size.
+-->
+<div class="test">LCP: Web Font Styled Text Resize</div>
+<script>
+ function LCPEntryList(t) {
+ return new Promise(resolve => {
+ let = lcpEntries = [];
+ new PerformanceObserver((entryList, observer) => {
+ lcpEntries = lcpEntries.concat(entryList.getEntries());
+ if (lcpEntries) {
+ // Adding timeout to wait a bit more so that if there are more than
+ // expected LCP entries emitted, they can be observed.
+ t.step_timeout(() => {
+ resolve(lcpEntries);
+ observer.disconnect();
+ }, 200);
+ }
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+ }
+
+ promise_test(async t => {
+ await document.fonts.ready;
+
+ // Verify web font is loaded.
+ assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported");
+ let url = '/fonts/CSSTest/csstest-basic-regular.ttf?pipe=trickle(d0.5)';
+ var absoluteURL = new URL(url, location.href).href;
+ assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font should be downloaded');
+
+ // Verify web font is available.
+ assert_true(document.fonts.check('10px TestFaceSmaller'), 'Font should be the web font added');
+
+ // Verify there is only one LCP entry.
+ let entryList = await LCPEntryList(t);
+ assert_equals(entryList.length, 1, 'Web font styled text resize that occurs during swap period but is smaller should not make a new LCP emission.')
+
+ }, "LCP should be not updated if the web font styled text resizes to be smaller during the swap period");
+</script>
diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-subnode.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-subnode.html
new file mode 100644
index 0000000000..eca5f3590f
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap-subnode.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @font-face {
+ font-family: 'ADTestFaceSwapSubnode';
+ src: url('/fonts/AD.woff?pipe=trickle(d0.5)');
+ font-display: swap;
+ }
+
+ .test {
+ font-family: 'ADTestFaceSwapSubnode';
+ }
+
+</style>
+<!--
+ Web-font styled subnode text that gets resized during swap period should make
+ a LCP emission if the new size is larger than the existing LCP element size.
+-->
+<div class="test">
+ <span>LCP: Web Font Styled Text Resize</span>
+</div>
+<script>
+ function LCPEntryList() {
+ return new Promise(resolve => {
+ let = lcpEntries = [];
+ new PerformanceObserver((entryList, observer) => {
+ lcpEntries = lcpEntries.concat(entryList.getEntries());
+ if (lcpEntries.length == 2) {
+ resolve(lcpEntries);
+ observer.disconnect();
+ }
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+ }
+
+ promise_test(async t => {
+ await document.fonts.ready;
+
+ // Verify web font is downloaded.
+ assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported");
+ let url = '/fonts/AD.woff?pipe=trickle(d0.5)';
+ var absoluteURL = new URL(url, location.href).href;
+ assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font\
+ should be downloaded');
+
+ // Verify web font is available.
+ assert_true(document.fonts.check('16px ADTestFaceSwapSubnode'), 'Font should be the web font added');
+
+ let entryList = await LCPEntryList();
+
+ // Verify there are 2 LCP entries emitted.
+ assert_equals(entryList.length, 2, 'There should be 2 LCP entries. The 1st one corresponds to the system font and the 2nd the web font.')
+
+ // Verify the size of 2nd LCP entry is larger than that of the 1st one.
+ assert_greater_than(entryList[1].size, entryList[0].size, 'The size of 2nd LCP entry should be larger than that of the 1st one.');
+
+ }, "LCP should be updated if the web font styled text resizes to be larger during the swap period");
+</script>
diff --git a/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap.html b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap.html
new file mode 100644
index 0000000000..61c00fad20
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/web-font-styled-text-resize-swap.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @font-face {
+ font-family: 'ADTestFaceSwap';
+ src: url('/fonts/AD.woff?pipe=trickle(d0.5)');
+ font-display: swap;
+ }
+
+ .test {
+ font-family: 'ADTestFaceSwap';
+ }
+
+</style>
+<!--
+ Web-font styled text that gets resized during swap period should make a
+ LCP emission if the new size is larger than the existing LCP element size.
+-->
+<div class="test">LCP: Web Font Styled Text Resize</div>
+<script>
+ function LCPEntryList() {
+ return new Promise(resolve => {
+ let = lcpEntries = [];
+ new PerformanceObserver((entryList, observer) => {
+ lcpEntries = lcpEntries.concat(entryList.getEntries());
+ if (lcpEntries.length == 2) {
+ resolve(lcpEntries);
+ observer.disconnect();
+ }
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ });
+ }
+
+ promise_test(async t => {
+ await document.fonts.ready;
+
+ // Verify web font is downloaded.
+ assert_own_property(window, 'PerformanceResourceTiming', "ResourceTiming not supported");
+ let url = '/fonts/AD.woff?pipe=trickle(d0.5)';
+ var absoluteURL = new URL(url, location.href).href;
+ assert_equals(performance.getEntriesByName(absoluteURL).length, 1, 'Web font\
+ should be downloaded');
+
+ // Verify web font is available.
+ assert_true(document.fonts.check('16px ADTestFaceSwap'), 'Font should be the web font added');
+
+ let entryList = await LCPEntryList();
+
+ // Verify there are 2 LCP entries emitted.
+ assert_equals(entryList.length, 2, 'There should be 2 LCP entries. The 1st one corresponds to the system font and the 2nd the web font.')
+
+ // Verify the size of 2nd LCP entry is larger than that of the 1st one.
+ assert_greater_than(entryList[1].size, entryList[0].size, 'The size of 2ndLCP entry should be larger than that of the 1st one.');
+
+ }, "LCP should be updated if the web font styled text resizes to be larger during the swap period");
+</script>