77a690fcf6
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
Remove the under-board status strip and relocate its signals: - bag count -> a badge on the exchange/pass control + the foot of the move table - whose-turn / win-lose -> a thin strip above the score plaques - the tentative-move caption -> the board itself: staged tiles tint green (legal) or pink (illegal), the board tiles a formed word runs through go a shade darker, and an orange score badge sits on the main word (digit sized like a tile value, clamped on-board) The word geometry (covered cells + badge anchor) is a new pure client-side helper (ui/src/lib/formed.ts), independent of the move evaluator, so it works on the local or network preview path alike; the badge's number still comes from the preview score. Rack: a seven-column grid filling the tray width in both layouts — square, full-width tiles — with the confirm control in the fixed 7th slot. Settings: a touch-only "Zoom the board" toggle (default on, device-local) gates the tile-placement auto-zoom; taking a hint while zoomed in now zooms out so the highlighted hint word is never left off-screen. Docs (FUNCTIONAL +_ru, UI_DESIGN, ARCHITECTURE) and e2e/unit tests updated.
108 lines
4.6 KiB
TypeScript
108 lines
4.6 KiB
TypeScript
import { expect, test } from './fixtures';
|
|
|
|
// Item 5: zooming the board must enlarge the labels too (a magnifying-glass zoom).
|
|
// cqw is sized against the zoom-scaled board, so the font grows with the cells.
|
|
test('zoom enlarges the board labels with the board', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await page.getByRole('button', { name: /Ann/ }).click();
|
|
|
|
const letter = page.locator('[data-cell] .letter').first();
|
|
await expect(letter).toBeVisible();
|
|
const base = await letter.evaluate((el) => parseFloat(getComputedStyle(el).fontSize));
|
|
|
|
// 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 page.waitForTimeout(400); // let the width transition settle
|
|
|
|
const zoomed = await letter.evaluate((el) => parseFloat(getComputedStyle(el).fontSize));
|
|
expect(zoomed).toBeGreaterThan(base * 1.4);
|
|
});
|
|
|
|
// Item 4 (UX): the zoomed board clamps at its edge — no native rubber-band/overscroll past
|
|
// the content. We assert the CSS contract on the zoomed scroll container, which is
|
|
// deterministic across engines (the bounce gesture itself is a native compositor behaviour
|
|
// Playwright cannot reliably synthesize).
|
|
test('zoomed board clamps overscroll at the edge', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await page.getByRole('button', { name: /Ann/ }).click();
|
|
|
|
// Double-tap an empty cell to zoom in.
|
|
await page
|
|
.locator('[data-cell]:not(.filled)')
|
|
.nth(20)
|
|
.evaluate((el: HTMLElement) => {
|
|
el.click();
|
|
el.click();
|
|
});
|
|
|
|
const viewport = page.locator('.viewport.zoomed');
|
|
await expect(viewport).toBeVisible();
|
|
const ob = await viewport.evaluate((el) => {
|
|
const s = getComputedStyle(el);
|
|
return { x: s.overscrollBehaviorX, y: s.overscrollBehaviorY };
|
|
});
|
|
expect(ob.x).toBe('none');
|
|
expect(ob.y).toBe('none');
|
|
});
|
|
|
|
// Placement auto-zoom (touch): in portrait, placing a tile magnifies into it. This is the
|
|
// counterpart of landscape.spec.ts's no-auto-zoom guard — together they lock the gate to
|
|
// orientation only. Skips on an engine whose touch emulation does not flip `(pointer: coarse)`.
|
|
test.describe('touch placement', () => {
|
|
test.use({ hasTouch: true });
|
|
|
|
test('placing a tile auto-zooms the board in portrait', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await page.getByRole('button', { name: /Ann/ }).click();
|
|
await expect(page.locator('[data-cell]').first()).toBeVisible();
|
|
test.skip(
|
|
!(await page.evaluate(() => matchMedia('(pointer: coarse)').matches)),
|
|
'touch emulation does not report a coarse pointer in this engine',
|
|
);
|
|
await page.locator('.rack .tile').first().click();
|
|
await page.locator('[data-cell][data-row="9"][data-col="6"]').click();
|
|
await expect(page.locator('.viewport.zoomed')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// A hint taken while the board is ALREADY zoomed in now zooms OUT to the whole board, so the played
|
|
// word — highlighted while composing — is guaranteed visible rather than parked off-screen under the
|
|
// old zoom (the owner changed this from the former recentre-on-the-word behaviour, so the word can
|
|
// never be lost off-screen). The mock hint plays at the centre star (7,7).
|
|
test('a hint zooms out an already-zoomed board so the played word is visible', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await page.getByRole('button', { name: /Ann/ }).click();
|
|
|
|
// Zoom into the top-left cell (far from the centre where the hint lands).
|
|
await page
|
|
.locator('[data-cell]:not(.filled)')
|
|
.first()
|
|
.evaluate((el: HTMLElement) => {
|
|
el.click();
|
|
el.click();
|
|
});
|
|
await expect(page.locator('.viewport.zoomed')).toBeVisible();
|
|
|
|
// Take a hint (the control confirms on a second tap), which plays at the centre.
|
|
const hint = page.getByRole('button', { name: 'Hint' });
|
|
await hint.click();
|
|
await hint.click();
|
|
|
|
// The board zoomed out — no zoomed viewport — with the hint's play staged (the confirm control
|
|
// shows). The mock hint plays on the centre star, which the seeded game already occupies, so the
|
|
// staged tile hides under the committed one; the confirm control is the reliable "a play is
|
|
// staged" signal.
|
|
await expect(page.locator('.viewport.zoomed')).toHaveCount(0);
|
|
await expect(page.locator('.make')).toBeVisible();
|
|
});
|