Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
52d68bc
Introduce synchronization promise for initialVdom to resolve race con…
Mar 2, 2026
9d7b387
Update hydration.ts
Mar 2, 2026
61ab3f4
Expose initialVdomPromise via privateApis
Mar 2, 2026
3030539
Fix router race condition by awaiting hydration
Mar 2, 2026
0208546
Fixed the TypeScript error (`TS18004`)
Mar 2, 2026
e6ca1d1
Fix Prettier formatting in hydration.ts
Mar 2, 2026
00dbd54
Fix Prettier formatting for resolveInitialVdom type
Mar 2, 2026
e500ae2
Add test block registration for router hydration race condition
Mar 3, 2026
8b9371a
Interactive markup for router race condition test block
Mar 3, 2026
74f658e
Force static router import to make hydration race deterministic in al…
Mar 3, 2026
4214b0d
Add store with context and state actions for router race condition test
Mar 3, 2026
08dba8c
Add e2e test for router hydration race condition
Mar 3, 2026
cbea0bd
Add PHP docblock to render.php to satisfy PHPCS coding standards
Mar 3, 2026
9d04f95
Fix TypeScript errors in spec: use requestUtils.createPage instead of…
Mar 3, 2026
ca764be
Fix e2e spec: use interactivityUtils fixture to activate test plugin
Mar 3, 2026
e651478
Evaluate router code
DAreRodz Mar 4, 2026
236c303
Make interactive region a router region
DAreRodz Mar 4, 2026
f55e9e0
Run e2e tests in Safari and Firefox too
DAreRodz Mar 4, 2026
b1b07f1
Move goto to a beforeEach callback and simplify comments
DAreRodz Mar 4, 2026
b5226c1
Update CHANGELOG.md
Mar 4, 2026
1e1601e
Update CHANGELOG iAPI
Mar 4, 2026
33fdef5
Update CHANGELOG.md
Mar 4, 2026
aaf7637
Update CHANGELOG.md (iAPI-router)
Mar 4, 2026
997f334
Interactivity: Remove initialVdom from privateApis exports
Mar 4, 2026
c1fed18
Interactivity Router: Remove initialVdom import, use WeakMap type in …
Mar 4, 2026
e5e37bd
Merge branch 'trunk' into Router-initialization-race-condition-on-Saf…
Mar 4, 2026
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
Prev Previous commit
Next Next commit
Fix TypeScript errors in spec: use requestUtils.createPage instead of…
… admin.insertBlock
  • Loading branch information
Marcus Karlos
Marcus Karlos authored Mar 3, 2026
commit 9d04f95152a311e9559b0dbbe9a7c41920604bcb
65 changes: 35 additions & 30 deletions test/e2e/specs/interactivity/router-race-condition.spec.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
import { test, expect } from '@wordpress/e2e-test-utils-playwright';

/**
* Covers the race condition from #75778 fixed in #76053.
* E2E tests: Interactivity API router hydration race condition.
*
* Covers the bug in #75778 fixed in #76053.
*
* The test/router-race-condition block declares the router as a STATIC
* dependency in view.asset.php ('import' => 'static'). This places the
* router at the root of the ES module graph so its module-level code
* always runs before hydrateRegions() populates initialVdom -- making
* the race deterministic in all browsers, not just Firefox and Safari.
* dependency in view.asset.php ( 'import' => 'static' ). This places the
* router in the initial ES module graph so its module-level code always
* runs before hydrateRegions() populates initialVdom, making the race
* deterministic in every browser and every CI run.
*
* Without the fix:
* router reads empty initialVdom -> calls toVdom() on full document ->
* every island is added to hydratedIslands -> hydrateRegions() skips
* all islands -> dead DOM, no event listeners attached.
* Without the fix: router reads empty initialVdom, calls toVdom() on the
* full document, marks every island in hydratedIslands, hydrateRegions()
* skips all islands, dead DOM on every load.
*
* With the fix (initialVdomPromise):
* router awaits the promise before reading initialVdom ->
* hydrateRegions() runs normally -> all bindings attached.
* With initialVdomPromise: router awaits the promise, hydrateRegions()
* runs normally, all bindings are attached.
*/

import { test, expect } from '@wordpress/e2e-test-utils-playwright';

