From 12f77640cfad82f850a02718e6fe7de41379b8ad Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 20 Dec 2024 13:23:15 +0100 Subject: [PATCH] iAPI Router: Handle styles assets on region-based navigation (#67826) * Experiment with * Handle relative URLs * Rename constructor to factory * Use `element.sheet` property if present * Remove unnecessary files * Remove extraneous param from tsdoc comment * Allow inner blocks in `addPostWithBlock` util * Add test blocks for testing styles * Fix directives in region block * Add styles to navigation counter * Implement first test * Add more tests WIP * Refactor test block to make them easier to understand WIP * Keep refactoring test blocks * Greatly simplify tests * Test support for referenced style sheets * Fix style files * Make HTML more clear * Add test for relative URLs * Add tests for modified inline style tags * Use text content instead of the id to identify inline style tags * Fix typo in comments Co-authored-by: Michal * Fix e2e interactivity utils --------- Co-authored-by: DAreRodz Co-authored-by: michalczaplinski Co-authored-by: luisherranz Co-authored-by: sethrubenstein --- .../assets/10x10_e2e_test_image_blue.png | Bin 0 -> 119 bytes .../router-styles-blue/block.json | 15 ++ .../router-styles-blue/render.php | 35 +++ .../router-styles-blue/style-from-link.css | 7 + .../router-styles-blue/style.css | 4 + .../assets/10x10_e2e_test_image_green.png | Bin 0 -> 119 bytes .../router-styles-green/block.json | 15 ++ .../router-styles-green/render.php | 35 +++ .../router-styles-green/style-from-link.css | 7 + .../router-styles-green/style.css | 4 + .../assets/10x10_e2e_test_image_red.png | Bin 0 -> 119 bytes .../router-styles-red/block.json | 15 ++ .../router-styles-red/render.php | 35 +++ .../router-styles-red/style-from-link.css | 7 + .../router-styles-red/style.css | 4 + .../router-styles-wrapper/block.json | 16 ++ .../router-styles-wrapper/render.php | 70 ++++++ .../router-styles-wrapper/style.css | 3 + .../router-styles-wrapper/view.asset.php | 9 + .../router-styles-wrapper/view.js | 20 ++ .../interactivity-router/src/assets/styles.ts | 79 ++++++ packages/interactivity-router/src/head.ts | 126 ---------- packages/interactivity-router/src/index.ts | 61 +++-- ...-in-referenced-style-sheets-1-chromium.png | Bin 0 -> 92 bytes ...-in-referenced-style-sheets-2-chromium.png | Bin 0 -> 96 bytes ...-in-referenced-style-sheets-3-chromium.png | Bin 0 -> 96 bytes ...-in-referenced-style-sheets-4-chromium.png | Bin 0 -> 97 bytes ...-in-referenced-style-sheets-5-chromium.png | Bin 0 -> 97 bytes .../fixtures/interactivity-utils.ts | 34 ++- .../specs/interactivity/router-styles.spec.ts | 232 ++++++++++++++++++ 30 files changed, 680 insertions(+), 153 deletions(-) create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js create mode 100644 packages/interactivity-router/src/assets/styles.ts delete mode 100644 packages/interactivity-router/src/head.ts create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png create mode 100644 test/e2e/specs/interactivity/router-styles.spec.ts 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 0000000000000000000000000000000000000000..c4f8e7c5146d362a505607042039b832115b9dfb GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP%~cJ|V6Q4FCStKRO8H@_M>BhE&W+PDw~O(#ybbn1TC*g4kN11cRrm KpUXO@geCyWY#fvT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..34ec87925d8c5045f71f6d09724db8fa00ff03af GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP%~cJ|V6Q{~1i;4y*ujc|BbmLn`JZrz9jC>1ALz%)os@L2NBhg2B_( K&t;ucLK6U;SQ~Hv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3264bf6427c2766e0b2e6cb8c5dc0f88d123e239 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP%~cJ|V9E85krVahe0Uyq+$OAr*6yQxX!6^fE9UX5c=dAhs4L!QkoY K=d#Wzp$Pzi{Ti_V literal 0 HcmV?d00001 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 @@ + +
> + +
+ Styles from block styles +

Red

+

Green

+

Blue

+

All

+
+ + +
+ Styles from referenced style sheets + + + + + +
+ + +
+ Styles from inline styles +

Red

+

Green

+

Blue

+

All

+
+ + + + + +
+ +
+ + +
+ 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 0000000000000000000000000000000000000000..4bc0f7a6b1dd7010f6e452350e97ea302ddd9e1b GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`hk44ofy`glX(f`NO`(AhGbWCMqFn`Pk44ofy`glX(f`NO`(AhGzopr04R|gIsgCw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b7c455784e8a425cb06f4d1c3e8397e552ca19c2 GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`NPD_ChGr8Danc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b7c455784e8a425cb06f4d1c3e8397e552ca19c2 GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`NPD_ChGr8Danc literal 0 HcmV?d00001 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 ); + } ); +} );