Merge pull request 'fix(router): sync route in navigate() — kill boot lobby-flash + offline e2e flake' (#210) from fix/boot-route-lag-e2e-flake into development
CI / changes (push) Successful in 1s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m4s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m39s
CI / changes (push) Successful in 1s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m4s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m39s
This commit was merged in pull request #210.
This commit is contained in:
@@ -13,10 +13,12 @@ async function forceStandalone(page: Page): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a load or reload, land in the lobby (dismiss the login screen if it is shown).
|
// After a load, land in the lobby. The mock cold-starts on the login screen (no seeded session), so
|
||||||
|
// click through as guest. Waiting for the button (rather than a point-in-time count()) is what makes
|
||||||
|
// this deterministic: a count() sampled during the pre-login splash frame returned 0, skipped the
|
||||||
|
// click, and then hung — or latched a transient lobby tab-bar — instead of opening the real lobby.
|
||||||
async function enterLobby(page: Page): Promise<void> {
|
async function enterLobby(page: Page): Promise<void> {
|
||||||
const guest = page.getByRole('button', { name: /guest|гост/i });
|
await page.getByRole('button', { name: /guest|гост/i }).first().click();
|
||||||
if (await guest.count()) await guest.first().click();
|
|
||||||
// The lobby tab bar has three tabs in a fixed order: New (0), Stats (1), Settings (2). nth() is
|
// The lobby tab bar has three tabs in a fixed order: New (0), Stats (1), Settings (2). nth() is
|
||||||
// robust to locale, emoji variation selectors and the coachmark anchors (which the first-run
|
// robust to locale, emoji variation selectors and the coachmark anchors (which the first-run
|
||||||
// onboarding strips after it completes).
|
// onboarding strips after it completes).
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { expect, test } from './fixtures';
|
||||||
|
|
||||||
|
// The hash router must update its reactive `route` rune synchronously inside navigate(), not defer it
|
||||||
|
// to the asynchronous `hashchange` event. Bootstrap flips `app.ready` in the same tick right after
|
||||||
|
// `navigate('/login')` on an unauthenticated cold start; a route that trailed the hash for one frame
|
||||||
|
// let App.svelte render the stale route (the empty-hash lobby) under the new login screen — a visible
|
||||||
|
// lobby-shell flash plus a doomed games.list. It also made the offline e2e flaky: enterLobby could
|
||||||
|
// latch that transient lobby tab-bar instead of clicking through the login screen.
|
||||||
|
|
||||||
|
test('navigate updates the reactive route synchronously (no hashchange lag)', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
// The bundle has loaded and installed the mock __router seam once the login screen is up.
|
||||||
|
await expect(page.getByRole('button', { name: /guest/i })).toBeVisible();
|
||||||
|
|
||||||
|
// Read the route in the SAME microtask as the navigate: the fix makes it the new route at once;
|
||||||
|
// the pre-fix code returned the previous route (it waited for the async hashchange).
|
||||||
|
const routeAfterNavigate = await page.evaluate(() => {
|
||||||
|
const r = (window as unknown as { __router: { navigate(p: string): void; route(): string } }).__router;
|
||||||
|
r.navigate('/settings');
|
||||||
|
return r.route();
|
||||||
|
});
|
||||||
|
expect(routeAfterNavigate).toBe('settings');
|
||||||
|
|
||||||
|
// The hash was written too, so a reload/back still resolves the same route.
|
||||||
|
expect(new URL(page.url()).hash).toBe('#/settings');
|
||||||
|
});
|
||||||
@@ -1310,4 +1310,11 @@ if (import.meta.env.MODE === 'mock' && typeof window !== 'undefined') {
|
|||||||
closeStream();
|
closeStream();
|
||||||
app.blocked = { blocked: true, permanent: true, until: '', reason: '', ...b };
|
app.blocked = { blocked: true, permanent: true, until: '', reason: '', ...b };
|
||||||
};
|
};
|
||||||
|
// Expose the router so the e2e can assert navigate() updates the route synchronously (no
|
||||||
|
// hashchange lag): a route that trailed the hash for a frame let bootstrap's `app.ready` flip
|
||||||
|
// render the previous route under the new screen (the empty-hash lobby flashing under login).
|
||||||
|
(window as unknown as { __router?: { navigate(p: string): void; route(): string } }).__router = {
|
||||||
|
navigate,
|
||||||
|
route: () => router.route.name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ if (typeof window !== 'undefined') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** navigate switches the hash route (and forces a re-parse if it is unchanged). */
|
/** navigate switches the hash route (and forces a re-parse if it is unchanged). The route rune is
|
||||||
|
* updated synchronously — not left to the asynchronous `hashchange` — so a caller that renders off
|
||||||
|
* the route in the same tick (notably bootstrap flipping `app.ready` right after `navigate('/login')`)
|
||||||
|
* never sees `router.route` lag the hash for a frame, which would briefly render the previous route
|
||||||
|
* (the empty-hash lobby) under the new screen. The later `hashchange` re-parses to the same value. */
|
||||||
export function navigate(path: string): void {
|
export function navigate(path: string): void {
|
||||||
const target = '#' + path;
|
const target = '#' + path;
|
||||||
if (location.hash === target) {
|
router.route = parse(target);
|
||||||
router.route = parse(target);
|
if (location.hash !== target) {
|
||||||
} else {
|
|
||||||
location.hash = path;
|
location.hash = path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user