diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json new file mode 100644 index 0000000000000..bdb5a19e03062 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json @@ -0,0 +1,14 @@ +{ + "apiVersion": 2, + "name": "test/router-navigate", + "title": "E2E Interactivity tests - router navigate", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "router-navigate-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php new file mode 100644 index 0000000000000..683e0eaff6e89 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php @@ -0,0 +1,44 @@ + + + +
+

+ + NaN + undefined + + $link ) { + $i = $key += 1; + echo <<link $i + link $i with hash +HTML; + } + } + ?> +
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js new file mode 100644 index 0000000000000..468b4a64482d3 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js @@ -0,0 +1,35 @@ +( ( { wp } ) => { + /** + * WordPress dependencies + */ + const { store, navigate } = wp.interactivity; + + store( { + state: { + router: { + status: 'idle', + navigations: 0, + } + }, + actions: { + router: { + navigate: async ( { state, event: e } ) => { + e.preventDefault(); + + state.router.navigations += 1; + state.router.status = 'busy'; + + const force = e.target.dataset.forceNavigation === 'true'; + + await navigate( e.target.href, { force } ); + + state.router.navigations -= 1; + + if ( state.router.navigations === 0) { + state.router.status = 'idle'; + } + }, + }, + }, + } ); +} )( window ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 636f78d3357cf..19e82ff30471b 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Improve `navigate()` to render only the result of the last call when multiple happen simultaneously. ([#54201](https://github.com/WordPress/gutenberg/pull/54201)) + ## 2.2.0 (2023-08-31) ### Enhancements diff --git a/packages/interactivity/src/router.js b/packages/interactivity/src/router.js index cc7925e2fc398..17b2b54e4d457 100644 --- a/packages/interactivity/src/router.js +++ b/packages/interactivity/src/router.js @@ -78,11 +78,21 @@ const renderRegions = ( page ) => { } ); }; +// Variable to store the current navigation. +let navigatingTo = ''; + // Navigate to a new page. export const navigate = async ( href, options = {} ) => { const url = cleanUrl( href ); + navigatingTo = href; prefetch( url, options ); const page = await pages.get( url ); + + // Once the page is fetched, the destination URL could have changed (e.g., + // by clicking another link in the meantime). If so, bail out, and let the + // newer execution to update the HTML. + if ( navigatingTo !== href ) return; + if ( page ) { renderRegions( page ); window.history[ options.replace ? 'replaceState' : 'pushState' ]( diff --git a/test/e2e/specs/interactivity/router-navigate.spec.ts b/test/e2e/specs/interactivity/router-navigate.spec.ts new file mode 100644 index 0000000000000..308bc6fd98618 --- /dev/null +++ b/test/e2e/specs/interactivity/router-navigate.spec.ts @@ -0,0 +1,129 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'Router navigate', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + const link2 = await utils.addPostWithBlock( 'test/router-navigate', { + alias: 'router navigate - link 2', + attributes: { title: 'Link 2' }, + } ); + const link1 = await utils.addPostWithBlock( 'test/router-navigate', { + alias: 'router navigate - link 1', + attributes: { title: 'Link 1' }, + } ); + await utils.addPostWithBlock( 'test/router-navigate', { + alias: 'router navigate - main', + attributes: { title: 'Main', links: [ link1, link2 ] }, + } ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'router navigate - main' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should update the HTML only for the latest navigation', async ( { + page, + interactivityUtils: utils, + } ) => { + const link1 = utils.getLink( 'router navigate - link 1' ); + const link2 = utils.getLink( 'router navigate - link 2' ); + + const navigations = page.getByTestId( 'router navigations' ); + const status = page.getByTestId( 'router status' ); + const title = page.getByTestId( 'title' ); + + await expect( navigations ).toHaveText( '0' ); + await expect( status ).toHaveText( 'idle' ); + + let resolveLink1: Function; + let resolveLink2: Function; + + await page.route( link1, async ( route ) => { + await new Promise( ( r ) => ( resolveLink1 = r ) ); + await route.continue(); + } ); + await page.route( link2, async ( route ) => { + await new Promise( ( r ) => ( resolveLink2 = r ) ); + await route.continue(); + } ); + + await page.getByTestId( 'link 1' ).click(); + await page.getByTestId( 'link 2' ).click(); + + await expect( navigations ).toHaveText( '2' ); + await expect( status ).toHaveText( 'busy' ); + await expect( title ).toHaveText( 'Main' ); + + await Promise.resolve().then( () => resolveLink2() ); + + await expect( navigations ).toHaveText( '1' ); + await expect( status ).toHaveText( 'busy' ); + await expect( title ).toHaveText( 'Link 2' ); + + await Promise.resolve().then( () => resolveLink1() ); + + await expect( navigations ).toHaveText( '0' ); + await expect( status ).toHaveText( 'idle' ); + await expect( title ).toHaveText( 'Link 2' ); + } ); + + test( 'should update the URL from the last navigation if only varies in the URL fragment', async ( { + page, + interactivityUtils: utils, + } ) => { + const link1 = utils.getLink( 'router navigate - link 1' ); + + const navigations = page.getByTestId( 'router navigations' ); + const status = page.getByTestId( 'router status' ); + const title = page.getByTestId( 'title' ); + + await expect( navigations ).toHaveText( '0' ); + await expect( status ).toHaveText( 'idle' ); + + const resolvers: Function[] = []; + + await page.route( link1, async ( route ) => { + await new Promise( ( r ) => resolvers.push( r ) ); + await route.continue(); + } ); + + await page.getByTestId( 'link 1' ).click(); + await page.getByTestId( 'link 1 with hash' ).click(); + + const href = ( await page + .getByTestId( 'link 1 with hash' ) + .getAttribute( 'href' ) ) as string; + + await expect( navigations ).toHaveText( '2' ); + await expect( status ).toHaveText( 'busy' ); + await expect( title ).toHaveText( 'Main' ); + + { + const resolver = resolvers.pop(); + if ( resolver ) resolver(); + } + + await expect( navigations ).toHaveText( '1' ); + await expect( status ).toHaveText( 'busy' ); + await expect( title ).toHaveText( 'Link 1' ); + await expect( page ).toHaveURL( href ); + + { + const resolver = resolvers.pop(); + if ( resolver ) resolver(); + } + + await expect( navigations ).toHaveText( '0' ); + await expect( status ).toHaveText( 'idle' ); + await expect( title ).toHaveText( 'Link 1' ); + await expect( page ).toHaveURL( href ); + } ); +} );