Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ function () {
<?php echo $label; ?>
</a>
<?php endforeach; ?>
<?php foreach ( $attributes['links'] as $label => $link ) : ?>
<a
data-testid="force link <?php echo $label; ?>"
data-wp-on--click="actions.navigateForce"
href="<?php echo $link; ?>"
>
<?php echo $label; ?> (force)
</a>
<?php endforeach; ?>
</nav>

<!-- HTML updated on navigation. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions packages/interactivity-router/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 14 additions & 33 deletions packages/interactivity-router/src/assets/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -217,33 +207,24 @@ const styleSheetCache = new Map< string, Promise< StyleElement >[] >();
* 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.
* @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 );
};

/**
Expand Down
21 changes: 9 additions & 12 deletions packages/interactivity-router/src/assets/test/styles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand Down Expand Up @@ -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.
Comment on lines +523 to +524
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior was present before. The note is just a clarification.

What has changed in preloadStyles() is the internal cache that makes the function return the same Array for the same URL. Now, it computes the styles list every time the function is executed, regardless of whether the URL is the same.

As mentioned, the document could be modified, so subsequent calls to preloadStyles() in the same document will return different results. That's because this function relocates style elements from the passed document to the global document.

This is the reason the document is cloned in the test.


// 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', () => {
Expand All @@ -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' );
Expand Down
2 changes: 1 addition & 1 deletion packages/interactivity-router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ),
] );

Expand Down
48 changes: 48 additions & 0 deletions test/e2e/specs/interactivity/router-styles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,54 @@ 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.
await page.getByTestId( 'force link red' ).click();
await expect( csn ).toBeHidden();
await expect( csn ).toBeVisible();

// Red and green styles should be present.
await expect( red ).toHaveCSS( 'color', COLOR_RED );
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,
} ) => {
Expand Down
Loading