diff --git a/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php b/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php index 6fdc2d07e350c8..0b7e40b6c1653b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php @@ -18,6 +18,28 @@ +
+ + + + + + + + + diff --git a/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js b/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js index 9225f88ce9d279..5717387395ff24 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js @@ -3,9 +3,16 @@ */ import { store } from '@wordpress/interactivity'; + +store( '', { + state: { + url: '/empty-string-url', + }, +} ); + store( 'namespace', { state: { - url: '/some-url', + url: '/namespace-url', }, } ); @@ -14,3 +21,50 @@ store( 'other', { url: '/other-store-url', }, } ); + +store( 'null', { + state: { + url: '/null-url', + }, +} ); + +store( '2', { + state: { + url: '/number-url', + }, +} ); + +store( '{}', { + state: { + url: '/object-url', + }, +} ); + +store( 'true', { + state: { + url: '/true-url', + }, +} ); + +store( 'false', { + state: { + url: '/false-url', + }, +} ); + +store( '[]', { + state: { + url: '/array-url', + }, +} ); + +store( '"quoted string"', { + state: { + url: '/quoted-url', + }, +} ); + + + + + diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index e40c68a50180a2..c3542b914de159 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug fixes + +- Fix null and number strings as namespaces runtime error. ([#61960](https://github.com/WordPress/gutenberg/pull/61960/)) + ### Breaking Changes - Variables like `process.env.IS_GUTENBERG_PLUGIN` have been replaced by `globalThis.IS_GUTENBERG_PLUGIN`. Build systems using `process.env` should be updated ([#61486](https://github.com/WordPress/gutenberg/pull/61486)). diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index d173f2cd842dc1..9b35192fe8b132 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -15,7 +15,6 @@ import { setNamespace, resetNamespace, } from './hooks'; - const isObject = ( item: unknown ): item is Record< string, unknown > => Boolean( item && typeof item === 'object' && item.constructor === Object ); diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index cc481f89ff772a..98deca656cfa6c 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -13,6 +13,8 @@ const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; const namespaces: Array< string | null > = []; const currentNamespace = () => namespaces[ namespaces.length - 1 ] ?? null; +const isObject = ( item: unknown ): item is Record< string, unknown > => + Boolean( item && typeof item === 'object' && item.constructor === Object ); // Regular expression for directive parsing. const directiveParser = new RegExp( @@ -100,8 +102,9 @@ export function toVdom( root: Node ): Array< ComponentChild > { const namespace = regexResult?.[ 1 ] ?? null; let value: any = regexResult?.[ 2 ] ?? attributeValue; try { - value = value && JSON.parse( value ); - } catch ( e ) {} + const parsedValue = JSON.parse( value ); + value = isObject( parsedValue ) ? parsedValue : value; + } catch {} if ( attributeName === islandAttr ) { island = true; const islandNamespace = diff --git a/test/e2e/specs/interactivity/namespace.spec.ts b/test/e2e/specs/interactivity/namespace.spec.ts index fd38a7ecf6ed64..9d0edc997886cb 100644 --- a/test/e2e/specs/interactivity/namespace.spec.ts +++ b/test/e2e/specs/interactivity/namespace.spec.ts @@ -20,30 +20,68 @@ test.describe( 'Namespaces', () => { test( 'Empty string as namespace should not work', async ( { page } ) => { const el = page.getByTestId( 'empty namespace' ); - await expect( el ).not.toHaveAttribute( 'href', '/some-url' ); + await expect( el ).not.toHaveAttribute( 'href', '/empty-string-url' ); } ); test( 'A string as namespace should work', async ( { page } ) => { const el = page.getByTestId( 'correct namespace' ); - await expect( el ).toHaveAttribute( 'href', '/some-url' ); + await expect( el ).toHaveAttribute( 'href', '/namespace-url' ); } ); - test( 'An empty object as namespace should work', async ( { page } ) => { + test( 'An empty object as namespace should not work', async ( { + page, + } ) => { const el = page.getByTestId( 'object namespace' ); - await expect( el ).not.toHaveAttribute( 'href', '/some-url' ); + await expect( el ).not.toHaveAttribute( 'href', '/object-url' ); } ); test( 'A wrong namespace should not break the runtime', async ( { page, } ) => { const el = page.getByTestId( 'object namespace' ); - await expect( el ).not.toHaveAttribute( 'href', '/some-url' ); + await expect( el ).not.toHaveAttribute( 'href', '/namespace-url' ); const correct = page.getByTestId( 'correct namespace' ); - await expect( correct ).toHaveAttribute( 'href', '/some-url' ); + await expect( correct ).toHaveAttribute( 'href', '/namespace-url' ); } ); test( 'A different store namespace should work', async ( { page } ) => { const el = page.getByTestId( 'other namespace' ); await expect( el ).toHaveAttribute( 'href', '/other-store-url' ); } ); + + test( 'A number as a string as namespace should work', async ( { + page, + } ) => { + const el = page.getByTestId( 'number namespace' ); + await expect( el ).toHaveAttribute( 'href', '/number-url' ); + } ); + + test( 'A null as a string as namespace should work', async ( { page } ) => { + const el = page.getByTestId( 'null namespace' ); + await expect( el ).toHaveAttribute( 'href', '/null-url' ); + } ); + + test( 'A true as a string as namespace should work', async ( { page } ) => { + const el = page.getByTestId( 'true namespace' ); + await expect( el ).toHaveAttribute( 'href', '/true-url' ); + } ); + + test( 'A false as a string as namespace should work', async ( { + page, + } ) => { + const el = page.getByTestId( 'false namespace' ); + await expect( el ).toHaveAttribute( 'href', '/false-url' ); + } ); + + test( 'A [] as a string as namespace should work', async ( { page } ) => { + const el = page.getByTestId( '[] namespace' ); + await expect( el ).toHaveAttribute( 'href', '/array-url' ); + } ); + + test( 'A "quoted string" as a string as namespace should work', async ( { + page, + } ) => { + const el = page.getByTestId( 'quoted namespace' ); + await expect( el ).toHaveAttribute( 'href', '/quoted-url' ); + } ); } );