Stage 7: regression tests for the polished UI (logic + behaviour)
Tests · UI / test (push) Successful in 14s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 14s

Lock the polish branch's behaviour so a future UI edit surfaces as a failing
assertion to re-agree or fix.

Unit (vitest, node env):
- placement: recallIndex, cellOccupied/isBlankSlot, non-linear direction, the
  single-tile submit default, and placementFromHint blank-fallback / rack-exhausted.
- banner: the marquee scroll-cycle repeat-then-advance, stop(), root-relative and
  multiple links.
- client.GatewayError. Extract the check-word constraints out of Game.svelte into a
  pure lib/checkword.ts (sanitize + canCheck) and cover them.

E2E (playwright mock, Chromium + WebKit):
- commit via the 🏁 control, history slide-down + close, the exchange dialog,
  check-word input sanitising + verdict, resign-to-finished, and the Settings
  board-label mode changing the on-board labels.
This commit is contained in:
Ilia Denisov
2026-06-03 17:33:47 +02:00
parent 4c475f2b0e
commit f8f7d39364
7 changed files with 267 additions and 5 deletions
+43
View File
@@ -1,12 +1,15 @@
import { describe, expect, it } from 'vitest';
import {
BLANK,
cellOccupied,
direction,
isBlankSlot,
newPlacement,
place,
placementFromHint,
rackView,
recallAt,
recallIndex,
reset,
toSubmit,
} from './placement';
@@ -62,6 +65,29 @@ describe('placement state machine', () => {
expect(toSubmit(place(newPlacement(rack), 0, 7, 7), 'V')?.dir).toBe('V');
expect(toSubmit(newPlacement(rack))).toBeNull();
});
it('recalls a tile by rack index and reports occupied cells / blank slots', () => {
let p = place(newPlacement(rack), 0, 7, 7);
p = place(p, 1, 7, 8);
expect(cellOccupied(p, 7, 7)).toBe(true);
expect(cellOccupied(p, 6, 6)).toBe(false);
p = recallIndex(p, 0);
expect(p.pending.map((t) => t.rackIndex)).toEqual([1]);
expect(isBlankSlot(newPlacement(rack), 2)).toBe(true); // '?' slot
expect(isBlankSlot(newPlacement(rack), 0)).toBe(false);
});
it('treats a non-linear placement as no inferred direction', () => {
let p = place(newPlacement(rack), 0, 7, 7);
p = place(p, 1, 8, 8); // diagonal
expect(direction(p)).toBeNull();
});
it('defaults a single-tile submit to H without an override', () => {
const sub = toSubmit(place(newPlacement(rack), 0, 7, 7));
expect(sub?.dir).toBe('H');
expect(sub?.tiles).toHaveLength(1);
});
});
describe('placementFromHint', () => {
@@ -78,4 +104,21 @@ describe('placementFromHint', () => {
expect(p.pending[0]).toMatchObject({ rackIndex: 0, letter: 'C', blank: false });
expect(p.pending[2]).toMatchObject({ rackIndex: 2, letter: 'B', blank: true });
});
it('falls back to a blank slot when the hint letter is not in the rack', () => {
const p = placementFromHint([{ row: 7, col: 7, letter: 'Z', blank: false }], ['A', BLANK]);
expect(p.pending).toHaveLength(1);
expect(p.pending[0]).toMatchObject({ rackIndex: 1, letter: 'Z', blank: true });
});
it('skips hint tiles once the rack is exhausted', () => {
const p = placementFromHint(
[
{ row: 7, col: 7, letter: 'A', blank: false },
{ row: 7, col: 8, letter: 'B', blank: false },
],
['A'],
);
expect(p.pending.map((t) => t.letter)).toEqual(['A']);
});
});