test.describe( 'Interactivity API router hydration race condition', () => {
let pageId: number;

test.beforeAll( async ( { requestUtils } ) => {
const createdPage = await requestUtils.createPage( {
title: 'Router race condition test',
content: '<!-- wp:test/router-race-condition /-->',
status: 'publish',
} );
pageId = createdPage.id;
} );

test.afterAll( async ( { requestUtils } ) => {
await requestUtils.deleteAllPages();
} );

/**
* context-counter: data-wp-on--click bound to local context.
* If hydration is skipped the click has no effect and the assertion
* times out, reproducing the dead DOM observable in #75778.
* If hydration is skipped the binding is never attached, clicks have
* no effect, and the assertion times out -- reproducing dead DOM.
*/
test( 'context-driven binding works when router is a static dependency', async ( {
page,
admin,
} ) => {
await admin.createNewPost( { postType: 'page' } );
await admin.insertBlock( 'test/router-race-condition' );
await admin.publishPost();

await page.goto( page.url() );
await page.goto( `/?p=${ pageId }` );

const button = page.getByTestId( 'context-counter' );

await expect( button ).toHaveText( '0' );

Check failure on line 50 in test/e2e/specs/interactivity/router-race-condition.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright - 5

[chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:43:6 › Interactivity API router hydration race condition › context-driven binding works when router is a static dependency

1) [chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:43:6 › Interactivity API router hydration race condition › context-driven binding works when router is a static dependency Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('context-counter') Expected: "0" Timeout: 5000ms Error: element(s) not found Call log: - Expect "toHaveText" with timeout 5000ms - waiting for getByTestId('context-counter') 48 | const button = page.getByTestId( 'context-counter' ); 49 | > 50 | await expect( button ).toHaveText( '0' ); | ^ 51 | 52 | await button.click(); 53 | await expect( button ).toHaveText( '1' ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-race-condition.spec.ts:50:26

Check failure on line 50 in test/e2e/specs/interactivity/router-race-condition.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright - 5

[chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:43:6 › Interactivity API router hydration race condition › context-driven binding works when router is a static dependency

1) [chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:43:6 › Interactivity API router hydration race condition › context-driven binding works when router is a static dependency Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('context-counter') Expected: "0" Timeout: 5000ms Error: element(s) not found Call log: - Expect "toHaveText" with timeout 5000ms - waiting for getByTestId('context-counter') 48 | const button = page.getByTestId( 'context-counter' ); 49 | > 50 | await expect( button ).toHaveText( '0' ); | ^ 51 | 52 | await button.click(); 53 | await expect( button ).toHaveText( '1' ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-race-condition.spec.ts:50:26

Check failure on line 50 in test/e2e/specs/interactivity/router-race-condition.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright - 5

[chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:43:6 › Interactivity API router hydration race condition › context-driven binding works when router is a static dependency

1) [chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:43:6 › Interactivity API router hydration race condition › context-driven binding works when router is a static dependency Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('context-counter') Expected: "0" Timeout: 5000ms Error: element(s) not found Call log: - Expect "toHaveText" with timeout 5000ms - waiting for getByTestId('context-counter') 48 | const button = page.getByTestId( 'context-counter' ); 49 | > 50 | await expect( button ).toHaveText( '0' ); | ^ 51 | 52 | await button.click(); 53 | await expect( button ).toHaveText( '1' ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-race-condition.spec.ts:50:26

await button.click();
await expect( button ).toHaveText( '1' );
Expand All @@ -47,22 +58,16 @@

/**
* global-counter: data-wp-on--click bound to global state.
* Tests that state-driven bindings are also correctly hydrated when
* the router is loaded statically.
* Tests that state-driven bindings are also correctly hydrated.
*/
test( 'global-state binding works when router is a static dependency', async ( {
page,
admin,
} ) => {
await admin.createNewPost( { postType: 'page' } );
await admin.insertBlock( 'test/router-race-condition' );
await admin.publishPost();

await page.goto( page.url() );
await page.goto( `/?p=${ pageId }` );

const button = page.getByTestId( 'global-counter' );

await expect( button ).toHaveText( '0' );

Check failure on line 70 in test/e2e/specs/interactivity/router-race-condition.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright - 5

[chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:63:6 › Interactivity API router hydration race condition › global-state binding works when router is a static dependency

2) [chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:63:6 › Interactivity API router hydration race condition › global-state binding works when router is a static dependency Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('global-counter') Expected: "0" Timeout: 5000ms Error: element(s) not found Call log: - Expect "toHaveText" with timeout 5000ms - waiting for getByTestId('global-counter') 68 | const button = page.getByTestId( 'global-counter' ); 69 | > 70 | await expect( button ).toHaveText( '0' ); | ^ 71 | 72 | await button.click(); 73 | await expect( button ).toHaveText( '1' ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-race-condition.spec.ts:70:26

Check failure on line 70 in test/e2e/specs/interactivity/router-race-condition.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright - 5

[chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:63:6 › Interactivity API router hydration race condition › global-state binding works when router is a static dependency

2) [chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:63:6 › Interactivity API router hydration race condition › global-state binding works when router is a static dependency Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('global-counter') Expected: "0" Timeout: 5000ms Error: element(s) not found Call log: - Expect "toHaveText" with timeout 5000ms - waiting for getByTestId('global-counter') 68 | const button = page.getByTestId( 'global-counter' ); 69 | > 70 | await expect( button ).toHaveText( '0' ); | ^ 71 | 72 | await button.click(); 73 | await expect( button ).toHaveText( '1' ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-race-condition.spec.ts:70:26

Check failure on line 70 in test/e2e/specs/interactivity/router-race-condition.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright - 5

[chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:63:6 › Interactivity API router hydration race condition › global-state binding works when router is a static dependency

2) [chromium] › test/e2e/specs/interactivity/router-race-condition.spec.ts:63:6 › Interactivity API router hydration race condition › global-state binding works when router is a static dependency Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('global-counter') Expected: "0" Timeout: 5000ms Error: element(s) not found Call log: - Expect "toHaveText" with timeout 5000ms - waiting for getByTestId('global-counter') 68 | const button = page.getByTestId( 'global-counter' ); 69 | > 70 | await expect( button ).toHaveText( '0' ); | ^ 71 | 72 | await button.click(); 73 | await expect( button ).toHaveText( '1' ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-race-condition.spec.ts:70:26

await button.click();
await expect( button ).toHaveText( '1' );
Expand Down
Loading