test(e2e): mission test — match dotted ACTIVE badge, idempotent across retries
CI / build + unit + e2e (pull_request) Successful in 2m25s
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:
+35
-19
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user