From 717291c4ecd219cb28a9e1f562f92e1829e9bc00 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 30 Jan 2026 15:06:56 +0100 Subject: [PATCH 1/6] Add failing e2e test --- .../router-styles-wrapper/render.php | 9 ++++ .../router-styles-wrapper/view.js | 9 ++++ .../specs/interactivity/router-styles.spec.ts | 53 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php index 6fe4675c81f063..29c17c42a215fc 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php @@ -74,6 +74,15 @@ function () { + $link ) : ?> + + (force) + + diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js index c352a0140c5e3e..5c6eb996a56065 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js @@ -19,6 +19,15 @@ const { state } = store( 'test/router-styles', { yield actions.navigate( e.target.href ); state.clientSideNavigation = true; } ), + navigateForce: withSyncEvent( function* ( e ) { + e.preventDefault(); + state.clientSideNavigation = false; + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( e.target.href, { force: true } ); + state.clientSideNavigation = true; + } ), *prefetch() { state.prefetching = true; const { ref } = getElement(); diff --git a/test/e2e/specs/interactivity/router-styles.spec.ts b/test/e2e/specs/interactivity/router-styles.spec.ts index 304d53a36a7b14..5c2ffcb2b52acf 100644 --- a/test/e2e/specs/interactivity/router-styles.spec.ts +++ b/test/e2e/specs/interactivity/router-styles.spec.ts @@ -446,6 +446,59 @@ test.describe( 'Router styles', () => { await expect( hideOnPrint ).toBeVisible(); } ); + test( 'should update styles when navigating to a cached page with force', async ( { + page, + request, + interactivityUtils: utils, + } ) => { + const csn = page.getByTestId( 'client-side navigation' ); + const red = page.getByTestId( 'red' ); + const green = page.getByTestId( 'green' ); + + // Navigate to "red" to cache the page and populate the + // internal style cache for the red page URL. + await page.getByTestId( 'link red' ).click(); + await expect( csn ).toBeHidden(); + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + + // Navigate to "green" so red styles are removed. + await page.getByTestId( 'link green' ).click(); + await expect( csn ).toBeHidden(); + await expect( csn ).toBeVisible(); + + // Intercept the next fetch to the red page URL and respond + // with the "all" page HTML instead. + const redLink = utils.getLink( 'red' ); + const allLink = utils.getLink( 'all' ); + await page.route( redLink, async ( route ) => { + // Fetch the "all" page HTML to simulate server-side content + // changes (e.g., new blocks appearing on the page). + const allPage = await request.fetch( allLink ); + const body = await allPage.body(); + return route.fulfill( { body, contentType: 'text/html' } ); + } ); + + // Force-navigate to "red". The response now contains all + // three color blocks with their styles, but the internal + // style cache still holds the old "red" entry, so the new + // green and blue styles are never processed. + await page.getByTestId( 'force link red' ).click(); + await expect( csn ).toBeHidden(); + await expect( csn ).toBeVisible(); + + // Red styles should be present (already in the style cache). + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + // Green styles from the new response should also be applied. + // This fails because the style cache is not invalidated on + // force navigations, so the fresh green styles are ignored. + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + + // Unroute previous route handler for "red". + await page.unroute( redLink ); + } ); + test( 'should ignore styles inside noscript elements during navigation', async ( { page, } ) => { From f8133c3e0883a57aedaa3faad4f70c6dfd81f325 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 30 Jan 2026 15:28:39 +0100 Subject: [PATCH 2/6] Remove redundant style cache --- .../interactivity-router/src/assets/styles.ts | 44 +++++-------------- packages/interactivity-router/src/index.ts | 2 +- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/packages/interactivity-router/src/assets/styles.ts b/packages/interactivity-router/src/assets/styles.ts index f0afcd4dbcedc6..c24b98d19e2c0d 100644 --- a/packages/interactivity-router/src/assets/styles.ts +++ b/packages/interactivity-router/src/assets/styles.ts @@ -195,16 +195,6 @@ const prepareStylePromise = ( return promise; }; -/** - * Cache of style promise lists per URL. - * - * It contains the list of style elements associated to the page with the - * passed URL. The original order is preserved to respect the CSS cascade. - * - * Each included promise resolves when the associated style element is ready. - */ -const styleSheetCache = new Map< string, Promise< StyleElement >[] >(); - /** * Prepares all style elements contained in the passed document. * @@ -218,32 +208,20 @@ const styleSheetCache = new Map< string, Promise< StyleElement >[] >(); * {@link applyStyles|`applyStyles`} function. * * @param doc Document instance. - * @param url URL for the passed document. * @return A list of promises for each style element in the passed document. */ -export const preloadStyles = ( - doc: Document, - url: string -): Promise< StyleElement >[] => { - if ( ! styleSheetCache.has( url ) ) { - const currentStyleElements = Array.from( - window.document.querySelectorAll< StyleElement >( - 'style,link[rel=stylesheet]' - ) - ); - const newStyleElements = Array.from( - doc.querySelectorAll< StyleElement >( 'style,link[rel=stylesheet]' ) - ); - - // Set styles in order. - const stylePromises = updateStylesWithSCS( - currentStyleElements, - newStyleElements - ); +export const preloadStyles = ( doc: Document ): Promise< StyleElement >[] => { + const currentStyleElements = Array.from( + window.document.querySelectorAll< StyleElement >( + 'style,link[rel=stylesheet]' + ) + ); + const newStyleElements = Array.from( + doc.querySelectorAll< StyleElement >( 'style,link[rel=stylesheet]' ) + ); - styleSheetCache.set( url, stylePromises ); - } - return styleSheetCache.get( url ); + // Set styles in order. + return updateStylesWithSCS( currentStyleElements, newStyleElements ); }; /** diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts index 19e589f83d2fe7..af7efcdaf29098 100644 --- a/packages/interactivity-router/src/index.ts +++ b/packages/interactivity-router/src/index.ts @@ -220,7 +220,7 @@ const preparePage: PreparePage = async ( url, dom, { vdom } = {} ) => { // Wait for styles and modules to be ready. const [ styles, scriptModules ] = await Promise.all( [ - Promise.all( preloadStyles( dom, url ) ), + Promise.all( preloadStyles( dom ) ), Promise.all( preloadScriptModules( dom ) ), ] ); From 33749dd59a5c6b61265843270aaedc85c199af0f Mon Sep 17 00:00:00 2001 From: David Date: Fri, 30 Jan 2026 15:35:11 +0100 Subject: [PATCH 3/6] Update e2e test comments. --- test/e2e/specs/interactivity/router-styles.spec.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/e2e/specs/interactivity/router-styles.spec.ts b/test/e2e/specs/interactivity/router-styles.spec.ts index 5c2ffcb2b52acf..77c35dadce1450 100644 --- a/test/e2e/specs/interactivity/router-styles.spec.ts +++ b/test/e2e/specs/interactivity/router-styles.spec.ts @@ -481,18 +481,13 @@ test.describe( 'Router styles', () => { } ); // Force-navigate to "red". The response now contains all - // three color blocks with their styles, but the internal - // style cache still holds the old "red" entry, so the new - // green and blue styles are never processed. + // three color blocks with their styles. await page.getByTestId( 'force link red' ).click(); await expect( csn ).toBeHidden(); await expect( csn ).toBeVisible(); - // Red styles should be present (already in the style cache). + // Red and green styles should be present. await expect( red ).toHaveCSS( 'color', COLOR_RED ); - // Green styles from the new response should also be applied. - // This fails because the style cache is not invalidated on - // force navigations, so the fresh green styles are ignored. await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); // Unroute previous route handler for "red". From ec6fb5b9cf6d5f8285652c9892df3b841fd3d85d Mon Sep 17 00:00:00 2001 From: David Date: Fri, 30 Jan 2026 18:07:12 +0100 Subject: [PATCH 4/6] Update unit tests --- .../src/assets/test/styles.test.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/interactivity-router/src/assets/test/styles.test.ts b/packages/interactivity-router/src/assets/test/styles.test.ts index 26e140d9de3ff6..fcc947a788343f 100644 --- a/packages/interactivity-router/src/assets/test/styles.test.ts +++ b/packages/interactivity-router/src/assets/test/styles.test.ts @@ -78,7 +78,7 @@ describe( 'Router styles management', () => { describe( 'updateStylesWithSCS', () => { it( 'should append all elements when X is empty in the correct order', () => { - const X = []; + const X: HTMLStyleElement[] = []; const Y = [ createStyleElement( 'style1' ), createLinkElement( 'link1' ), @@ -514,25 +514,22 @@ describe( 'Router styles management', () => { // Tests for preloadStyles function. describe( 'preloadStyles', () => { - it( 'should use cached promises for the same URL', () => { + it( 'should return cached promises for the same HTML', () => { // Create a test document. const doc = document.implementation.createHTMLDocument(); const style = doc.createElement( 'style' ); doc.head.appendChild( style ); + // NOTE: preloadStyles() modifies the passed document. That's why + // the document is cloned beforehand. + // First call should update the DOM. - const firstResult = preloadStyles( - doc, - 'https://example.com/test' - ); + const firstResult = preloadStyles( doc.cloneNode() as Document ); expect( firstResult ).toBeTruthy(); // Second call should return the same promises. - const secondResult = preloadStyles( - doc, - 'https://example.com/test' - ); - expect( secondResult ).toBe( firstResult ); + const secondResult = preloadStyles( doc.cloneNode() as Document ); + expect( secondResult ).toEqual( firstResult ); } ); it( 'should extract style elements from the provided document', () => { @@ -546,7 +543,7 @@ describe( 'Router styles management', () => { doc.head.appendChild( style1 ); doc.head.appendChild( style2 ); - preloadStyles( doc, 'https://example.com/another-test-page' ); + preloadStyles( doc ); // Check that styles were extracted and added to the document. const addedStyle1 = document.querySelector( '#test-style-1' ); From 221f057640e4264502bf3dd2ceea04e06d8ac141 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 30 Jan 2026 18:08:41 +0100 Subject: [PATCH 5/6] Add note to `preloadStyles` --- packages/interactivity-router/src/assets/styles.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/interactivity-router/src/assets/styles.ts b/packages/interactivity-router/src/assets/styles.ts index c24b98d19e2c0d..18d668dec5aefd 100644 --- a/packages/interactivity-router/src/assets/styles.ts +++ b/packages/interactivity-router/src/assets/styles.ts @@ -207,6 +207,9 @@ const prepareStylePromise = ( * make them effectively disabled until they are applied with the * {@link applyStyles|`applyStyles`} function. * + * Note that this function alters the passed document, as it can transfer + * nodes from it to the global document. + * * @param doc Document instance. * @return A list of promises for each style element in the passed document. */ From 26377f5448128e53893453a27d7d30fb4b0ad6d1 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 30 Jan 2026 18:10:44 +0100 Subject: [PATCH 6/6] Update changelog --- packages/interactivity-router/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/interactivity-router/CHANGELOG.md b/packages/interactivity-router/CHANGELOG.md index 46adb4bfa62bb9..d79a1e4f8046e5 100644 --- a/packages/interactivity-router/CHANGELOG.md +++ b/packages/interactivity-router/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Update cached styles for re-fetched pages. ([#75097](https://github.com/WordPress/gutenberg/pull/75097)) + ## 2.39.0 (2026-01-29) ### Bug Fixes