Files
scrabble-game/ui/e2e/zoom.spec.ts
T
Ilia Denisov 622d3965a7
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 59s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m12s
fix(game): no placement auto-zoom in landscape
Landscape fits the whole board, so the coarse-pointer auto-zoom on tile
placement, drag hover-hold and hint only hid the rest of the position.
Gate all three on portrait; manual double-tap/pinch zoom is unchanged.
New e2e lock both sides: landscape placement stays unzoomed, portrait
placement still auto-zooms (touch-emulated, both engines).
2026-07-02 15:08:30 +02:00

110 lines
4.7 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 must still scroll to the played word — the
// zoom state does not change, so without an explicit recenter the board stays parked where the
// player was looking (the reported rough edge). The mock hint plays at the centre star (7,7), so
// zooming into the top-left corner first and then hinting must pan the board toward the centre.
test('a hint recentres an already-zoomed board on the played word', 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();
});
const viewport = page.locator('.viewport.zoomed');
await expect(viewport).toBeVisible();
await page.waitForTimeout(400); // let the zoom-in settle near the top-left corner
const before = await viewport.evaluate((el) => ({ left: el.scrollLeft, top: el.scrollTop }));
// 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();
await page.waitForTimeout(300); // the board pans to the hint word
const after = await viewport.evaluate((el) => ({ left: el.scrollLeft, top: el.scrollTop }));
// The board panned from the top-left toward the centre word: both offsets grew.
expect(after.left).toBeGreaterThan(before.left + 20);
expect(after.top).toBeGreaterThan(before.top + 20);
});