Merge pull request 'fix(game): no placement auto-zoom in landscape' (#158) from feature/landscape-no-autozoom into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 59s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m20s

This commit was merged in pull request #158.
This commit is contained in:
2026-07-02 15:19:24 +00:00
3 changed files with 53 additions and 6 deletions
+22
View File
@@ -53,3 +53,25 @@ test('landscape still places a tile and commits via the make button', async ({ p
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);
});
});
+21
View File
@@ -53,6 +53,27 @@ test('zoomed board clamps overscroll at the edge', async ({ page }) => {
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
+10 -6
View File
@@ -509,6 +509,7 @@
draggingPend = src.from === 'board' ? { row: src.row, col: src.col } : null;
// No zoom on drag start: the player may still change their mind. Holding the tile
// over a cell for ~1s auto-zooms there (hover-hold below); a drop also zooms+centres.
// Both auto-zooms are portrait-only — landscape fits the whole board.
}
if (!drag) return;
drag = { ...drag, x: e.clientX, y: e.clientY };
@@ -539,7 +540,7 @@
? setTimeout(() => {
// Still holding the tile over this cell: magnify into it. Only the first
// (zoom-in) hold centres; once zoomed we never move the board on hover.
if (drag && isCoarse() && !zoomed) {
if (drag && isCoarse() && !landscape && !zoomed) {
focus = c;
zoomed = true;
haptic('light');
@@ -652,7 +653,9 @@
if (board[row]?.[col]) return;
if (pendingMap.has(`${row},${col}`)) return;
focus = { row, col };
if (isCoarse() && !zoomed) zoomed = true;
// Auto-zoom is portrait-only: landscape fits the whole board, magnifying it there
// only hides the rest of the position.
if (isCoarse() && !landscape && !zoomed) zoomed = true;
if (placement.rack[index] === BLANK) {
blankPrompt = { rackIndex: index, row, col };
return;
@@ -844,12 +847,13 @@
row: Math.round((Math.min(...rows) + Math.max(...rows)) / 2),
col: Math.round((Math.min(...cols) + Math.max(...cols)) / 2),
};
// Ask the board to scroll to the hint word. A zoom-out board zooms in (and centres) on
// the next line; this nonce also recentres a board that is already zoomed in, where the
// unchanged zoom state would otherwise leave it parked where the player was looking.
// Ask the board to scroll to the hint word. A zoomed-out board zooms in (and centres)
// on the next line (portrait only); this nonce also recentres a board that is already
// zoomed in, where the unchanged zoom state would otherwise leave it parked where the
// player was looking.
recenter++;
}
if (isCoarse()) zoomed = true;
if (isCoarse() && !landscape) zoomed = true;
view = { ...view, hintsRemaining: h.hintsRemaining, walletBalance: h.walletBalance };
syncWallet(h.walletBalance);
// The hint is the engine's own top-ranked, fully scored legal move: reuse it as the