summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/soft-navigation-heuristics/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/soft-navigation-heuristics/resources')
-rw-r--r--testing/web-platform/tests/soft-navigation-heuristics/resources/empty.html2
-rw-r--r--testing/web-platform/tests/soft-navigation-heuristics/resources/history_push.js1
-rw-r--r--testing/web-platform/tests/soft-navigation-heuristics/resources/soft-navigation-helper.js356
3 files changed, 359 insertions, 0 deletions
diff --git a/testing/web-platform/tests/soft-navigation-heuristics/resources/empty.html b/testing/web-platform/tests/soft-navigation-heuristics/resources/empty.html
new file mode 100644
index 0000000000..5fa1cdf5e6
--- /dev/null
+++ b/testing/web-platform/tests/soft-navigation-heuristics/resources/empty.html
@@ -0,0 +1,2 @@
+<!DOCTYPE HTML>
+
diff --git a/testing/web-platform/tests/soft-navigation-heuristics/resources/history_push.js b/testing/web-platform/tests/soft-navigation-heuristics/resources/history_push.js
new file mode 100644
index 0000000000..6647dd740a
--- /dev/null
+++ b/testing/web-platform/tests/soft-navigation-heuristics/resources/history_push.js
@@ -0,0 +1 @@
+history.pushState({}, '', URL);
diff --git a/testing/web-platform/tests/soft-navigation-heuristics/resources/soft-navigation-helper.js b/testing/web-platform/tests/soft-navigation-heuristics/resources/soft-navigation-helper.js
new file mode 100644
index 0000000000..d405adb4e7
--- /dev/null
+++ b/testing/web-platform/tests/soft-navigation-heuristics/resources/soft-navigation-helper.js
@@ -0,0 +1,356 @@
+var counter = 0;
+var interacted;
+var timestamps = []
+const MAX_CLICKS = 50;
+// Entries for one hard navigation + 50 soft navigations.
+const MAX_PAINT_ENTRIES = 51;
+const URL = "foobar.html";
+const readValue = (value, defaultValue) => {
+ return value !== undefined ? value : defaultValue;
+}
+const testSoftNavigation =
+ options => {
+ const addContent = options.addContent;
+ const link = options.link;
+ const pushState = readValue(options.pushState,
+ url=>{history.pushState({}, '', url)});
+ const clicks = readValue(options.clicks, 1);
+ const extraValidations = readValue(options.extraValidations,
+ () => {});
+ const testName = options.testName;
+ const pushUrl = readValue(options.pushUrl, true);
+ const eventType = readValue(options.eventType, "click");
+ const interactionFunc = options.interactionFunc;
+ const eventPrepWork = options.eventPrepWork;
+ promise_test(async t => {
+ await waitInitialLCP();
+ const preClickLcp = await getLcpEntries();
+ setEvent(t, link, pushState, addContent, pushUrl, eventType,
+ eventPrepWork);
+ let first_navigation_id;
+ for (let i = 0; i < clicks; ++i) {
+ const firstClick = (i === 0);
+ let paint_entries_promise =
+ waitOnPaintEntriesPromise(firstClick);
+ interacted = false;
+ interact(link, interactionFunc);
+
+ const navigation_id = await waitOnSoftNav();
+ if (!first_navigation_id) {
+ first_navigation_id = navigation_id;
+ }
+ // Ensure paint timing entries are fired before moving on to the next
+ // click.
+ await paint_entries_promise;
+ }
+ assert_equals(
+ document.softNavigations, clicks,
+ 'Soft Navigations detected are the same as the number of clicks');
+ await validateSoftNavigationEntry(
+ clicks, extraValidations, pushUrl);
+
+ await runEntryValidations(preClickLcp, first_navigation_id, clicks + 1, options.validate);
+ }, testName);
+ };
+
+const testNavigationApi = (testName, navigateEventHandler, link) => {
+ promise_test(async t => {
+ navigation.addEventListener('navigate', navigateEventHandler);
+ const navigated = new Promise(resolve => {
+ navigation.addEventListener('navigatesuccess', resolve);
+ navigation.addEventListener('navigateerror', resolve);
+ });
+ await waitInitialLCP();
+ const preClickLcp = await getLcpEntries();
+ let paint_entries_promise = waitOnPaintEntriesPromise();
+ interact(link);
+ const first_navigation_id = await waitOnSoftNav();
+ await navigated;
+ await paint_entries_promise;
+ assert_equals(document.softNavigations, 1, 'Soft Navigation detected');
+ await validateSoftNavigationEntry(1, () => {}, 'foobar.html');
+
+ await runEntryValidations(preClickLcp, first_navigation_id);
+ }, testName);
+};
+
+const testSoftNavigationNotDetected = options => {
+ promise_test(async t => {
+ const preClickLcp = await getLcpEntries();
+ options.eventTarget.addEventListener(options.eventName, options.eventHandler);
+ interact(options.link);
+ await new Promise((resolve, reject) => {
+ (new PerformanceObserver(() =>
+ reject("Soft navigation should not be triggered"))).observe({
+ type: 'soft-navigation',
+ buffered: true
+ });
+ t.step_timeout(resolve, 1000);
+ });
+ if (document.softNavigations) {
+ assert_equals(
+ document.softNavigations, 0, 'Soft Navigation not detected');
+ }
+ const postClickLcp = await getLcpEntries();
+ assert_equals(
+ preClickLcp.length, postClickLcp.length, 'No LCP entries accumulated');
+ }, options.testName);
+ };
+
+const runEntryValidations =
+ async (preClickLcp, first_navigation_id, entries_expected_number = 2,
+ validate = null) => {
+ await validatePaintEntries('first-contentful-paint', entries_expected_number,
+ first_navigation_id);
+ await validatePaintEntries('first-paint', entries_expected_number,
+ first_navigation_id);
+ const postClickLcp = await getLcpEntries();
+ const postClickLcpWithoutSoftNavs = await getLcpEntriesWithoutSoftNavs();
+ assert_greater_than(
+ postClickLcp.length, preClickLcp.length,
+ 'Soft navigation should have triggered at least an LCP entry');
+
+ if (validate) {
+ await validate();
+ }
+ assert_equals(
+ postClickLcpWithoutSoftNavs.length, preClickLcp.length,
+ 'Soft navigation should not have triggered an LCP entry when the ' +
+ 'observer did not opt in');
+ assert_not_equals(
+ postClickLcp[postClickLcp.length - 1].size,
+ preClickLcp[preClickLcp.length - 1].size,
+ 'Soft navigation LCP element should not have identical size to the hard ' +
+ 'navigation LCP element');
+ assert_equals(
+ postClickLcp[preClickLcp.length].navigationId,
+ first_navigation_id, 'Soft navigation LCP should have the same navigation ' +
+ 'ID as the last soft nav entry')
+};
+
+const interact =
+ (link, interactionFunc = undefined) => {
+ if (test_driver) {
+ if (interactionFunc) {
+ interactionFunc();
+ } else {
+ test_driver.click(link);
+ }
+ timestamps[counter] = {"syncPostInteraction": performance.now()};
+ }
+ }
+
+const setEvent = (t, button, pushState, addContent, pushUrl, eventType, prepWork) => {
+ const eventObject =
+ (eventType == 'click' || eventType.startsWith("key")) ? button : window;
+ eventObject.addEventListener(eventType, async e => {
+ let prepWorkFailed = false;
+ if (prepWork &&!prepWork(t)) {
+ prepWorkFailed = true;
+ }
+ // This is the end of the event's sync processing.
+ if (!timestamps[counter]["eventEnd"]) {
+ timestamps[counter]["eventEnd"] = performance.now();
+ }
+ if (prepWorkFailed) {
+ return;
+ }
+ // Jump through a task, to ensure task tracking is working properly.
+ await new Promise(r => t.step_timeout(r, 0));
+
+ const url = URL + "?" + counter;
+ if (pushState) {
+ // Change the URL
+ if (pushUrl) {
+ pushState(url);
+ } else {
+ pushState();
+ }
+ }
+
+ // Wait 10 ms to make sure the timestamps are correct.
+ await new Promise(r => t.step_timeout(r, 10));
+
+ await addContent(url);
+
+ interacted = true;
+ ++counter;
+ });
+};
+
+const validateSoftNavigationEntry = async (clicks, extraValidations,
+ pushUrl) => {
+ const [entries, options] = await new Promise(resolve => {
+ (new PerformanceObserver((list, obs, options) => resolve(
+ [list.getEntries(), options]))).observe(
+ {type: 'soft-navigation', buffered: true});
+ });
+ const expectedClicks = Math.min(clicks, MAX_CLICKS);
+
+ assert_equals(entries.length, expectedClicks,
+ "Performance observer got an entry");
+ for (let i = 0; i < entries.length; ++i) {
+ const entry = entries[i];
+ assert_true(entry.name.includes(pushUrl ? URL : document.location.href),
+ "The soft navigation name is properly set");
+ const entryTimestamp = entry.startTime;
+ assert_less_than_equal(timestamps[i]["syncPostInteraction"], entryTimestamp,
+ "Entry timestamp is lower than the post interaction one");
+ assert_greater_than_equal(
+ entryTimestamp, timestamps[i]['eventEnd'],
+ 'Event start timestamp matches');
+ assert_not_equals(entry.navigationId,
+ performance.getEntriesByType("navigation")[0].navigationId,
+ "The navigation ID was re-generated and different from the initial one.");
+ if (i > 0) {
+ assert_not_equals(entry.navigationId,
+ entries[i-1].navigationId,
+ "The navigation ID was re-generated between clicks");
+ }
+ }
+ assert_equals(performance.getEntriesByType("soft-navigation").length,
+ expectedClicks, "Performance timeline got an entry");
+ await extraValidations(entries, options);
+
+};
+
+const validatePaintEntries = async (type, entries_number, first_navigation_id) => {
+ if (!performance.softNavPaintMetricsSupported) {
+ return;
+ }
+ const expected_entries_number = Math.min(entries_number, MAX_PAINT_ENTRIES);
+ const entries = await new Promise(resolve => {
+ const entries = [];
+ (new PerformanceObserver(list => {
+ entries.push(...list.getEntriesByName(type));
+ if (entries.length >= expected_entries_number) {
+ resolve(entries);
+ }
+ })).observe(
+ {type: 'paint', buffered: true, includeSoftNavigationObservations: true});
+ });
+ const entries_without_softnavs = await new Promise(resolve => {
+ (new PerformanceObserver(list => resolve(
+ list.getEntriesByName(type)))).observe(
+ {type: 'paint', buffered: true});
+ });
+ assert_equals(entries.length, expected_entries_number,
+ `There are ${entries_number} entries for ${type}`);
+ assert_equals(entries_without_softnavs.length, 1,
+ `There is one non-softnav entry for ${type}`);
+ if (entries_number > 1) {
+ assert_not_equals(entries[0].startTime, entries[1].startTime,
+ "Entries have different timestamps for " + type);
+ }
+ if (expected_entries_number > entries_without_softnavs.length) {
+ assert_equals(entries[entries_without_softnavs.length].navigationId,
+ first_navigation_id,
+ "First paint entry should have the same navigation ID as the last soft " +
+ "navigation entry");
+ }
+};
+
+const waitInitialLCP = () => {
+ return new Promise(resolve => {
+ new PerformanceObserver(list => resolve()).observe({
+ type: 'largest-contentful-paint',
+ buffered: true
+ });
+ });
+}
+
+const waitOnSoftNav = () => {
+ return new Promise(resolve => {
+ (new PerformanceObserver(list => {
+ const entries = list.getEntries();
+ assert_equals(entries.length, 1,
+ "Only one soft navigation entry");
+ resolve(entries[0].navigationId);
+ })).observe({
+ type: 'soft-navigation'
+ });
+ });
+};
+
+const getLcpEntries = async () => {
+ const entries = await new Promise(resolve => {
+ (new PerformanceObserver(list => resolve(
+ list.getEntries()))).observe(
+ {type: 'largest-contentful-paint', buffered: true,
+ includeSoftNavigationObservations: true});
+ });
+ return entries;
+};
+
+const getLcpEntriesWithoutSoftNavs = async () => {
+ const entries = await new Promise(resolve => {
+ (new PerformanceObserver(list => resolve(
+ list.getEntries()))).observe(
+ {type: 'largest-contentful-paint', buffered: true});
+ });
+ return entries;
+};
+
+const addImage = async (element, url="blue.png", id = "imagelcp") => {
+ const img = new Image();
+ img.src = '/images/'+ url + "?" + Math.random();
+ img.id=id
+ img.setAttribute("elementtiming", id);
+ await img.decode();
+ element.appendChild(img);
+};
+const addImageToMain = async (url="blue.png", id = "imagelcp") => {
+ await addImage(document.getElementById('main'), url, id);
+};
+
+const addTextParagraphToMain = (text, element_timing = "") => {
+ const main = document.getElementById("main");
+ const p = document.createElement("p");
+ const textNode = document.createTextNode(text);
+ p.appendChild(textNode);
+ if (element_timing) {
+ p.setAttribute("elementtiming", element_timing);
+ }
+ p.style = "font-size: 3em";
+ main.appendChild(p);
+ return p;
+};
+const addTextToDivOnMain = () => {
+ const main = document.getElementById("main");
+ const prevDiv = document.getElementsByTagName("div")[0];
+ if (prevDiv) {
+ main.removeChild(prevDiv);
+ }
+ const div = document.createElement("div");
+ const text = document.createTextNode("Lorem Ipsum");
+ div.appendChild(text);
+ div.style = "font-size: 3em";
+ main.appendChild(div);
+}
+
+const waitOnPaintEntriesPromise = (expectLCP = true) => {
+ return new Promise((resolve, reject) => {
+ if (performance.softNavPaintMetricsSupported) {
+ 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});
+ } else if (expectLCP) {
+ new PerformanceObserver(list => {
+ resolve();
+ }).observe({
+ type: 'largest-contentful-paint',
+ includeSoftNavigationObservations: true
+ });
+ } else {
+ step_timeout(
+ () => requestAnimationFrame(() => requestAnimationFrame(resolve)),
+ 100);
+ }
+ });
+};