chromium/chrome/browser/page_load_metrics/integration_tests/data/largest_contentful_paint.html

<script src="resources/testharness.js"></script>
<script>
// Tell testharness.js to not wait for 'real' tests; we only want
// testharness.js for its assertion helpers.
setup({'output': false});
</script>

<script>
  // 'AsyncBuffer' serves as a helper to buffer LCP reports asynchronously.
  class AsyncBuffer {
    constructor() {
      // 'pending' is an array that will buffer entries reported through the
      // PerformanceObserver and can be collected with 'pop'.
      this.pending = [];

      // 'resolve_fn' is a reference to the 'resolve' function of a
      // Promise that blocks for new entries to arrive via 'push()'. Calling
      // the function resolves the promise and unblocks calls to 'pop()'.
      this.resolve_fn = null;
    }

    // Concatenates the given 'entries' list to this AsyncBuffer.
    push(entries) {
      if (entries.length == 0) {
        throw new Error("Must not push an empty list of entries!");
      }
      this.pending = this.pending.concat(entries);

      // If there are calls to 'pop' that are blocked waiting for items, signal
      // that they can continue.
      if (this.resolve_fn != null) {
        this.resolve_fn();
        this.resolve_fn = null;
      }
    }

    // Takes the current pending entries from this AsyncBuffer. If there are no
    // entries queued already, this will block until some show up.
    async pop() {
      if (this.pending.length == 0) {
        // Need to instantiate a promise to block on. The next call to 'push'
        // will resolve the promise once it has queued the entries.
        await new Promise(resolve => {
          this.resolve_fn = resolve;
        });
      }
      assert_true(this.pending.length > 0);

      const result = this.pending;
      this.pending = [];
      return result;
    }
  }

  const buffer = new AsyncBuffer();
  const po = new PerformanceObserver(entryList => {
    buffer.push(entryList.getEntries());
  });
  po.observe({type: 'largest-contentful-paint', buffered: true});
</script>

<div id="content_div_1"></div>
<div id="content_div_2"></div>

<script>
  const block_for_next_lcp = async () => {
    return buffer.pop().then(seen_events => {
      // This test case assumes each LCP entry is handled before the next could
      // possibly be generated.
      assert_equals(seen_events.length, 1);
      return seen_events[0];
    });
  };

  // Adds the first image "lcp-16x16.png" to "content_div_1". We expect this
  // operation to trigger a new LCP entry.
  const add_first_image = () => {
    let img = document.createElement("img");
    content_div_1.appendChild(img);
    img.src = "images/lcp-16x16.png";
  };

  // Adds another image that is larger than "lcp-16x16.png". We expect this
  // operation to trigger a new LCP entry.
  const add_larger_image = () => {
    let new_img = document.createElement("img");
    content_div_2.appendChild(new_img);
    new_img.src = "images/lcp-96x96.png";
    new_img.id = "larger_image";
  };

  // Adds the largest image. We expect this
  // operation to trigger a new LCP entry.
  const add_largest_image = () => {
    let new_img = document.createElement("img");
    content_div_2.appendChild(new_img);
    new_img.src = "images/lcp-256x256.png";
  };

  // Removes the image added by 'add_larger_image'. We expect this operation to
  // not trigger a new LCP entry.
  const remove_larger_image = () => {
    const larger_image = document.getElementById("larger_image");
    assert_not_equals(larger_image, null);
    content_div_2.removeChild(larger_image);
  };

  const waitForAnimationFrames = frameCount => {
    return new Promise(resolve => {
      const handleFrame = () => {
        if (--frameCount <= 0)
          resolve();
        else
          requestAnimationFrame(handleFrame);
      };
      requestAnimationFrame(handleFrame);
    });
  };

  const test_first_image = async () => {
    // This test exercises the following scenario
    //  - have an initial page load with an image
    //  - assert that LCP fires for that image
    add_first_image();
    const lcp = await block_for_next_lcp();

    // Now that we've run through the scenario and collected our measurements,
    // return them in a structure that the C++ side can easily query.
    let output = [
    // lcp
    {
      url: lcp.url,
      time: lcp.startTime
    }];
    return output;
  };
  const test_larger_image = async () => {
    // This test exercises the following scenario
    //  - add a larger image to the page
    //  - assert that LCP fires for the new image
    //  - remove the larger image
    //  - wait for some rAFs
    add_larger_image();
    const lcp = await block_for_next_lcp();
    remove_larger_image();
    await waitForAnimationFrames(3);

    // Now that we've run through the scenario and collected our measurements,
    // return them in a structure that the C++ side can easily query.
    let output = [
    // lcp
    {
      url: lcp.url,
      time: lcp.startTime
    }];
    return output;
  };
  const test_largest_image = async () => {
    // This test exercises the following scenario
    //  - add the largest image to the page
    //  - assert that the new LCP is for the largest
    add_largest_image();
    const lcp = await block_for_next_lcp();

    // Now that we've run through the scenario and collected our measurements,
    // return them in a structure that the C++ side can easily query.
    let output = [
    // lcp
    {
      url: lcp.url,
      time: lcp.startTime
    }];
    return output;
  };
</script>