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
+44
View File
@@ -12,6 +12,12 @@ describe('linkify', () => {
expect(linkify('[x](ftp://evil)')).toBe('x');
expect(linkify('[y](javascript:boom)')).toBe('y');
});
it('keeps root-relative links and renders several in one string', () => {
expect(linkify('go [home](/lobby) or [docs](https://x.com)')).toBe(
'go <a href="/lobby" target="_blank" rel="noopener noreferrer">home</a> or ' +
'<a href="https://x.com" target="_blank" rel="noopener noreferrer">docs</a>',
);
});
});
describe('banner rotator', () => {
@@ -43,4 +49,42 @@ describe('banner rotator', () => {
expect(scrolled).toBe(1);
r.stop();
});
it('repeats the scroll cycle while under holdMs, then advances', () => {
vi.useFakeTimers();
const cfg = { holdMs: 2500, edgePauseMs: 100, fadeMs: 10, scrollPxPerSec: 100 };
const shown: number[] = [];
let scrolls = 0;
const r = createBannerRotator(
[{ md: 'long' }, { md: 'short' }],
{ overflowPx: (i) => (i === 0 ? 100 : 0), show: (i) => shown.push(i), scrollTo: () => scrolls++ },
cfg,
);
r.start();
vi.advanceTimersByTime(110); // fade(10) + edgePause(100) -> first scroll
expect(scrolls).toBe(1);
expect(shown).toEqual([0]);
vi.advanceTimersByTime(1200); // scrollDur(1000) + edgePause + edgePause -> re-show + second scroll
expect(scrolls).toBe(2);
expect(shown).toEqual([0, 0]); // re-shown to reset scroll, still item 0 (under holdMs)
vi.advanceTimersByTime(10_000);
expect(shown).toContain(1); // eventually exceeds holdMs and advances to the fitting message
r.stop();
});
it('stop() halts further advancement', () => {
vi.useFakeTimers();
const cfg = { ...defaultBannerConfig, holdMs: 100, fadeMs: 5, edgePauseMs: 5 };
const shown: number[] = [];
const r = createBannerRotator(
[{ md: 'a' }, { md: 'b' }],
{ overflowPx: () => 0, show: (i) => shown.push(i), scrollTo: () => {} },
cfg,
);
r.start();
vi.advanceTimersByTime(cfg.fadeMs);
r.stop();
vi.advanceTimersByTime(10_000);
expect(shown).toEqual([0]);
});
});