diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png
new file mode 100644
index 00000000000000..c4f8e7c5146d36
Binary files /dev/null and b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png differ
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json
new file mode 100644
index 00000000000000..644ea70f74dca1
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "test/router-styles-blue",
+ "title": "E2E Interactivity tests - router styles - Blue",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewStyle": "file:./style.css",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php
new file mode 100644
index 00000000000000..3f5da308db092a
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php
@@ -0,0 +1,35 @@
+ 'blue-block' )
+);
+?>
+
>Blue
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css
new file mode 100644
index 00000000000000..f55f12f4d594cf
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css
@@ -0,0 +1,7 @@
+.blue-from-link {
+ color: rgb(0, 0, 255);
+}
+
+.background-from-link {
+ background-image: url('./assets/10x10_e2e_test_image_blue.png');
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css
new file mode 100644
index 00000000000000..84d891e90242a5
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css
@@ -0,0 +1,4 @@
+.wp-block-test-router-styles-blue,
+.blue {
+ color: rgb(0, 0, 255);
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png
new file mode 100644
index 00000000000000..34ec87925d8c50
Binary files /dev/null and b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png differ
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json
new file mode 100644
index 00000000000000..e2edda625571b9
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "test/router-styles-green",
+ "title": "E2E Interactivity tests - router styles - Green",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewStyle": "file:./style.css",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php
new file mode 100644
index 00000000000000..4418a2d3ab0f3d
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php
@@ -0,0 +1,35 @@
+ 'green-block' )
+);
+?>
+>Green
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css
new file mode 100644
index 00000000000000..b3d7d7b111e52a
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css
@@ -0,0 +1,7 @@
+.green-from-link {
+ color: rgb(0, 255, 0);
+}
+
+.background-from-link {
+ background-image: url('./assets/10x10_e2e_test_image_green.png');
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css
new file mode 100644
index 00000000000000..0c457588f625cb
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css
@@ -0,0 +1,4 @@
+.wp-block-test-router-styles-green,
+.green {
+ color: rgb(0, 255, 0);
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png
new file mode 100644
index 00000000000000..3264bf6427c276
Binary files /dev/null and b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png differ
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json
new file mode 100644
index 00000000000000..582d7019062c6e
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "test/router-styles-red",
+ "title": "E2E Interactivity tests - router styles - Red",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewStyle": "file:./style.css",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php
new file mode 100644
index 00000000000000..e8474cf69b825a
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php
@@ -0,0 +1,35 @@
+ 'red-block' )
+);
+?>
+>Red
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css
new file mode 100644
index 00000000000000..0f7d6228079897
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css
@@ -0,0 +1,7 @@
+.red-from-link {
+ color: rgb(255, 0, 0);
+}
+
+.background-from-link {
+ background-image: url('./assets/10x10_e2e_test_image_red.png');
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css
new file mode 100644
index 00000000000000..eac7e3af16e0b5
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css
@@ -0,0 +1,4 @@
+.wp-block-test-router-styles-red,
+.red {
+ color: rgb(255, 0, 0);
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json
new file mode 100644
index 00000000000000..a1a95b4c81e3b6
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "test/router-styles-wrapper",
+ "title": "E2E Interactivity tests - router styles - Wrapper",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewScriptModule": "file:./view.js",
+ "viewStyle": "file:./style.css",
+ "render": "file:./render.php"
+}
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
new file mode 100644
index 00000000000000..6373e8e9bc235b
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php
@@ -0,0 +1,70 @@
+
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Client-side navigation
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css
new file mode 100644
index 00000000000000..12773560c4180f
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css
@@ -0,0 +1,3 @@
+.wp-block-test-router-styles-wrapper {
+ color: rgb(160, 12, 60);
+}
\ No newline at end of file
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php
new file mode 100644
index 00000000000000..bdaec8d1b67a9d
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php
@@ -0,0 +1,9 @@
+ array(
+ '@wordpress/interactivity',
+ array(
+ 'id' => '@wordpress/interactivity-router',
+ 'import' => 'dynamic',
+ ),
+ ),
+);
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
new file mode 100644
index 00000000000000..5b3b42f2b413e4
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js
@@ -0,0 +1,20 @@
+/**
+ * WordPress dependencies
+ */
+import { store } from '@wordpress/interactivity';
+
+const { state } = store( 'test/router-styles', {
+ state: {
+ clientSideNavigation: false,
+ },
+ actions: {
+ *navigate( e ) {
+ e.preventDefault();
+ const { actions } = yield import(
+ '@wordpress/interactivity-router'
+ );
+ yield actions.navigate( e.target.href );
+ state.clientSideNavigation = true;
+ },
+ },
+} );
diff --git a/packages/interactivity-router/src/assets/styles.ts b/packages/interactivity-router/src/assets/styles.ts
new file mode 100644
index 00000000000000..ddb41eabc7a758
--- /dev/null
+++ b/packages/interactivity-router/src/assets/styles.ts
@@ -0,0 +1,79 @@
+const cssUrlRegEx =
+ /url\(\s*(?:(["'])((?:\\.|[^\n\\"'])+)\1|((?:\\.|[^\s,"'()\\])+))\s*\)/g;
+
+const resolveUrl = ( relativeUrl: string, baseUrl: string ) => {
+ try {
+ return new URL( relativeUrl, baseUrl ).toString();
+ } catch ( e ) {
+ return relativeUrl;
+ }
+};
+
+const withAbsoluteUrls = ( cssText: string, baseUrl: string ) =>
+ cssText.replace(
+ cssUrlRegEx,
+ ( _match, quotes = '', relUrl1, relUrl2 ) =>
+ `url(${ quotes }${ resolveUrl(
+ relUrl1 || relUrl2,
+ baseUrl
+ ) }${ quotes })`
+ );
+
+const styleSheetCache = new Map< string, Promise< CSSStyleSheet > >();
+
+const getCachedSheet = async (
+ sheetId: string,
+ factory: () => Promise< CSSStyleSheet >
+) => {
+ if ( ! styleSheetCache.has( sheetId ) ) {
+ styleSheetCache.set( sheetId, factory() );
+ }
+ return styleSheetCache.get( sheetId );
+};
+
+const sheetFromLink = async (
+ { id, href, sheet: elementSheet }: HTMLLinkElement,
+ baseUrl: string
+) => {
+ const sheetId = id || href;
+ const sheetUrl = resolveUrl( href, baseUrl );
+
+ if ( elementSheet ) {
+ return getCachedSheet( sheetId, () => {
+ const sheet = new CSSStyleSheet();
+ for ( const { cssText } of elementSheet.cssRules ) {
+ sheet.insertRule( withAbsoluteUrls( cssText, sheetUrl ) );
+ }
+ return Promise.resolve( sheet );
+ } );
+ }
+ return getCachedSheet( sheetId, async () => {
+ const response = await fetch( href );
+ const text = await response.text();
+ const sheet = new CSSStyleSheet();
+ await sheet.replace( withAbsoluteUrls( text, sheetUrl ) );
+ return sheet;
+ } );
+};
+
+const sheetFromStyle = async ( { textContent }: HTMLStyleElement ) => {
+ const sheetId = textContent;
+ return getCachedSheet( sheetId, async () => {
+ const sheet = new CSSStyleSheet();
+ await sheet.replace( textContent );
+ return sheet;
+ } );
+};
+
+export const generateCSSStyleSheets = (
+ doc: Document,
+ baseUrl: string = ( doc.location || window.location ).href
+): Promise< CSSStyleSheet >[] =>
+ [ ...doc.querySelectorAll( 'style,link[rel=stylesheet]' ) ].map(
+ ( element ) => {
+ if ( 'LINK' === element.nodeName ) {
+ return sheetFromLink( element as HTMLLinkElement, baseUrl );
+ }
+ return sheetFromStyle( element as HTMLStyleElement );
+ }
+ );
diff --git a/packages/interactivity-router/src/head.ts b/packages/interactivity-router/src/head.ts
deleted file mode 100644
index 69139348b582ff..00000000000000
--- a/packages/interactivity-router/src/head.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * The cache of prefetched stylesheets and scripts.
- */
-export const headElements = new Map<
- string,
- { tag: HTMLElement; text?: string }
->();
-
-/**
- * Helper to update only the necessary tags in the head.
- *
- * @async
- * @param newHead The head elements of the new page.
- */
-export const updateHead = async ( newHead: HTMLHeadElement[] ) => {
- // Helper to get the tag id store in the cache.
- const getTagId = ( tag: Element ) => tag.id || tag.outerHTML;
-
- // Map incoming head tags by their content.
- const newHeadMap = new Map< string, Element >();
- for ( const child of newHead ) {
- newHeadMap.set( getTagId( child ), child );
- }
-
- const toRemove: Element[] = [];
-
- // Detect nodes that should be added or removed.
- for ( const child of document.head.children ) {
- const id = getTagId( child );
- // Always remove styles and links as they might change.
- if ( child.nodeName === 'LINK' || child.nodeName === 'STYLE' ) {
- toRemove.push( child );
- } else if ( newHeadMap.has( id ) ) {
- newHeadMap.delete( id );
- } else if ( child.nodeName !== 'SCRIPT' && child.nodeName !== 'META' ) {
- toRemove.push( child );
- }
- }
-
- await Promise.all(
- [ ...headElements.entries() ]
- .filter( ( [ , { tag } ] ) => tag.nodeName === 'SCRIPT' )
- .map( async ( [ url ] ) => {
- await import( /* webpackIgnore: true */ url );
- } )
- );
-
- // Prepare new assets.
- const toAppend = [ ...newHeadMap.values() ];
-
- // Apply the changes.
- toRemove.forEach( ( n ) => n.remove() );
- document.head.append( ...toAppend );
-};
-
-/**
- * Fetches and processes head assets (stylesheets and scripts) from a specified document.
- *
- * @async
- * @param doc The document from which to fetch head assets. It should support standard DOM querying methods.
- *
- * @return Returns an array of HTML elements representing the head assets.
- */
-export const fetchHeadAssets = async (
- doc: Document
-): Promise< HTMLElement[] > => {
- const headTags = [];
-
- // We only want to fetch module scripts because regular scripts (without
- // `async` or `defer` attributes) can depend on the execution of other scripts.
- // Scripts found in the head are blocking and must be executed in order.
- const scripts = doc.querySelectorAll< HTMLScriptElement >(
- 'script[type="module"][src]'
- );
-
- scripts.forEach( ( script ) => {
- const src = script.getAttribute( 'src' );
- if ( ! headElements.has( src ) ) {
- // add the elements to prefetch the module scripts
- const link = doc.createElement( 'link' );
- link.rel = 'modulepreload';
- link.href = src;
- document.head.append( link );
- headElements.set( src, { tag: script } );
- }
- } );
-
- const stylesheets = doc.querySelectorAll< HTMLLinkElement >(
- 'link[rel=stylesheet]'
- );
-
- await Promise.all(
- Array.from( stylesheets ).map( async ( tag ) => {
- const href = tag.getAttribute( 'href' );
- if ( ! href ) {
- return;
- }
-
- if ( ! headElements.has( href ) ) {
- try {
- const response = await fetch( href );
- const text = await response.text();
- headElements.set( href, {
- tag,
- text,
- } );
- } catch ( e ) {
- // eslint-disable-next-line no-console
- console.error( e );
- }
- }
-
- const headElement = headElements.get( href );
- const styleElement = doc.createElement( 'style' );
- styleElement.textContent = headElement.text;
-
- headTags.push( styleElement );
- } )
- );
-
- return [
- doc.querySelector( 'title' ),
- ...doc.querySelectorAll( 'style' ),
- ...headTags,
- ];
-};
diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts
index 0c10e896ce1ef5..ded21d35dd5886 100644
--- a/packages/interactivity-router/src/index.ts
+++ b/packages/interactivity-router/src/index.ts
@@ -6,7 +6,7 @@ import { store, privateApis, getConfig } from '@wordpress/interactivity';
/**
* Internal dependencies
*/
-import { fetchHeadAssets, updateHead, headElements } from './head';
+import { generateCSSStyleSheets } from './assets/styles';
const {
directivePrefix,
@@ -37,16 +37,18 @@ interface PrefetchOptions {
interface VdomParams {
vdom?: typeof initialVdom;
+ baseUrl?: string;
}
interface Page {
regions: Record< string, any >;
- head: HTMLHeadElement[];
+ styles: Promise< CSSStyleSheet >[];
+ scriptModules: string[];
title: string;
initialData: any;
}
-type RegionsToVdom = ( dom: Document, params?: VdomParams ) => Promise< Page >;
+type RegionsToVdom = ( dom: Document, params?: VdomParams ) => Page;
// Check if the navigation mode is full page or region based.
const navigationMode: 'regionBased' | 'fullPage' =
@@ -73,7 +75,7 @@ const fetchPage = async ( url: string, { html }: { html: string } ) => {
html = await res.text();
}
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
- return regionsToVdom( dom );
+ return regionsToVdom( dom, { baseUrl: url } );
} catch ( e ) {
return false;
}
@@ -81,12 +83,17 @@ const fetchPage = async ( url: string, { html }: { html: string } ) => {
// Return an object with VDOM trees of those HTML regions marked with a
// `router-region` directive.
-const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => {
+const regionsToVdom: RegionsToVdom = ( dom, { vdom, baseUrl } = {} ) => {
const regions = { body: undefined };
- let head: HTMLElement[];
+ const styles = generateCSSStyleSheets( dom, baseUrl );
+ const scriptModules = [
+ ...dom.querySelectorAll< HTMLScriptElement >(
+ 'script[type=module][src]'
+ ),
+ ].map( ( s ) => s.src );
+
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( navigationMode === 'fullPage' ) {
- head = await fetchHeadAssets( dom );
regions.body = vdom
? vdom.get( document.body )
: toVdom( dom.body );
@@ -103,15 +110,28 @@ const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => {
}
const title = dom.querySelector( 'title' )?.innerText;
const initialData = parseServerData( dom );
- return { regions, head, title, initialData };
+ return { regions, styles, scriptModules, title, initialData };
};
// Render all interactive regions contained in the given page.
const renderRegions = async ( page: Page ) => {
+ // Wait for styles and modules to be ready.
+ await Promise.all( [
+ ...page.styles,
+ ...page.scriptModules.map(
+ ( src ) => import( /* webpackIgnore: true */ src )
+ ),
+ ] );
+ // Replace style sheets.
+ const sheets = await Promise.all( page.styles );
+ window.document
+ .querySelectorAll( 'style,link[rel=stylesheet]' )
+ .forEach( ( element ) => element.remove() );
+ window.document.adoptedStyleSheets = sheets;
+
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( navigationMode === 'fullPage' ) {
- // Once this code is tested and more mature, the head should be updated for region based navigation as well.
- await updateHead( page.head );
+ // Update HTML.
const fragment = getRegionRootFragment( document.body );
batch( () => {
populateServerData( page.initialData );
@@ -169,23 +189,14 @@ window.addEventListener( 'popstate', async () => {
// Initialize the router and cache the initial page using the initial vDOM.
// Once this code is tested and more mature, the head should be updated for
// region based navigation as well.
-if ( globalThis.IS_GUTENBERG_PLUGIN ) {
- if ( navigationMode === 'fullPage' ) {
- // Cache the scripts. Has to be called before fetching the assets.
- [].map.call(
- document.querySelectorAll( 'script[type="module"][src]' ),
- ( script ) => {
- headElements.set( script.getAttribute( 'src' ), {
- tag: script,
- } );
- }
- );
- await fetchHeadAssets( document );
- }
-}
pages.set(
getPagePath( window.location.href ),
- Promise.resolve( regionsToVdom( document, { vdom: initialVdom } ) )
+ Promise.resolve(
+ regionsToVdom( document, {
+ vdom: initialVdom,
+ baseUrl: window.location.href,
+ } )
+ )
);
// Check if the link is valid for client-side navigation.
diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png
new file mode 100644
index 00000000000000..4bc0f7a6b1dd70
Binary files /dev/null and b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png differ
diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png
new file mode 100644
index 00000000000000..7339cccdb78f28
Binary files /dev/null and b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png differ
diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png
new file mode 100644
index 00000000000000..97943030eb1e88
Binary files /dev/null and b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png differ
diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png
new file mode 100644
index 00000000000000..b7c455784e8a42
Binary files /dev/null and b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png differ
diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png
new file mode 100644
index 00000000000000..b7c455784e8a42
Binary files /dev/null and b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png differ
diff --git a/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts b/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts
index fd850a6e39fae2..74436673f10b79 100644
--- a/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts
+++ b/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts
@@ -6,6 +6,30 @@ import type { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
type AddPostWithBlockOptions = {
alias?: string;
attributes?: Record< string, any >;
+ innerBlocks?: Block[];
+};
+
+type Block = [
+ type: string,
+ attributes?: Record< string, any >,
+ innerBlocks?: Block[],
+];
+
+const generateBlockMarkup = ( [
+ type,
+ attributes,
+ innerBlocks,
+]: Block ): string => {
+ const typeAndAttributes = attributes
+ ? `${ type } ${ JSON.stringify( attributes ) }`
+ : type;
+
+ if ( ! innerBlocks ) {
+ return ``;
+ }
+ return `${ innerBlocks
+ .map( generateBlockMarkup )
+ .join( '' ) }`;
};
export default class InteractivityUtils {
@@ -40,7 +64,7 @@ export default class InteractivityUtils {
async addPostWithBlock(
name: string,
- { attributes, alias }: AddPostWithBlockOptions = {}
+ { attributes, alias, innerBlocks }: AddPostWithBlockOptions = {}
) {
const block = attributes
? `${ name } ${ JSON.stringify( attributes ) }`
@@ -50,8 +74,14 @@ export default class InteractivityUtils {
alias = block;
}
+ const content = generateBlockMarkup( [
+ name,
+ attributes,
+ innerBlocks,
+ ] );
+
const payload = {
- content: ``,
+ content,
status: 'publish' as 'publish',
date_gmt: '2023-01-01T00:00:00',
title: alias,
diff --git a/test/e2e/specs/interactivity/router-styles.spec.ts b/test/e2e/specs/interactivity/router-styles.spec.ts
new file mode 100644
index 00000000000000..7bc575af37816c
--- /dev/null
+++ b/test/e2e/specs/interactivity/router-styles.spec.ts
@@ -0,0 +1,232 @@
+/**
+ * Internal dependencies
+ */
+import { test, expect } from './fixtures';
+
+const COLOR_RED = 'rgb(255, 0, 0)';
+const COLOR_GREEN = 'rgb(0, 255, 0)';
+const COLOR_BLUE = 'rgb(0, 0, 255)';
+const COLOR_WRAPPER = 'rgb(160, 12, 60)';
+
+test.describe( 'Router styles', () => {
+ test.beforeAll( async ( { interactivityUtils: utils } ) => {
+ await utils.activatePlugins();
+ const red = await utils.addPostWithBlock(
+ 'test/router-styles-wrapper',
+ {
+ alias: 'red',
+ innerBlocks: [ [ 'test/router-styles-red' ] ],
+ }
+ );
+ const green = await utils.addPostWithBlock(
+ 'test/router-styles-wrapper',
+ {
+ alias: 'green',
+ innerBlocks: [ [ 'test/router-styles-green' ] ],
+ }
+ );
+ const blue = await utils.addPostWithBlock(
+ 'test/router-styles-wrapper',
+ {
+ alias: 'blue',
+ innerBlocks: [ [ 'test/router-styles-blue' ] ],
+ }
+ );
+
+ const all = await utils.addPostWithBlock(
+ 'test/router-styles-wrapper',
+ {
+ alias: 'all',
+ innerBlocks: [
+ [ 'test/router-styles-red' ],
+ [ 'test/router-styles-green' ],
+ [ 'test/router-styles-blue' ],
+ ],
+ }
+ );
+
+ await utils.addPostWithBlock( 'test/router-styles-wrapper', {
+ alias: 'none',
+ attributes: { links: { red, green, blue, all } },
+ } );
+ } );
+
+ test.beforeEach( async ( { page, interactivityUtils: utils } ) => {
+ await page.goto( utils.getLink( 'none' ) );
+ } );
+
+ test.afterAll( async ( { interactivityUtils: utils } ) => {
+ await utils.deactivatePlugins();
+ await utils.deleteAllPosts();
+ } );
+
+ test( 'should add and remove styles from style tags', async ( {
+ page,
+ } ) => {
+ const csn = page.getByTestId( 'client-side navigation' );
+ const red = page.getByTestId( 'red' );
+ const green = page.getByTestId( 'green' );
+ const blue = page.getByTestId( 'blue' );
+ const all = page.getByTestId( 'all' );
+
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_WRAPPER );
+
+ await page.getByTestId( 'link red' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_RED );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_RED );
+
+ await page.getByTestId( 'link green' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_GREEN );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_GREEN );
+
+ await page.getByTestId( 'link blue' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_BLUE );
+ await expect( all ).toHaveCSS( 'color', COLOR_BLUE );
+
+ await page.getByTestId( 'link all' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_RED );
+ await expect( green ).toHaveCSS( 'color', COLOR_GREEN );
+ await expect( blue ).toHaveCSS( 'color', COLOR_BLUE );
+ await expect( all ).toHaveCSS( 'color', COLOR_BLUE );
+ } );
+
+ test( 'should add and remove styles from referenced style sheets', async ( {
+ page,
+ } ) => {
+ const csn = page.getByTestId( 'client-side navigation' );
+ const red = page.getByTestId( 'red-from-link' );
+ const green = page.getByTestId( 'green-from-link' );
+ const blue = page.getByTestId( 'blue-from-link' );
+ const all = page.getByTestId( 'all-from-link' );
+
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_WRAPPER );
+
+ await page.getByTestId( 'link red' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_RED );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_RED );
+
+ await page.getByTestId( 'link green' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_GREEN );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_GREEN );
+
+ await page.getByTestId( 'link blue' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_BLUE );
+ await expect( all ).toHaveCSS( 'color', COLOR_BLUE );
+
+ await page.getByTestId( 'link all' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_RED );
+ await expect( green ).toHaveCSS( 'color', COLOR_GREEN );
+ await expect( blue ).toHaveCSS( 'color', COLOR_BLUE );
+ await expect( all ).toHaveCSS( 'color', COLOR_BLUE );
+ } );
+
+ test( 'should support relative URLs in referenced style sheets', async ( {
+ page,
+ } ) => {
+ const csn = page.getByTestId( 'client-side navigation' );
+ const background = page.getByTestId( 'background-from-link' );
+
+ await expect( background ).toHaveScreenshot();
+
+ await page.getByTestId( 'link red' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( background ).toHaveScreenshot();
+
+ await page.getByTestId( 'link green' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( background ).toHaveScreenshot();
+
+ await page.getByTestId( 'link blue' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( background ).toHaveScreenshot();
+
+ await page.getByTestId( 'link all' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( background ).toHaveScreenshot();
+ } );
+
+ test( 'should update style tags with modified content', async ( {
+ page,
+ } ) => {
+ const csn = page.getByTestId( 'client-side navigation' );
+ const red = page.getByTestId( 'red-from-inline' );
+ const green = page.getByTestId( 'green-from-inline' );
+ const blue = page.getByTestId( 'blue-from-inline' );
+ const all = page.getByTestId( 'all-from-inline' );
+
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_WRAPPER );
+
+ await page.getByTestId( 'link red' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_RED );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_RED );
+
+ await page.getByTestId( 'link green' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_GREEN );
+ await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( all ).toHaveCSS( 'color', COLOR_GREEN );
+
+ await page.getByTestId( 'link blue' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER );
+ await expect( blue ).toHaveCSS( 'color', COLOR_BLUE );
+ await expect( all ).toHaveCSS( 'color', COLOR_BLUE );
+
+ await page.getByTestId( 'link all' ).click();
+
+ await expect( csn ).toBeVisible();
+ await expect( red ).toHaveCSS( 'color', COLOR_RED );
+ await expect( green ).toHaveCSS( 'color', COLOR_GREEN );
+ await expect( blue ).toHaveCSS( 'color', COLOR_BLUE );
+ await expect( all ).toHaveCSS( 'color', COLOR_BLUE );
+ } );
+} );