From bbe5de8028c994d8ee5e22586269b79c63794079 Mon Sep 17 00:00:00 2001 From: Dmytro Molkov Date: Thu, 9 Oct 2025 14:37:48 -0700 Subject: [PATCH] Fix the case of lazy loading ttvc --- src/networkIdleObservable.ts | 90 +++++++++++++++++--------------- test/e2e/late-load/index.html | 33 ++++++++++++ test/e2e/late-load/index.spec.ts | 24 +++++++++ test/util/entries.ts | 2 +- 4 files changed, 106 insertions(+), 43 deletions(-) create mode 100644 test/e2e/late-load/index.html create mode 100644 test/e2e/late-load/index.spec.ts diff --git a/src/networkIdleObservable.ts b/src/networkIdleObservable.ts index bc43678..c069383 100644 --- a/src/networkIdleObservable.ts +++ b/src/networkIdleObservable.ts @@ -99,51 +99,57 @@ class ResourceLoadingIdleObservable { public didNetworkTimeOut = false; private cleanupTimeout?: number; // time out if resource never resolves + private registerResourceLoadListener = () => { + // watch for added or updated script tags + const o = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if ( + node instanceof HTMLScriptElement || + node instanceof HTMLLinkElement || + node instanceof HTMLImageElement || + node instanceof HTMLIFrameElement + ) { + this.add(node); + } else if (node.hasChildNodes() && node instanceof HTMLElement) { + // images may be mounted within large subtrees, this is less + // common with link/script elements + node.querySelectorAll('img').forEach(this.add); + } + }); + }); + }); + + // watch for new tags added anywhere in the document + o.observe(window.document.documentElement, {childList: true, subtree: true}); + + // as resources load, remove them from pendingResources + ['load', 'error'].forEach((eventType) => { + window.document.addEventListener( + eventType, + (event) => { + if ( + event.target instanceof HTMLScriptElement || + event.target instanceof HTMLLinkElement || + event.target instanceof HTMLImageElement || + event.target instanceof HTMLIFrameElement + ) { + this.remove(event.target); + } + }, + {capture: true} + ); + }); + }; + constructor() { // watch out for SSR if (typeof window !== 'undefined' && window?.MutationObserver) { - window.addEventListener('load', () => { - // watch for added or updated script tags - const o = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if ( - node instanceof HTMLScriptElement || - node instanceof HTMLLinkElement || - node instanceof HTMLImageElement || - node instanceof HTMLIFrameElement - ) { - this.add(node); - } else if (node.hasChildNodes() && node instanceof HTMLElement) { - // images may be mounted within large subtrees, this is less - // common with link/script elements - node.querySelectorAll('img').forEach(this.add); - } - }); - }); - }); - - // watch for new tags added anywhere in the document - o.observe(window.document.documentElement, {childList: true, subtree: true}); - - // as resources load, remove them from pendingResources - ['load', 'error'].forEach((eventType) => { - window.document.addEventListener( - eventType, - (event) => { - if ( - event.target instanceof HTMLScriptElement || - event.target instanceof HTMLLinkElement || - event.target instanceof HTMLImageElement || - event.target instanceof HTMLIFrameElement - ) { - this.remove(event.target); - } - }, - {capture: true} - ); - }); - }); + if (document.readyState === 'loading') { + window.addEventListener('load', this.registerResourceLoadListener, {once: true}); + } else { + this.registerResourceLoadListener(); + } } } diff --git a/test/e2e/late-load/index.html b/test/e2e/late-load/index.html new file mode 100644 index 0000000..3e2b421 --- /dev/null +++ b/test/e2e/late-load/index.html @@ -0,0 +1,33 @@ + + + + + + +

late load test

+ \ No newline at end of file diff --git a/test/e2e/late-load/index.spec.ts b/test/e2e/late-load/index.spec.ts new file mode 100644 index 0000000..f1540c2 --- /dev/null +++ b/test/e2e/late-load/index.spec.ts @@ -0,0 +1,24 @@ +import {expect, test} from '@playwright/test'; + +import {entryCountIs, getEntriesAndErrors} from '../../util/entries'; + +const IMG_DELAY = 500; + +test.describe('TTVC late load', () => { + test('library loaded after window.load should not miss resource tracking', async ({page}) => { + // Navigate to the page and wait for window load. + // On window load the page will dynamically import and initialize TTVC + // And after TTVC is initialized will add an img resource + // This tests that for SPAs where ttvc is loaded after window.load, resource tracking still works + await page.goto(`/test/late-load`, {waitUntil: 'load'}); + + // Wait for ttvc + await entryCountIs(page, 1, 3000); + + const {entries, errors} = await getEntriesAndErrors(page); + expect(entries.length).toBe(1); + expect(errors.length).toBe(0); + + expect(entries[0].duration).toBeGreaterThan(IMG_DELAY); + }); +}); \ No newline at end of file diff --git a/test/util/entries.ts b/test/util/entries.ts index 8c8d52b..9e9ff43 100644 --- a/test/util/entries.ts +++ b/test/util/entries.ts @@ -22,7 +22,7 @@ export const getEntriesAndErrors = (page: Page) => * Wait until at least {count} performance entries have been logged. */ export const entryCountIs = async (page: Page, count: number, timeout = 5000): Promise => { - await page.waitForFunction((count) => window.entries.length >= count, count, { + await page.waitForFunction((count) => window.entries && window.entries.length >= count, count, { polling: 500, timeout: timeout, });