url deep-link: game id in #/game/<id>, multi-tab + bad-link fallback (v0.3.0)
CI / build + unit + e2e (pull_request) Successful in 1m52s
CI / build + unit + e2e (pull_request) Successful in 1m52s
This commit is contained in:
+103
@@ -180,3 +180,106 @@ test.describe('History — save / resume across a page refresh', () => {
|
||||
await expect(page.getByTestId('player-nation')).toHaveText('United Kingdom');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* URL deep-linking (issue #3). The active game's id lives in the URL fragment:
|
||||
* home is `#/` and an active game is `#/game/<id>`. A fresh page navigated to
|
||||
* that hash loads that exact game; a bad/missing id falls back to home and
|
||||
* clears the hash; returning to home clears it. A browser context is fresh per
|
||||
* test, so localStorage is isolated, but the e2e webServer shares one SQLite
|
||||
* (`data/e2e`) across tests — so each test captures the *specific* id of the
|
||||
* game it creates (via the POST /api/games response) rather than assuming "the
|
||||
* only game".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Start the first scenario (World War II) and pick the United Kingdom, returning
|
||||
* the freshly created game's id (read from the POST /api/games response). Leaves
|
||||
* the caller on the game screen at turn 1.
|
||||
*/
|
||||
async function createUkGame(page: Page): Promise<string> {
|
||||
await page.goto('/');
|
||||
await page.getByTestId('preset-card').first().waitFor({ timeout: 15000 });
|
||||
await page.getByTestId('preset-card').first().getByRole('button', { name: 'Play' }).click();
|
||||
await expect(page.getByTestId('world-background')).toBeVisible();
|
||||
const [resp] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(r) => r.request().method() === 'POST' && new URL(r.url()).pathname === '/api/games',
|
||||
),
|
||||
page
|
||||
.getByTestId('nation-card-uk')
|
||||
.getByRole('button', { name: 'Play as United Kingdom' })
|
||||
.click(),
|
||||
]);
|
||||
const body = (await resp.json()) as { game: { id: string } };
|
||||
return body.game.id;
|
||||
}
|
||||
|
||||
/** Poll until `location.hash` equals `expected` (hash writes happen in effects). */
|
||||
async function waitHash(page: Page, expected: string): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
(exp) => window.location.hash === exp,
|
||||
expected,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('History — URL deep-linking (#/game/<id>)', () => {
|
||||
test('creating a game puts its id in the URL', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const id = await createUkGame(page);
|
||||
|
||||
// The game screen mounts at turn 1; the URL-sync effect writes the deep link.
|
||||
await expect(page.getByTestId('world-map')).toBeVisible();
|
||||
await expect(page.getByTestId('turn-count')).toHaveText('1');
|
||||
await expect(page.getByTestId('player-nation')).toHaveText('United Kingdom');
|
||||
await waitHash(page, `#/game/${id}`);
|
||||
});
|
||||
|
||||
test('a fresh page deep-links to the exact saved game', async ({ page, context }) => {
|
||||
test.setTimeout(60000);
|
||||
const id = await createUkGame(page);
|
||||
await waitHash(page, `#/game/${id}`);
|
||||
const origin = await page.evaluate(() => location.origin);
|
||||
|
||||
// A brand-new page navigated straight to the deep link must load that exact
|
||||
// game (matching id, preset, and player) — the URL hash takes precedence over
|
||||
// the (shared) localStorage, so this is deterministic.
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(`${origin}/#/game/${id}`);
|
||||
await expect(page2.getByTestId('world-map')).toBeVisible();
|
||||
await expect(page2.getByTestId('turn-count')).toHaveText('1');
|
||||
await expect(page2.getByTestId('player-nation')).toHaveText('United Kingdom');
|
||||
await waitHash(page2, `#/game/${id}`);
|
||||
await page2.close();
|
||||
});
|
||||
|
||||
test('a bad game id falls back to the home screen and clears the hash', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
test.setTimeout(60000);
|
||||
await page.goto('/');
|
||||
const origin = await page.evaluate(() => location.origin);
|
||||
|
||||
// A fresh page navigated to a non-existent id must not crash: it lands on the
|
||||
// home screen and the bad hash is cleared to `#/`.
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(`${origin}/#/game/does-not-exist-xyz`);
|
||||
await expect(page2.getByTestId('preset-card').first()).toBeVisible({ timeout: 20000 });
|
||||
await waitHash(page2, '#/');
|
||||
await page2.close();
|
||||
});
|
||||
|
||||
test('returning to home from a game clears the hash to #/', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const id = await createUkGame(page);
|
||||
await expect(page.getByTestId('world-map')).toBeVisible();
|
||||
await waitHash(page, `#/game/${id}`);
|
||||
|
||||
// "← Scenarios" returns to the home screen; the URL-sync effect clears the hash.
|
||||
await page.getByTestId('back-to-scenarios').click();
|
||||
await expect(page.getByTestId('preset-card').first()).toBeVisible({ timeout: 15000 });
|
||||
await waitHash(page, '#/');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user