test(e2e): mission test — match dotted ACTIVE badge, idempotent across retries
CI / build + unit + e2e (pull_request) Successful in 2m25s

CI run 358: the ACTIVE assertion used exact text but badge.tsx renders a
'●' dot prefix for ACTIVE variants (text is '●ACTIVE'), so it could never
match. And attempt 1's accept persisted an ACTIVE mission, leaving every
ACCEPT button disabled on retry. Drop exact matching and, when a leftover
active mission exists, assert the UI state and complete that one instead of
re-accepting.
This commit is contained in:
2026-09-29 02:18:14 +00:00
parent 85eeeecc4a
commit b5a072c9aa
+35 -19
View File
@@ -290,30 +290,46 @@ test('MISSIONS tab accepts a contract and completing it grants the reward', asyn
const board = (await (await page.request.get('/api/missions')).json()) as MissionDto[]; const board = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
expect(board.filter((m) => m.state === 'AVAILABLE').length).toBeGreaterThanOrEqual(3); // the board is always topped up expect(board.filter((m) => m.state === 'AVAILABLE').length).toBeGreaterThanOrEqual(3); // the board is always topped up
// Prefer a scan-completable contract — fresh boards are all EXPLORE/FIND (DELIVER only appears once systems are discovered). await page.goto('/');
const picked = board.find((m) => m.state === 'AVAILABLE' && m.type !== 'DELIVER') ?? board.find((m) => m.state === 'AVAILABLE')!; await page.getByRole('tab', { name: 'MISSIONS' }).click();
const idx = board.indexOf(picked); const acceptButtons = page.getByRole('button', { name: 'ACCEPT' });
const panel = page.getByRole('tabpanel');
// Idempotent across retries sharing a DB: if a previous attempt already accepted a mission, it's still
// ACTIVE and every ACCEPT button is disabled — complete that one instead of accepting again.
const leftover = board.find((m) => m.state === 'ACTIVE');
let picked: MissionDto;
if (leftover) {
picked = leftover;
// No exact match — ACTIVE badges render a "●" dot prefix (components/ui/badge.tsx), so their text is "●ACTIVE".
await expect(panel.getByText('ACTIVE')).toBeVisible();
for (const button of await acceptButtons.all()) {
await expect(button).toBeDisabled();
}
} else {
// Prefer a scan-completable contract — fresh boards are all EXPLORE/FIND (DELIVER only appears once systems are discovered).
picked = board.find((m) => m.state === 'AVAILABLE' && m.type !== 'DELIVER') ?? board.find((m) => m.state === 'AVAILABLE')!;
const idx = board.indexOf(picked);
// One ACCEPT button per card, in board order — the picked contract's is enabled (AVAILABLE, nothing active).
await expect(acceptButtons.nth(idx)).toBeEnabled();
await acceptButtons.nth(idx).click();
// UI flipped: exactly one ACTIVE badge on the board and the accepted card's button is now disabled.
// No exact match — ACTIVE badges render a "●" dot prefix (components/ui/badge.tsx), so their text is "●ACTIVE".
await expect(panel.getByText('ACTIVE')).toBeVisible();
await expect(acceptButtons.nth(idx)).toBeDisabled();
// API confirms it: exactly one ACTIVE mission, the picked one (accepting cost a turn).
const boardAfterAccept = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
expect(boardAfterAccept.filter((m) => m.state === 'ACTIVE')).toEqual([boardAfterAccept.find((m) => m.id === picked.id)!]);
}
// Baseline of the reward kind in the hold — scans during completion may add more, so assert >=. // Baseline of the reward kind in the hold — scans during completion may add more, so assert >=.
const shipBefore = (await (await page.request.get('/api/ship')).json()) as ShipState; const shipBefore = (await (await page.request.get('/api/ship')).json()) as ShipState;
const baseline = totalQty(shipBefore.inventory, picked.rewardKind); const baseline = totalQty(shipBefore.inventory, picked.rewardKind);
await page.goto('/');
await page.getByRole('tab', { name: 'MISSIONS' }).click();
// One ACCEPT button per card, in board order — the picked contract's is enabled (AVAILABLE, nothing active).
const acceptButtons = page.getByRole('button', { name: 'ACCEPT' });
await expect(acceptButtons.nth(idx)).toBeEnabled();
await acceptButtons.nth(idx).click();
// UI flipped: exactly one ACTIVE badge on the board and the accepted card's button is now disabled.
const panel = page.getByRole('tabpanel');
await expect(panel.getByText('ACTIVE', { exact: true })).toBeVisible();
await expect(acceptButtons.nth(idx)).toBeDisabled();
// API confirms it: exactly one ACTIVE mission, the picked one (accepting cost a turn).
const boardAfterAccept = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
expect(boardAfterAccept.filter((m) => m.state === 'ACTIVE')).toEqual([boardAfterAccept.find((m) => m.id === picked.id)!]);
// Complete it. EXPLORE/FIND: scan until done, jumping when a system exhausts (parts drop on ~30% of // Complete it. EXPLORE/FIND: scan until done, jumping when a system exhausts (parts drop on ~30% of
// scans, so P(a FIND-part misses in 80 scans) ≈ 0.7^80 — negligible). DELIVER: follow the shortest // scans, so P(a FIND-part misses in 80 scans) ≈ 0.7^80 — negligible). DELIVER: follow the shortest
// jump path to its target (edges only ever get added, so the precomputed path stays valid). // jump path to its target (edges only ever get added, so the precomputed path stays valid).