76 lines
3.5 KiB
Markdown
76 lines
3.5 KiB
Markdown
---
|
|
type: Task
|
|
status: AI Gen TODO
|
|
category: QA
|
|
isAgentGenerated: "true"
|
|
---
|
|
|
|
# Test: Blazor WASM Hydration Timing and State Reconciliation
|
|
|
|
## Description
|
|
|
|
Verify that the Blazor WASM client correctly reconciles state during hydration — the prerendered HTML should match the client-rendered output, and user interactions initiated **before** Blazor finishes loading should either be queued or gracefully ignored, not cause a crash or a stale render.
|
|
|
|
## Rationale
|
|
|
|
The app uses Blazor WASM with server-side prerendering. During the hydration gap (between `window.onload` and Blazor attaching its event handlers), the page shows static prerendered HTML. If a user (or test) interacts with a `<select>` or button during this gap, Blazor may:
|
|
|
|
- Lose the native-DOM change on re-render (the select value reverts).
|
|
- Miss the event entirely (no handler is connected yet).
|
|
- Throw a JS interop exception after hydration completes.
|
|
|
|
Our existing tests all navigate with `waitUntil: 'load'` and immediately start clicking. The `debug_initial_state.js` script revealed that Blazor components re-render asynchronously after load, destroying and recreating the DOM elements. This gap is a real source of flakiness.
|
|
|
|
## Playwright Feature
|
|
|
|
This test uses **`page.waitForResponse()`** to wait for a specific WASM resource (`dotnet.js` or `dotnet.wasm`) to confirm the .NET runtime has loaded, plus **`page.evaluate()` with polling** to detect when Blazor's component tree is fully hydrated.
|
|
|
|
### Approach
|
|
|
|
```js
|
|
// Wait until the Blazor runtime signals it's ready
|
|
await page.waitForResponse(response =>
|
|
response.url().includes('dotnet.wasm') && response.status() === 200
|
|
);
|
|
|
|
// OR poll for a Blazor-specific DOM signal
|
|
await page.waitForFunction(() => {
|
|
// Blazor sets the `_blazorCircuitId` or similar on the root element
|
|
// Note: adjust the selector to match the app's actual signal
|
|
return document.querySelector('select')?.classList.contains('blazor-hydrated')
|
|
|| document.querySelector('.keyContainer > div') !== null;
|
|
});
|
|
```
|
|
|
|
### Test Cases
|
|
|
|
| Scenario | Interaction Timing | Expected Behavior |
|
|
|---|---|---|
|
|
| **Early select** | Select faction BEFORE dotnet.wasm loads | Select reverts on hydration, but WASM should apply the correct default filter |
|
|
| **Early click** | Click "Clear Build Order" before Blazor ready | Click is ignored or queued; no crash |
|
|
| **Early keyboard** | Press Q/W/E before key handler attached | Keys are ignored; no crash; no entity added |
|
|
| **Mid-hydration** | Interact during component re-render (DOM destruction phase) | Interaction should be lost but never crash |
|
|
| **Late interaction** | Wait for `dotnet.wasm` response, THEN interact | Should work correctly (this is the happy path that already passes) |
|
|
|
|
### Detecting Hydration Completion
|
|
|
|
Since Blazor WASM with prerendering doesn't emit a built-in "I'm done" event, the most reliable signal is:
|
|
|
|
```js
|
|
await page.waitForFunction(() => {
|
|
// Wait for at least one Blazor-triggered re-render by checking
|
|
// the keyContainer has child divs with keyboard buttons
|
|
const container = document.querySelector('.keyContainer');
|
|
if (!container) return false;
|
|
const buttons = container.querySelectorAll(':scope > div');
|
|
return buttons.length === 19 || buttons.length > 10;
|
|
});
|
|
```
|
|
|
|
### What This Test Catches
|
|
|
|
- Race conditions between `page.goto('waitUntil: load')` and Blazor hydration.
|
|
- DOM elements that are destroyed and re-created during hydration (stale locator references).
|
|
- Test flakiness caused by Blazor's async initialization.
|
|
- Missing error boundaries when JS interop calls fail during hydration.
|