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); });