diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index e2a14d4..8042a23 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1046,3 +1046,47 @@ test('NEXT TURN with an empty queue is a free skip — +1 turn, the strip stays expect(after.turn).toBe(T + 1); expect(after.tasks.length).toBe(0); }); + +test('NEXT TURN button is full-width in the right pane and not covered by the task strip (#52)', async ({ page }) => { + await drainQueue(page); + const world = (await (await page.request.get('/api/world')).json()) as WorldStateQ; + const currentId = world.systems.find((s) => s.current)!.id; + const edge = world.edges.find(([a, b]) => a === currentId || b === currentId)!; + const targetId = edge[0] === currentId ? edge[1] : edge[0]; + const { crew } = (await (await page.request.get('/api/crew')).json()) as { crew: CrewDto[]; tasks: QueueTaskDto[] }; + expect(crew.length).toBeGreaterThanOrEqual(2); + + // Queue a one-person JUMP and deliberately do not tick it (the #44 jump scenarios' pattern, + // the in-transit 409 one leaves it that way on purpose): the strip renders from the shell's + // system snapshot — the same queue — so a non-empty queue keeps ACTIVE TASKS visible all test. + const res = await page.request.post('/api/jump', { data: { systemId: targetId, leadCrewId: crew[0].id, helperCrewIds: [] } }); + expect(res.status()).toBe(200); + const queued = (await res.json()) as WorldStateQ; + expect(queued.tasks.find((t) => t.type === 'JUMP')!.status).toBe('ACTIVE'); + + await page.goto('/'); + await expect(page.getByText('ACTIVE TASKS', { exact: true })).toBeVisible(); // the strip is up + + // Two rects intersect when their x AND y ranges overlap (touching edges do not count). + type Box = { x: number; y: number; width: number; height: number }; + const intersects = (a: Box, b: Box) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height; + + const buttonBox = await page.getByRole('button', { name: 'NEXT TURN' }).boundingBox(); + expect(buttonBox).not.toBeNull(); + // The strip's root div is the parent of its 'ACTIVE TASKS' label (components/task-strip.tsx). + const stripBox = await page.getByText('ACTIVE TASKS', { exact: true }).locator('xpath=..').boundingBox(); + expect(stripBox).not.toBeNull(); + // The CARGO HOLD panel: the Panel root is the h3 heading's grandparent div (components/ui/panel.tsx). + const panelBox = await page.getByRole('heading', { name: 'CARGO HOLD // INVENTORY' }).locator('xpath=ancestor::div[2]').boundingBox(); + expect(panelBox).not.toBeNull(); + + // (a) The button spans the right pane's content width — >= 280 on its own, and within ±8px of + // the CARGO HOLD panel, a sibling full-width element in the same pane column. + expect(buttonBox!.width).toBeGreaterThanOrEqual(280); + expect(Math.abs(buttonBox!.width - panelBox!.width)).toBeLessThanOrEqual(8); + + // (b) The button's box does not intersect the active-tasks strip's box. + expect(intersects(buttonBox!, stripBox!)).toBe(false); + + await drainQueue(page); // leave the queue clean for the next scenario +});