Files
scrabble-game/ui/e2e/history.spec.ts
T
Ilia Denisov 92700d0646
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m18s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m6s
feat(ui): open the move history at the newest move; scale the landscape rack glyphs
The move table fills top to bottom, oldest first, so it used to open on the
opening moves — the least interesting end of a long game. Pin it to its last
row when it is shown (the portrait drawer and the landscape dock alike), once
per showing rather than on every arriving move, so a player reading back
through the journal is not yanked to the bottom by an opponent's move.

The landscape tray shares the narrow left panel, so its tiles — and with them
the cqw-sized glyphs — come out markedly smaller than the full-width portrait
tray. Scale the letter and Erudit's blank star up a quarter there; the point
value keeps its size (it reads fine small, and growing it would crowd the
letter on a tile that size).
2026-07-27 21:11:34 +02:00

167 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, type Page } from './fixtures';
// The redesigned move history: a ruled matrix with one column per seat (no player names, no
// running total), opened by a swipe-down on the zoom-out board. The gesture is touch-only and
// the desktop projects have no touch input, so the swipe is dispatched as PointerEvents with
// pointerType:'touch'; a phone viewport so the board fills the width.
test.use({ viewport: { width: 390, height: 844 } });
async function openGame(page: Page): Promise<void> {
await page.goto('/');
await page.getByRole('button', { name: /guest/i }).click();
await page.getByRole('button', { name: /Ann/ }).click(); // the seeded active game G1 (your turn)
await expect(page.locator('[data-cell]').first()).toBeVisible();
await expect(page.locator('.pane')).toHaveCount(1); // let the screen-slide settle
}
/** touchSwipeDown dispatches a single-finger vertical touch drag on the board wrapper. */
async function touchSwipeDown(page: Page, fromY: number, toY: number): Promise<void> {
const box = (await page.locator('.boardwrap').boundingBox())!;
const x = box.x + box.width / 2;
await page.locator('.boardwrap').evaluate(
(el, { x, fromY, toY }) => {
const fire = (type: string, y: number) =>
el.dispatchEvent(
new PointerEvent(type, { pointerType: 'touch', clientX: x, clientY: y, bubbles: true, cancelable: true }),
);
fire('pointerdown', fromY);
for (let i = 1; i <= 8; i++) fire('pointermove', fromY + ((toY - fromY) * i) / 8);
fire('pointerup', toY);
},
{ x, fromY, toY },
);
}
test('a swipe-down on the zoom-out board opens the history', async ({ page }) => {
await openGame(page);
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0)); // the pull only opens at the top
const box = (await page.locator('.boardwrap').boundingBox())!;
await touchSwipeDown(page, box.y + 20, box.y + 180);
await expect(page.locator('.history')).toBeVisible();
await expect(page.locator('.boardwrap.slid')).toBeVisible();
});
test('a recall drag onto the rack does not break the next board swipe', async ({ page }) => {
await openGame(page);
// Place a tile, then drag it back onto the rack — the recall's pointerup lands off the
// boardwrap, which once left a stale pointer there so the next swipe read as multi-touch.
await page.locator('.rack .tile').first().click();
await page.locator('[data-cell]:not(.filled)').nth(30).click();
const pending = page.locator('[data-cell].pending');
await expect(pending).toHaveCount(1);
const fb = (await pending.first().boundingBox())!;
const slot = (await page.locator('[data-rack] .tile').nth(2).boundingBox())!;
await page.mouse.move(fb.x + fb.width / 2, fb.y + fb.height / 2);
await page.mouse.down();
await page.mouse.move(slot.x + 2, slot.y + slot.height / 2, { steps: 10 });
await page.mouse.up();
await expect(pending).toHaveCount(0);
// The swipe-down still opens the history (the recall pointer was reconciled).
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0));
const box = (await page.locator('.boardwrap').boundingBox())!;
await touchSwipeDown(page, box.y + 20, box.y + 180);
await expect(page.locator('.history')).toBeVisible();
});
test('the history is a per-seat grid with move scores but no names or running total', async ({ page }) => {
await openGame(page);
await page.locator('.scoreboard').click(); // open the history (gesture is covered separately)
const grid = page.locator('.hgrid');
await expect(grid).toBeVisible();
// G1: You [HELLO 16, RAT 3] vs Ann [WORLD 9, AND 4] — a 2×2 matrix, no empty/thinking cells.
await expect(page.locator('.hcell')).toHaveCount(4);
await expect(grid).toContainText('HELLO');
await expect(grid).toContainText('(3)'); // RAT's per-move score
// The running total (RAT's was 19) and the seat names no longer appear in the table.
await expect(grid).not.toContainText('19');
await expect(grid).not.toContainText('Ann');
});
test('a two-finger pinch does not open the history (pinch-zoom vs swipe-down)', async ({ page }) => {
await openGame(page);
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0));
const box = (await page.locator('.boardwrap').boundingBox())!;
const cx = box.x + box.width / 2;
const y0 = box.y + 40;
// Two fingers land, then spread apart while their centroid drifts down — a pinch-out whose
// downward component used to also slide the history open. With the single-pointer guard the
// second finger disarms the pull, so the history stays closed.
await page.locator('.boardwrap').evaluate(
(el, { cx, y0 }) => {
const fire = (type: string, id: number, x: number, y: number) =>
el.dispatchEvent(
new PointerEvent(type, {
pointerType: 'touch',
pointerId: id,
isPrimary: id === 1,
clientX: x,
clientY: y,
bubbles: true,
cancelable: true,
}),
);
fire('pointerdown', 1, cx - 20, y0);
fire('pointerdown', 2, cx + 20, y0);
for (let i = 1; i <= 8; i++) {
const d = i * 18;
fire('pointermove', 1, cx - 20 - d, y0 + d);
fire('pointermove', 2, cx + 20 + d, y0 + d);
}
fire('pointerup', 1, cx - 164, y0 + 144);
fire('pointerup', 2, cx + 164, y0 + 144);
},
{ cx, y0 },
);
await expect(page.locator('.history')).toHaveCount(0);
});
/** tallHistoryRows stretches the move-table cells so the seeded four-move game overflows the
* table's scroller — the condition under which "opens at the newest move" is observable at all
* (with no overflow the assertion would hold vacuously). Injected at document start, since the
* table is pinned as soon as it is shown. */
async function tallHistoryRows(page: Page): Promise<void> {
await page.addInitScript(() => {
document.addEventListener('DOMContentLoaded', () => {
const style = document.createElement('style');
style.textContent = '.hcell { min-height: 200px !important; }';
document.head.append(style);
});
});
}
test('the history opens scrolled to the newest move', async ({ page }) => {
await tallHistoryRows(page);
await openGame(page);
await page.locator('.scoreboard').click(); // open the history
const m = await page
.locator('.hgridwrap')
.evaluate((el) => ({ top: el.scrollTop, scroll: el.scrollHeight, client: el.clientHeight }));
expect(m.scroll).toBeGreaterThan(m.client); // the table really overflows — no vacuous pass
expect(m.top).toBeGreaterThanOrEqual(m.scroll - m.client - 1); // pinned to the last row
});
test('a swipe-down on the zoomed-in board does not open the history (native scroll wins)', async ({ page }) => {
await openGame(page);
// Double-tap an empty cell to zoom in (two synchronous clicks = a double-tap).
await page
.locator('[data-cell]:not(.filled)')
.nth(20)
.evaluate((el: HTMLElement) => {
el.click();
el.click();
});
await expect(page.locator('.viewport.zoomed')).toBeVisible();
const box = (await page.locator('.boardwrap').boundingBox())!;
await touchSwipeDown(page, box.y + 20, box.y + 180);
await expect(page.locator('.history')).toHaveCount(0);
});