From ac6ea501b12a40bd658411c7d7903f8597dc8165 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 24 Apr 2022 21:09:10 -0700 Subject: [PATCH] Add navigationType property to the Metric object --- README.md | 10 +- src/getCLS.ts | 2 +- src/getFCP.ts | 6 +- src/getFID.ts | 2 +- src/getLCP.ts | 2 +- src/getTTFB.ts | 39 ++------ src/lib/{onBFCacheRestore.ts => bfcache.ts} | 5 + src/lib/getNavigationEntry.ts | 42 ++++++++ src/lib/getVisibilityWatcher.ts | 2 +- src/lib/initMetric.ts | 9 +- src/types.ts | 18 ++++ test/e2e/getCLS-test.js | 28 +++++- test/e2e/getFCP-test.js | 6 ++ test/e2e/getFID-test.js | 12 ++- test/e2e/getLCP-test.js | 103 +++++++++++++++++++- test/e2e/getTTFB-test.js | 3 + 16 files changed, 239 insertions(+), 50 deletions(-) rename src/lib/{onBFCacheRestore.ts => bfcache.ts} (89%) create mode 100644 src/lib/getNavigationEntry.ts diff --git a/README.md b/README.md index deaba16f..41b1126f 100644 --- a/README.md +++ b/README.md @@ -201,12 +201,12 @@ Note that some of these metrics will not report until the user has interacted wi Also, in some cases a metric callback may never be called: - FID is not reported if the user never interacts with the page. -- FCP, FID, and LCP are not reported if the page was loaded in the background. +- CLS, FCP, FID, and LCP are not reported if the page was loaded in the background. In other cases, a metric callback may be called more than once: - CLS should be reported any time the [page's `visibilityState` changes to hidden](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#advice-hidden). -- CLS, FCP, FID, and LCP are reported again after a page is restored from the [back/forward cache](https://web.dev/bfcache/). +- All metrics are reported again (with the above exceptions) after a page is restored from the [back/forward cache](https://web.dev/bfcache/). _**Warning:** do not call any of the Web Vitals functions (e.g. `getCLS()`, `getFID()`, `getLCP()`) more than once per page load. Each of these functions creates a `PerformanceObserver` instance and registers event listeners for the lifetime of the page. While the overhead of calling these functions once is negligible, calling them repeatedly on the same page may eventually result in a memory leak._ @@ -554,6 +554,12 @@ interface Metric { // The array may also be empty if the metric value was not based on any // entries (e.g. a CLS value of 0 given no layout shifts). entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[]; + + // For regular navigations, the type will be the same as the type indicated + // by the Navigation Timing API (or `undefined` if the browser doesn't + // support that API). For pages that are restored from the bfcache, this + // value will be 'back_forward_cache'. + navigationType: NavigationType | 'back_forward_cache' | undefined; } ``` diff --git a/src/getCLS.ts b/src/getCLS.ts index 0ebcdf95..42abc8cd 100644 --- a/src/getCLS.ts +++ b/src/getCLS.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import {onBFCacheRestore} from './lib/bfcache.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; -import {onBFCacheRestore} from './lib/onBFCacheRestore.js'; import {bindReporter} from './lib/bindReporter.js'; import {getFCP} from './getFCP.js'; import {LayoutShift, Metric, ReportHandler} from './types.js'; diff --git a/src/getFCP.ts b/src/getFCP.ts index 9333fb4a..91e9cfb6 100644 --- a/src/getFCP.ts +++ b/src/getFCP.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {onBFCacheRestore} from './lib/onBFCacheRestore.js'; import {Metric, ReportHandler} from './types.js'; @@ -50,8 +50,8 @@ export const getFCP = (onReport: ReportHandler, reportAllChanges?: boolean) => { // https://github.com/GoogleChrome/web-vitals/issues/159 // The check for `window.performance` is needed to support Opera mini: // https://github.com/GoogleChrome/web-vitals/issues/185 - const fcpEntry = window.performance && performance.getEntriesByName && - performance.getEntriesByName('first-contentful-paint')[0]; + const fcpEntry = window.performance && window.performance.getEntriesByName && + window.performance.getEntriesByName('first-contentful-paint')[0]; const po = fcpEntry ? null : observe('paint', handleEntries); diff --git a/src/getFID.ts b/src/getFID.ts index 2694a0d5..c44e2dd3 100644 --- a/src/getFID.ts +++ b/src/getFID.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {onBFCacheRestore} from './lib/onBFCacheRestore.js'; import {onHidden} from './lib/onHidden.js'; import {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js'; import {FirstInputPolyfillCallback, Metric, PerformanceEventTiming, ReportHandler} from './types.js'; diff --git a/src/getLCP.ts b/src/getLCP.ts index 4b2de307..9f304cf5 100644 --- a/src/getLCP.ts +++ b/src/getLCP.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {onBFCacheRestore} from './lib/onBFCacheRestore.js'; import {onHidden} from './lib/onHidden.js'; import {Metric, ReportHandler} from './types.js'; diff --git a/src/getTTFB.ts b/src/getTTFB.ts index d8467762..147e3913 100644 --- a/src/getTTFB.ts +++ b/src/getTTFB.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import {bindReporter} from './lib/bindReporter.js'; import {initMetric} from './lib/initMetric.js'; -import {ReportHandler, NavigationTimingPolyfillEntry} from './types.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; +import {ReportHandler} from './types.js'; const afterLoad = (callback: () => void) => { @@ -28,36 +30,15 @@ const afterLoad = (callback: () => void) => { } } -const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => { - // Really annoying that TypeScript errors when using `PerformanceTiming`. - const timing = performance.timing; - - const navigationEntry: {[key: string]: number | string} = { - entryType: 'navigation', - startTime: 0, - }; - - for (const key in timing) { - if (key !== 'navigationStart' && key !== 'toJSON') { - navigationEntry[key] = Math.max( - (timing[key as keyof PerformanceTiming] as number) - - timing.navigationStart, 0); - } - } - return navigationEntry as unknown as NavigationTimingPolyfillEntry; -}; - -export const getTTFB = (onReport: ReportHandler) => { +export const getTTFB = (onReport: ReportHandler, reportAllChanges?: boolean) => { const metric = initMetric('TTFB'); + const report = bindReporter(onReport, metric, reportAllChanges); afterLoad(() => { - try { - // Use the NavigationTiming L2 entry if available. - const navigationEntry = performance.getEntriesByType('navigation')[0] || - getNavigationEntryFromPerformanceTiming(); + const navigationEntry = getNavigationEntry(); - metric.value = metric.delta = - (navigationEntry as PerformanceNavigationTiming).responseStart; + if (navigationEntry) { + metric.value = navigationEntry.responseStart; // In some cases the value reported is negative or is larger // than the current page time. Ignore these cases: @@ -67,9 +48,7 @@ export const getTTFB = (onReport: ReportHandler) => { metric.entries = [navigationEntry]; - onReport(metric); - } catch (error) { - // Do nothing. + report(true); } }); }; diff --git a/src/lib/onBFCacheRestore.ts b/src/lib/bfcache.ts similarity index 89% rename from src/lib/onBFCacheRestore.ts rename to src/lib/bfcache.ts index 94a723f7..86daf7a1 100644 --- a/src/lib/onBFCacheRestore.ts +++ b/src/lib/bfcache.ts @@ -18,9 +18,14 @@ interface onBFCacheRestoreCallback { (event: PageTransitionEvent): void; } +let isPersisted = false; + +export const isBFCacheRestore = () => isPersisted; + export const onBFCacheRestore = (cb: onBFCacheRestoreCallback) => { addEventListener('pageshow', (event) => { if (event.persisted) { + isPersisted = true; cb(event); } }, true); diff --git a/src/lib/getNavigationEntry.ts b/src/lib/getNavigationEntry.ts new file mode 100644 index 00000000..c6373da3 --- /dev/null +++ b/src/lib/getNavigationEntry.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NavigationTimingPolyfillEntry} from '../types.js'; + + +const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => { + const timing = performance.timing; + + const navigationEntry: {[key: string]: number | string} = { + entryType: 'navigation', + startTime: 0, + }; + + for (const key in timing) { + if (key !== 'navigationStart' && key !== 'toJSON') { + navigationEntry[key] = Math.max( + (timing[key as keyof PerformanceTiming] as number) - + timing.navigationStart, 0); + } + } + return navigationEntry as unknown as NavigationTimingPolyfillEntry; +}; + +export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => { + return window.performance && (performance.getEntriesByType && + performance.getEntriesByType('navigation')[0] || + getNavigationEntryFromPerformanceTiming()); +}; diff --git a/src/lib/getVisibilityWatcher.ts b/src/lib/getVisibilityWatcher.ts index fc61c858..7e1e4eff 100644 --- a/src/lib/getVisibilityWatcher.ts +++ b/src/lib/getVisibilityWatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {onBFCacheRestore} from './onBFCacheRestore.js'; +import {onBFCacheRestore} from './bfcache.js'; import {onHidden} from './onHidden.js'; let firstHiddenTime = -1; diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index bcc8e446..8ec77001 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -14,16 +14,21 @@ * limitations under the License. */ -import {Metric} from '../types.js'; +import {isBFCacheRestore} from './bfcache.js'; import {generateUniqueID} from './generateUniqueID.js'; +import {getNavigationEntry} from './getNavigationEntry.js'; +import {Metric} from '../types.js'; export const initMetric = (name: Metric['name'], value?: number): Metric => { + const navigationEntry = getNavigationEntry(); return { name, value: typeof value === 'undefined' ? -1 : value, delta: 0, entries: [], - id: generateUniqueID() + id: generateUniqueID(), + navigationType: isBFCacheRestore() ? 'back_forward_cache' : + navigationEntry && navigationEntry.type, }; }; diff --git a/src/types.ts b/src/types.ts index 12dafb24..8b025bdb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,12 +38,30 @@ export interface Metric { // The array may also be empty if the metric value was not based on any // entries (e.g. a CLS value of 0 given no layout shifts). entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[]; + + // For regular navigations, the type will be the same as the type indicated + // by the Navigation Timing API (or `undefined` if the browser doesn't + // support that API). For pages that are restored from the bfcache, this + // value will be 'back_forward_cache'. + navigationType: NavigationType | 'back_forward_cache' | undefined; } export interface ReportHandler { (metric: Metric): void; } + interface PerformanceEntryMap { + 'navigation': PerformanceNavigationTiming; + 'resource': PerformanceResourceTiming; + 'paint': PerformancePaintTiming; +} + +declare global { + interface Performance { + getEntriesByType(type: K): PerformanceEntryMap[K][] + } +} + // https://wicg.github.io/event-timing/#sec-performance-event-timing export interface PerformanceEventTiming extends PerformanceEntry { processingStart: DOMHighResTimeStamp; diff --git a/test/e2e/getCLS-test.js b/test/e2e/getCLS-test.js index bd041a87..7c940b7b 100644 --- a/test/e2e/getCLS-test.js +++ b/test/e2e/getCLS-test.js @@ -53,6 +53,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.name, 'CLS'); assert.strictEqual(cls.value, cls.delta); assert.strictEqual(cls.entries.length, 2); + assert.match(cls.navigationType, /navigate|reload/); }); it('reports the correct value on page unload after shifts (reportAllChanges === false)', async function() { @@ -72,6 +73,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.name, 'CLS'); assert.strictEqual(cls.value, cls.delta); assert.strictEqual(cls.entries.length, 2); + assert.match(cls.navigationType, /navigate|reload/); }); it('resets the session after timeout or gap elapses', async function() { @@ -93,6 +95,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls1.name, 'CLS'); assert.strictEqual(cls1.value, cls1.delta); assert.strictEqual(cls1.entries.length, 2); + assert.match(cls1.navigationType, /navigate|reload/); await browser.pause(1000); await stubVisibilityChange('visible'); @@ -118,7 +121,8 @@ describe('getCLS()', async function() { assert.strictEqual(cls2.name, 'CLS'); assert.strictEqual(cls2.value, cls1.value + cls2.delta); assert.strictEqual(cls2.entries.length, 2); - assert(cls2.id.match(/^v2-\d+-\d+$/)); + assert.match(cls2.navigationType, /navigate|reload/); + assert.match(cls2.id, /^v2-\d+-\d+$/); await browser.pause(1000); await stubVisibilityChange('visible'); @@ -149,7 +153,8 @@ describe('getCLS()', async function() { assert.strictEqual(cls3.name, 'CLS'); assert.strictEqual(cls3.value, cls2.value + cls3.delta); assert.strictEqual(cls3.entries.length, 4); - assert(cls3.id.match(/^v2-\d+-\d+$/)); + assert.match(cls3.navigationType, /navigate|reload/); + assert.match(cls3.id, /^v2-\d+-\d+$/); await browser.pause(1000); await stubVisibilityChange('visible'); @@ -207,12 +212,14 @@ describe('getCLS()', async function() { assert.strictEqual(cls1.name, 'CLS'); assert.strictEqual(cls1.value, cls1.delta); assert.strictEqual(cls1.entries.length, 1); + assert.match(cls1.navigationType, /navigate|reload/); assert(cls2.value >= cls1.value); assert.strictEqual(cls2.name, 'CLS'); assert.strictEqual(cls2.id, cls1.id); assert.strictEqual(cls2.value, cls1.value + cls2.delta); assert.strictEqual(cls2.entries.length, 2); + assert.match(cls2.navigationType, /navigate|reload/); await clearBeacons(); await stubVisibilityChange('hidden'); @@ -238,12 +245,14 @@ describe('getCLS()', async function() { assert(cls1.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(cls1.value, cls1.delta); assert.strictEqual(cls1.entries.length, 1); + assert.match(cls1.navigationType, /navigate|reload/); assert(cls2.value >= cls1.value); assert.strictEqual(cls2.name, 'CLS'); assert.strictEqual(cls2.id, cls1.id); assert.strictEqual(cls2.value, cls1.value + cls2.delta); assert.strictEqual(cls2.entries.length, 2); + assert.match(cls2.navigationType, /navigate|reload/); // Unload the page after no new shifts have occurred. await clearBeacons(); @@ -275,6 +284,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls1.name, 'CLS'); assert.strictEqual(cls1.value, cls1.delta); assert.strictEqual(cls1.entries.length, 2); + assert.match(cls1.navigationType, /navigate|reload/); await clearBeacons(); await stubVisibilityChange('visible'); @@ -295,6 +305,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls2.id, cls1.id); assert.strictEqual(cls2.value, cls1.value + cls2.delta); assert.strictEqual(cls2.entries.length, 3); + assert.match(cls2.navigationType, /navigate|reload/); }); it('continues reporting after visibilitychange (reportAllChanges === true)', async function() { @@ -316,6 +327,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls2.id, cls1.id); assert.strictEqual(cls2.value, cls1.value + cls2.delta); assert.strictEqual(cls2.entries.length, 2); + assert.match(cls2.navigationType, /navigate|reload/); // Unload the page after no new shifts have occurred. await clearBeacons(); @@ -335,6 +347,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls3.id, cls2.id); assert.strictEqual(cls3.value, cls2.value + cls3.delta); assert.strictEqual(cls3.entries.length, 3); + assert.match(cls3.navigationType, /navigate|reload/); }); it('continues reporting after bfcache restore (reportAllChanges === false)', async function() { @@ -356,6 +369,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls1.name, 'CLS'); assert.strictEqual(cls1.value, cls1.delta); assert.strictEqual(cls1.entries.length, 2); + assert.match(cls1.navigationType, /navigate|reload/); await clearBeacons(); await triggerLayoutShift(); @@ -373,6 +387,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls2.name, 'CLS'); assert.strictEqual(cls2.value, cls2.delta); assert.strictEqual(cls2.entries.length, 1); + assert.strictEqual(cls2.navigationType, 'back_forward_cache'); await clearBeacons(); await triggerLayoutShift(); @@ -390,6 +405,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls3.name, 'CLS'); assert.strictEqual(cls3.value, cls3.delta); assert.strictEqual(cls3.entries.length, 1); + assert.strictEqual(cls3.navigationType, 'back_forward_cache'); }); it('continues reporting after bfcache restore (reportAllChanges === true)', async function() { @@ -405,12 +421,14 @@ describe('getCLS()', async function() { assert.strictEqual(cls1.name, 'CLS'); assert.strictEqual(cls1.value, cls1.delta); assert.strictEqual(cls1.entries.length, 1); + assert.match(cls1.navigationType, /navigate|reload/); assert(cls2.value > cls1.value); assert.strictEqual(cls2.name, 'CLS'); assert.strictEqual(cls2.id, cls1.id); assert.strictEqual(cls2.value, cls1.value + cls2.delta); assert.strictEqual(cls2.entries.length, 2); + assert.match(cls2.navigationType, /navigate|reload/); await clearBeacons(); await stubForwardBack(); @@ -429,6 +447,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls3.name, 'CLS'); assert.strictEqual(cls3.value, cls3.delta); assert.strictEqual(cls3.entries.length, 1); + assert.strictEqual(cls3.navigationType, 'back_forward_cache'); }); it('reports zero if no layout shifts occurred on first visibility hidden (reportAllChanges === false)', async function() { @@ -448,6 +467,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.value, 0); assert.strictEqual(cls.delta, 0); assert.strictEqual(cls.entries.length, 0); + assert.match(cls.navigationType, /navigate|reload/); }); it('reports zero if no layout shifts occurred on first visibility hidden (reportAllChanges === true)', async function() { @@ -467,6 +487,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.value, 0); assert.strictEqual(cls.delta, 0); assert.strictEqual(cls.entries.length, 0); + assert.match(cls.navigationType, /navigate|reload/); }); it('reports zero if no layout shifts occurred on page unload (reportAllChanges === false)', async function() { @@ -486,6 +507,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.value, 0); assert.strictEqual(cls.delta, 0); assert.strictEqual(cls.entries.length, 0); + assert.match(cls.navigationType, /navigate|reload/); }); it('reports zero if no layout shifts occurred on page unload (reportAllChanges === true)', async function() { @@ -505,6 +527,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.value, 0); assert.strictEqual(cls.delta, 0); assert.strictEqual(cls.entries.length, 0); + assert.match(cls.navigationType, /navigate|reload/); }); it('does not report if the document was hidden at page load time', async function() { @@ -541,6 +564,7 @@ describe('getCLS()', async function() { assert.strictEqual(cls.name, 'CLS'); assert.strictEqual(cls.delta, cls.value); assert.strictEqual(cls.entries.length, 1); + assert.strictEqual(cls.navigationType, 'back_forward_cache'); }); }); diff --git a/test/e2e/getFCP-test.js b/test/e2e/getFCP-test.js index cb463703..e225125a 100644 --- a/test/e2e/getFCP-test.js +++ b/test/e2e/getFCP-test.js @@ -46,6 +46,7 @@ describe('getFCP()', async function() { assert.strictEqual(fcp.name, 'FCP'); assert.strictEqual(fcp.value, fcp.delta); assert.strictEqual(fcp.entries.length, 1); + assert.match(fcp.navigationType, /navigate|reload/); }); it('does not report if the browser does not support FCP (including bfcache restores)', async function() { @@ -111,6 +112,7 @@ describe('getFCP()', async function() { assert.strictEqual(fcp1.name, 'FCP'); assert.strictEqual(fcp1.value, fcp1.delta); assert.strictEqual(fcp1.entries.length, 1); + assert.match(fcp1.navigationType, /navigate|reload/); await clearBeacons(); await stubForwardBack(); @@ -124,6 +126,7 @@ describe('getFCP()', async function() { assert.strictEqual(fcp2.name, 'FCP'); assert.strictEqual(fcp2.value, fcp2.delta); assert.strictEqual(fcp2.entries.length, 0); + assert.strictEqual(fcp2.navigationType, 'back_forward_cache'); await clearBeacons(); await stubForwardBack(); @@ -137,6 +140,7 @@ describe('getFCP()', async function() { assert.strictEqual(fcp3.name, 'FCP'); assert.strictEqual(fcp3.value, fcp3.delta); assert.strictEqual(fcp3.entries.length, 0); + assert.strictEqual(fcp3.navigationType, 'back_forward_cache'); }); it('reports if the page is restored from bfcache even when the document was hidden at page load time', async function() { @@ -162,6 +166,7 @@ describe('getFCP()', async function() { assert.strictEqual(fcp1.name, 'FCP'); assert.strictEqual(fcp1.value, fcp1.delta); assert.strictEqual(fcp1.entries.length, 0); + assert.strictEqual(fcp1.navigationType, 'back_forward_cache'); await clearBeacons(); await stubForwardBack(); @@ -175,5 +180,6 @@ describe('getFCP()', async function() { assert.strictEqual(fcp2.name, 'FCP'); assert.strictEqual(fcp2.value, fcp2.delta); assert.strictEqual(fcp2.entries.length, 0); + assert.strictEqual(fcp2.navigationType, 'back_forward_cache'); }); }); diff --git a/test/e2e/getFID-test.js b/test/e2e/getFID-test.js index 127b0ff8..7d286248 100644 --- a/test/e2e/getFID-test.js +++ b/test/e2e/getFID-test.js @@ -50,7 +50,8 @@ describe('getFID()', async function() { assert(fid.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(fid.name, 'FID'); assert.strictEqual(fid.value, fid.delta); - assert(fid.entries[0].name.match(/(mouse|pointer)down/)); + assert.match(fid.navigationType, /navigate|reload/); + assert.match(fid.entries[0].name, /(mouse|pointer)down/); }); it('does not report if the browser does not support FID and the polyfill is not used', async function() { @@ -100,7 +101,8 @@ describe('getFID()', async function() { assert(fid.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(fid.name, 'FID'); assert.strictEqual(fid.value, fid.delta); - assert(fid.entries[0].name.match(/(mouse|pointer)down/)); + assert.match(fid.navigationType, /navigate|reload/); + assert.match(fid.entries[0].name, /(mouse|pointer)down/); if (browserSupportsFID) { assert('duration' in fid.entries[0]); } else { @@ -165,7 +167,8 @@ describe('getFID()', async function() { assert(fid1.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(fid1.name, 'FID'); assert.strictEqual(fid1.value, fid1.delta); - assert(fid1.entries[0].name.match(/(mouse|pointer)down/)); + assert.match(fid1.navigationType, /navigate|reload/); + assert.match(fid1.entries[0].name, /(mouse|pointer)down/); await clearBeacons(); await stubForwardBack(); @@ -181,7 +184,8 @@ describe('getFID()', async function() { assert(fid1.id !== fid2.id); assert.strictEqual(fid2.name, 'FID'); assert.strictEqual(fid2.value, fid2.delta); - assert(fid2.entries[0].name.match(/(mouse|pointer)down/)); + assert.strictEqual(fid2.navigationType, 'back_forward_cache'); + assert.match(fid2.entries[0].name, /(mouse|pointer)down/); }); }); diff --git a/test/e2e/getLCP-test.js b/test/e2e/getLCP-test.js index 8d876101..cf26e084 100644 --- a/test/e2e/getLCP-test.js +++ b/test/e2e/getLCP-test.js @@ -18,6 +18,7 @@ const assert = require('assert'); const {beaconCountIs, clearBeacons, getBeacons} = require('../utils/beacons.js'); const {browserSupportsEntry} = require('../utils/browserSupportsEntry.js'); const {imagesPainted} = require('../utils/imagesPainted.js'); +const {stubForwardBack} = require('../utils/stubForwardBack.js'); const {stubVisibilityChange} = require('../utils/stubVisibilityChange.js'); @@ -100,7 +101,7 @@ describe('getLCP()', async function() { assertFullReportsAreCorrect(await getBeacons()); }); - it('does not report if the browser does not support LCP', async function() { + it('does not report if the browser does not support LCP (including bfcache restores)', async function() { if (browserSupportsLCP) this.skip(); await browser.url('/test/lcp'); @@ -122,8 +123,15 @@ describe('getLCP()', async function() { // Wait a bit to ensure no beacons were sent. await browser.pause(1000); - const beacons = await getBeacons(); - assert.strictEqual(beacons.length, 0); + assert.strictEqual((await getBeacons()).length, 0); + + await clearBeacons(); + await stubForwardBack(); + + // Wait a bit to ensure no beacons were sent. + await browser.pause(1000); + + assert.strictEqual((await getBeacons()).length, 0); }); it('does not report if the document was hidden at page load time', async function() { @@ -194,6 +202,7 @@ describe('getLCP()', async function() { assert.strictEqual(lcp1.value, lcp1.delta); assert.strictEqual(lcp1.entries.length, 1); assert.strictEqual(lcp1.entries[0].element, 'h1'); + assert.match(lcp1.navigationType, /navigate|reload/); }); it('stops reporting after the document changes to hidden (reportAllChanges === true)', async function() { @@ -209,6 +218,7 @@ describe('getLCP()', async function() { assert.strictEqual(lcp.value, lcp.delta); assert.strictEqual(lcp.entries.length, 1); assert.strictEqual(lcp.entries[0].element, 'h1'); + assert.match(lcp.navigationType, /navigate|reload/); await clearBeacons(); await stubVisibilityChange('hidden'); @@ -224,6 +234,90 @@ describe('getLCP()', async function() { const beacons = await getBeacons(); assert.strictEqual(beacons.length, 0); }); + + it('reports if the page is restored from bfcache', async function() { + if (!browserSupportsLCP) this.skip(); + + await browser.url('/test/lcp'); + + // Wait until all images are loaded and fully rendered. + await imagesPainted(); + + const h1 = await $('h1'); + await h1.click(); + await beaconCountIs(1); + + assertStandardReportsAreCorrect(await getBeacons()); + await clearBeacons(); + + await stubForwardBack(); + await beaconCountIs(1); + + const [lcp2] = await getBeacons(); + + assert(lcp2.value > 0); // Greater than the image load delay. + assert(lcp2.id.match(/^v2-\d+-\d+$/)); + assert.strictEqual(lcp2.name, 'LCP'); + assert.strictEqual(lcp2.value, lcp2.delta); + assert.strictEqual(lcp2.entries.length, 0); + assert.strictEqual(lcp2.navigationType, 'back_forward_cache'); + + await clearBeacons(); + await stubForwardBack(); + await beaconCountIs(1); + + const [lcp3] = await getBeacons(); + + assert(lcp3.value > 0); // Greater than the image load delay. + assert(lcp3.id.match(/^v2-\d+-\d+$/)); + assert.strictEqual(lcp3.name, 'LCP'); + assert.strictEqual(lcp3.value, lcp3.delta); + assert.strictEqual(lcp3.entries.length, 0); + assert.strictEqual(lcp3.navigationType, 'back_forward_cache'); + }); + + it('reports if the page is restored from bfcache even when the document was hidden at page load time', async function() { + if (!browserSupportsLCP) this.skip(); + + await browser.url('/test/lcp?hidden=1'); + + await stubVisibilityChange('visible'); + + // Click on the h1. + const h1 = await $('h1'); + await h1.click(); + + // Wait a bit to ensure no beacons were sent. + await browser.pause(1000); + + const beacons = await getBeacons(); + assert.strictEqual(beacons.length, 0); + + await stubForwardBack(); + await beaconCountIs(1); + + const [lcp2] = await getBeacons(); + + assert(lcp2.value > 0); // Greater than the image load delay. + assert(lcp2.id.match(/^v2-\d+-\d+$/)); + assert.strictEqual(lcp2.name, 'LCP'); + assert.strictEqual(lcp2.value, lcp2.delta); + assert.strictEqual(lcp2.entries.length, 0); + assert.strictEqual(lcp2.navigationType, 'back_forward_cache'); + + await clearBeacons(); + await stubForwardBack(); + await beaconCountIs(1); + + const [lcp3] = await getBeacons(); + + assert(lcp3.value > 0); // Greater than the image load delay. + assert(lcp3.id.match(/^v2-\d+-\d+$/)); + assert.strictEqual(lcp3.name, 'LCP'); + assert.strictEqual(lcp3.value, lcp3.delta); + assert.strictEqual(lcp3.entries.length, 0); + assert.strictEqual(lcp3.navigationType, 'back_forward_cache'); + }); }); const assertStandardReportsAreCorrect = (beacons) => { @@ -234,6 +328,7 @@ const assertStandardReportsAreCorrect = (beacons) => { assert.strictEqual(lcp.name, 'LCP'); assert.strictEqual(lcp.value, lcp.delta); assert.strictEqual(lcp.entries.length, 1); + assert.match(lcp.navigationType, /navigate|reload/); }; const assertFullReportsAreCorrect = (beacons) => { @@ -244,6 +339,7 @@ const assertFullReportsAreCorrect = (beacons) => { assert.strictEqual(lcp1.name, 'LCP'); assert.strictEqual(lcp1.value, lcp1.delta); assert.strictEqual(lcp1.entries.length, 1); + assert.match(lcp1.navigationType, /navigate|reload/); assert(lcp2.value > 500); // Greater than the image load delay. assert.strictEqual(lcp2.value, lcp1.value + lcp2.delta); @@ -251,4 +347,5 @@ const assertFullReportsAreCorrect = (beacons) => { assert.strictEqual(lcp2.id, lcp1.id); assert.strictEqual(lcp2.entries.length, 1); assert(lcp2.entries[0].startTime > lcp1.entries[0].startTime); + assert.match(lcp2.navigationType, /navigate|reload/); }; diff --git a/test/e2e/getTTFB-test.js b/test/e2e/getTTFB-test.js index 71156014..03f5b98c 100644 --- a/test/e2e/getTTFB-test.js +++ b/test/e2e/getTTFB-test.js @@ -85,6 +85,7 @@ describe('getTTFB()', async function() { assert(ttfb.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(ttfb.name, 'TTFB'); assert.strictEqual(ttfb.value, ttfb.delta); + assert.strictEqual(ttfb.navigationType, 'navigate'); assert.strictEqual(ttfb.entries.length, 1); assertValidEntry(ttfb.entries[0]); @@ -106,6 +107,7 @@ describe('getTTFB()', async function() { assert(ttfb.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(ttfb.name, 'TTFB'); assert.strictEqual(ttfb.value, ttfb.delta); + assert.strictEqual(ttfb.navigationType, 'navigate'); assert.strictEqual(ttfb.entries.length, 1); assertValidEntry(ttfb.entries[0]); @@ -127,6 +129,7 @@ describe('getTTFB()', async function() { assert(ttfb.id.match(/^v2-\d+-\d+$/)); assert.strictEqual(ttfb.name, 'TTFB'); assert.strictEqual(ttfb.value, ttfb.delta); + assert.strictEqual(ttfb.navigationType, 'navigate'); assert.strictEqual(ttfb.entries.length, 1); assertValidEntry(ttfb.entries[0]);