From b5a072c9aa85f735ab8176bc982618f115804964 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 02:18:14 +0000 Subject: [PATCH] =?UTF-8?q?test(e2e):=20mission=20test=20=E2=80=94=20match?= =?UTF-8?q?=20dotted=20ACTIVE=20badge,=20idempotent=20across=20retries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- e2e/smoke.spec.ts | 54 ++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 518f699..7e0c260 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -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[]; 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). - const picked = board.find((m) => m.state === 'AVAILABLE' && m.type !== 'DELIVER') ?? board.find((m) => m.state === 'AVAILABLE')!; - const idx = board.indexOf(picked); + await page.goto('/'); + await page.getByRole('tab', { name: 'MISSIONS' }).click(); + 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 >=. const shipBefore = (await (await page.request.get('/api/ship')).json()) as ShipState; 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 // 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).