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

<!DOCTYPE HTML>
<html>

<head>
  <meta name="viewport" content="width=device-width,minimum-scale=1">
  <title>Test soft navigation UKM collection</title>
  <script src="resources/util.js"></script>
</head>

<body>
  <style>
    #shifter {
      position: relative;
      width: 300px;
      height: 200px;
      background: blue;
    }
  </style>
  <div id="shifter">shifter</div>
  <main id=main>
    <div>
      <a id=link><img src="/images/lcp-256x256.png"></a>
    </div>
  </main>
  <div id="div">content</div>
  <script>
    const addImage = async (element) => {
      const img = new Image();
      img.src = '/images/lcp-133x106.png' + "?" + Math.random();
      await img.decode();
      element.appendChild(img);
    };

    const addImageToMain = async () => {
      await addImage(document.body);
    };
    const waitOnPaintEntriesPromise = () => {
      return new Promise((resolve, reject) => {
        if (!performance.softNavPaintMetricsSupported) {
          // If we don't have paint entries, fall back to a timer instead.
          setTimeout(resolve, 200);
        }
        const paint_entries = []
        new PerformanceObserver(list => {
          paint_entries.push(...list.getEntries());
          if (paint_entries.length == 2) {
            resolve();
          } else if (paint_entries.length > 2) {
            reject();
          }
        }).observe({ type: 'paint', includeSoftNavigationObservations: true });
      });
    };

    const waitForRegularLcpEntries = () => {
      return new Promise(resolve => {
        new PerformanceObserver(resolve).observe(
          {
            type: 'largest-contentful-paint', buffered: true
          });
      })
    }


    const waitForSoftNavigationLcpEntries = () => {
      return new Promise(resolve => {
        new PerformanceObserver(resolve).observe(
          {
            type: 'largest-contentful-paint', buffered: true,
            includeSoftNavigationObservations: true
          });
      })
    }

    const GetSoftNavigationLCPEntries = async () => {
      const soft_nav_entry_navigation_ids = await new Promise(resolve => {
        (new PerformanceObserver(list => {
          if (list.getEntries().length >= 2) {
            resolve(list.getEntries().map(e => e.navigationId));
          }
        })).observe({
          type: 'soft-navigation', buffered: true
        });
      });

      const soft_nav_lcp_entries = await new Promise(resolve => {
        (new PerformanceObserver(list => {
          let soft_nav_entries = list.getEntries().filter(
            e => soft_nav_entry_navigation_ids.includes(e.navigationId));
          if (soft_nav_entries.length >= 2) {
            resolve(soft_nav_entries);
          }
        })).observe({
          type: 'largest-contentful-paint', buffered: true,
          includeSoftNavigationObservations: true
        });
      });

      return soft_nav_lcp_entries.map(e => JSON.stringify(e.toJSON()));
    }

    let counter = 0;
    const setEvent = (button, pushState, addContent, pushUrl, eventType) => {
      const URL = "foobar.html";
      button.addEventListener(eventType, async e => {
        // Jump through a task, to ensure task tracking is working properly.
        await new Promise(r => setTimeout(r, 0));
        const url = URL + "?" + counter;
        if (pushState) {
          if (pushUrl) {
            pushState(url);
          } else {
            pushState();
          }
        }
        await new Promise(r => setTimeout(r, 10));
        await addContent(url);
        ++counter;
      });
    };

    const setEventAndWait = async () => {
      await waitForRegularLcpEntries();
      const link = document.getElementById("link");
      setEvent(link, /*pushState=*/url => history.pushState({}, '', url),
        /*addContent=*/async () => await addImageToMain(), /*pushUrl=*/true,
        /*eventType=*/"click");
    }

    const waitForSoftNavigationEntry = async () => {
      await new Promise(resolve => {
        (new PerformanceObserver(resolve)).observe({
          type: 'soft-navigation', buffered: true
        });
      });
      await waitOnPaintEntriesPromise();;
      await waitForSoftNavigationLcpEntries();
    }

    const waitForSoftNavigationEntry2 = async () => {
      await new Promise(resolve => {
        (new PerformanceObserver(() => resolve())).observe({
          type: 'soft-navigation'
        });
      });
      await waitOnPaintEntriesPromise();
      await waitForSoftNavigationLcpEntries();
    }

    let eventPromises = [];
    const registerEventListeners = () => {
      eventPromises = [];
      const element = document.getElementById('div');
      for (const event of ['mouseup', 'pointerup', 'click']) {
        eventPromises.push(new Promise(resolve => {
          element.addEventListener(event, resolve, { once: true });
        }));
      }
    }

    const waitForClick = async () => {
      await Promise.all(eventPromises);
    }

    const addChangeColorEventListener = () => {
      const element = document.getElementById('div');
      element.addEventListener("pointerdown", () => {
        element.style = "color:red";
      });
    }


    if (PerformanceObserver.supportedEntryTypes.indexOf("layout-shift") == -1)
      throw new Error("Layout Instability API not supported");

    // An list to record all the entries with startTime and score.
    let element = document.querySelector("#shifter");

    (async () => {
      await waitUntilAfterNextLayout();
      document.querySelector("#shifter").style = "top: 160px";
    })();

    const triggerLayoutShift = async (multiplier = 1) => {
      // Wait for 50 milliseconds that the click simulation that caused the
      // soft navigation would not be counted as recent input.
      await new Promise(resolve => setTimeout(resolve, 500));
      let element = document.querySelector("#shifter")
      let val = parseInt(getComputedStyle(element).top, 10);
      element.style.top = (val + 40) * multiplier + "px";
    }

    const GetLayoutShift = async (expected_soft_nav_count = 0) => {
      let soft_nav_entry_navigation_id;
      if (expected_soft_nav_count > 0) {
        soft_nav_entry_navigation_id = await new Promise(resolve => {
          const observer = new PerformanceObserver(list => {
            let soft_nav_entries = list.getEntries();
            if (soft_nav_entries.length >= expected_soft_nav_count) {
              observer.disconnect();

              resolve(list.getEntries()[soft_nav_entries.length - 1].navigationId);
            }
          });
          observer.observe({
            type: 'soft-navigation', buffered: true
          });
        });
      }

      return await new Promise((resolve) => {
        const layout_shifts = [];
        const observer = new PerformanceObserver((list) => {
          for (const entry of list.getEntries()) {
            layout_shifts.push(entry);
          }

          if (expected_soft_nav_count > 0) {
            let soft_nav_layout_shifts = layout_shifts.filter(
              e => soft_nav_entry_navigation_id == e.navigationId);

            if (soft_nav_layout_shifts.length >= 1) {
              observer.disconnect();

              resolve(soft_nav_layout_shifts.map((entry) => ({
                startTime: entry.startTime,
                score: entry.value,
                hadRecentInput: entry.hadRecentInput,
              })));
            }
          } else {
            if (layout_shifts.length >= 1) {
              observer.disconnect();

              resolve(layout_shifts.map((entry) => ({
                startTime: entry.startTime,
                score: entry.value,
                hadRecentInput: entry.hadRecentInput,
              })));
            }
          }
        });

        observer.observe({
          type: 'layout-shift', buffered: true
        });
      });
    }
  </script>
</body>

</html>