import { expect, test, type Page } from './fixtures'; // The landscape (wide) layout: when the viewport is wider than tall, the game switches to a // two-column layout — the board fills the right side fitted to the height (no zoom), while the // rack, status, scores, the always-open history and the controls stack in a left panel. These // lock the key differences from the portrait layout so a future edit surfaces as a failing // assertion. The rest of the suite runs portrait (see playwright.config.ts); this spec overrides // the viewport to a wide, short one. test.use({ viewport: { width: 1100, height: 640 } }); async function openGame(page: Page): Promise { await page.goto('/'); await page.getByRole('button', { name: /guest/i }).click(); await page.getByRole('button', { name: /Ann/ }).click(); // the seeded active game vs Ann await expect(page.locator('[data-cell]').first()).toBeVisible(); } test('landscape lays the game in two columns with the history docked open', async ({ page }) => { await openGame(page); // The two-column landscape wrapper is mounted instead of the portrait stacking. await expect(page.locator('.game-land')).toBeVisible(); // The history is shown without tapping the score bar — it is docked open in the left panel, // not a slide-down drawer. await expect(page.locator('.history')).toBeVisible(); // The controls live in the left panel (no bottom tab bar in landscape). await expect(page.locator('.leftpane .tabbar')).toBeVisible(); }); test('landscape zooms the board on a double-tap and the zoomed board overflows the pane', async ({ page }) => { await openGame(page); // Double-tap an empty cell zooms in, same as portrait. The board is height-driven here, so the // zoomed square grows past the wide pane and becomes pannable (scrollWidth exceeds the viewport). 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(); await page.waitForTimeout(350); // let the width/height magnify settle const overflows = await viewport.evaluate((el) => el.scrollWidth > el.clientWidth + 5); expect(overflows).toBe(true); }); test('landscape still places a tile and commits via the make button', async ({ page }) => { await openGame(page); // Placement works the same in landscape: tap a rack tile, tap an empty cell adjacent to the // seeded word, and the make (✅) control appears next to the rack. await page.locator('.rack .tile').first().click(); await page.locator('[data-cell][data-row="9"][data-col="6"]').click(); await expect(page.locator('.cell.pending')).toHaveCount(1); await expect(page.locator('.make')).toBeVisible(); }); // Placement auto-zoom is portrait-only (Game.svelte gates it on !landscape): landscape fits the // whole board, so magnifying on every placed tile would only hide the position. The gate also // requires a coarse pointer, so the block emulates touch and skips on an engine whose emulation // does not flip `(pointer: coarse)` (desktop WebKit). The portrait counterpart lives in // zoom.spec.ts. test.describe('touch placement', () => { test.use({ hasTouch: true }); test('landscape does not auto-zoom when a tile is placed', async ({ page }) => { await openGame(page); 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('.cell.pending')).toHaveCount(1); await page.waitForTimeout(400); // the would-be zoom transition window await expect(page.locator('.viewport.zoomed')).toHaveCount(0); }); });