b156d3403c
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
- The rack now animates the opening drop gap for a board-tile recall drag too
(the reorder transition was gated on a rack-source drag only).
- While a board tile is dragged over the rack (a recall, gap shown), the ✅
confirm and its preview caption are hidden so they don't sit over the gap;
they return if the tile is dragged back out over the board.
- A recall drop lands off the boardwrap, so its pointerup never reached the
boardwrap handler and left a stale id in the active-pointer set — the next
board swipe then read as multi-touch and the shuffle / history pull went dead.
Reconcile the pointer when a drag ends or cancels. Regression e2e added.
141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
import { expect, test, type Page } from './fixtures';
|
||
|
||
// The redesigned move history: a ruled matrix with one column per seat (no player names, no
|
||
// running total), opened by a swipe-down on the zoom-out board. The gesture is touch-only and
|
||
// the desktop projects have no touch input, so the swipe is dispatched as PointerEvents with
|
||
// pointerType:'touch'; a phone viewport so the board fills the width.
|
||
test.use({ viewport: { width: 390, height: 844 } });
|
||
|
||
async function openGame(page: Page): Promise<void> {
|
||
await page.goto('/');
|
||
await page.getByRole('button', { name: /guest/i }).click();
|
||
await page.getByRole('button', { name: /Ann/ }).click(); // the seeded active game G1 (your turn)
|
||
await expect(page.locator('[data-cell]').first()).toBeVisible();
|
||
await expect(page.locator('.pane')).toHaveCount(1); // let the screen-slide settle
|
||
}
|
||
|
||
/** touchSwipeDown dispatches a single-finger vertical touch drag on the board wrapper. */
|
||
async function touchSwipeDown(page: Page, fromY: number, toY: number): Promise<void> {
|
||
const box = (await page.locator('.boardwrap').boundingBox())!;
|
||
const x = box.x + box.width / 2;
|
||
await page.locator('.boardwrap').evaluate(
|
||
(el, { x, fromY, toY }) => {
|
||
const fire = (type: string, y: number) =>
|
||
el.dispatchEvent(
|
||
new PointerEvent(type, { pointerType: 'touch', clientX: x, clientY: y, bubbles: true, cancelable: true }),
|
||
);
|
||
fire('pointerdown', fromY);
|
||
for (let i = 1; i <= 8; i++) fire('pointermove', fromY + ((toY - fromY) * i) / 8);
|
||
fire('pointerup', toY);
|
||
},
|
||
{ x, fromY, toY },
|
||
);
|
||
}
|
||
|
||
test('a swipe-down on the zoom-out board opens the history', async ({ page }) => {
|
||
await openGame(page);
|
||
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0)); // the pull only opens at the top
|
||
const box = (await page.locator('.boardwrap').boundingBox())!;
|
||
|
||
await touchSwipeDown(page, box.y + 20, box.y + 180);
|
||
|
||
await expect(page.locator('.history')).toBeVisible();
|
||
await expect(page.locator('.boardwrap.slid')).toBeVisible();
|
||
});
|
||
|
||
test('a recall drag onto the rack does not break the next board swipe', async ({ page }) => {
|
||
await openGame(page);
|
||
// Place a tile, then drag it back onto the rack — the recall's pointerup lands off the
|
||
// boardwrap, which once left a stale pointer there so the next swipe read as multi-touch.
|
||
await page.locator('.rack .tile').first().click();
|
||
await page.locator('[data-cell]:not(.filled)').nth(30).click();
|
||
const pending = page.locator('[data-cell].pending');
|
||
await expect(pending).toHaveCount(1);
|
||
const fb = (await pending.first().boundingBox())!;
|
||
const slot = (await page.locator('[data-rack] .tile').nth(2).boundingBox())!;
|
||
await page.mouse.move(fb.x + fb.width / 2, fb.y + fb.height / 2);
|
||
await page.mouse.down();
|
||
await page.mouse.move(slot.x + 2, slot.y + slot.height / 2, { steps: 10 });
|
||
await page.mouse.up();
|
||
await expect(pending).toHaveCount(0);
|
||
|
||
// The swipe-down still opens the history (the recall pointer was reconciled).
|
||
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0));
|
||
const box = (await page.locator('.boardwrap').boundingBox())!;
|
||
await touchSwipeDown(page, box.y + 20, box.y + 180);
|
||
await expect(page.locator('.history')).toBeVisible();
|
||
});
|
||
|
||
test('the history is a per-seat grid with move scores but no names or running total', async ({ page }) => {
|
||
await openGame(page);
|
||
await page.locator('.scoreboard').click(); // open the history (gesture is covered separately)
|
||
|
||
const grid = page.locator('.hgrid');
|
||
await expect(grid).toBeVisible();
|
||
// G1: You [HELLO 16, RAT 3] vs Ann [WORLD 9, AND 4] — a 2×2 matrix, no empty/thinking cells.
|
||
await expect(page.locator('.hcell')).toHaveCount(4);
|
||
await expect(grid).toContainText('HELLO');
|
||
await expect(grid).toContainText('(3)'); // RAT's per-move score
|
||
// The running total (RAT's was 19) and the seat names no longer appear in the table.
|
||
await expect(grid).not.toContainText('19');
|
||
await expect(grid).not.toContainText('Ann');
|
||
});
|
||
|
||
test('a two-finger pinch does not open the history (pinch-zoom vs swipe-down)', async ({ page }) => {
|
||
await openGame(page);
|
||
await page.locator('.stage').evaluate((el) => (el.scrollTop = 0));
|
||
const box = (await page.locator('.boardwrap').boundingBox())!;
|
||
const cx = box.x + box.width / 2;
|
||
const y0 = box.y + 40;
|
||
|
||
// Two fingers land, then spread apart while their centroid drifts down — a pinch-out whose
|
||
// downward component used to also slide the history open. With the single-pointer guard the
|
||
// second finger disarms the pull, so the history stays closed.
|
||
await page.locator('.boardwrap').evaluate(
|
||
(el, { cx, y0 }) => {
|
||
const fire = (type: string, id: number, x: number, y: number) =>
|
||
el.dispatchEvent(
|
||
new PointerEvent(type, {
|
||
pointerType: 'touch',
|
||
pointerId: id,
|
||
isPrimary: id === 1,
|
||
clientX: x,
|
||
clientY: y,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
}),
|
||
);
|
||
fire('pointerdown', 1, cx - 20, y0);
|
||
fire('pointerdown', 2, cx + 20, y0);
|
||
for (let i = 1; i <= 8; i++) {
|
||
const d = i * 18;
|
||
fire('pointermove', 1, cx - 20 - d, y0 + d);
|
||
fire('pointermove', 2, cx + 20 + d, y0 + d);
|
||
}
|
||
fire('pointerup', 1, cx - 164, y0 + 144);
|
||
fire('pointerup', 2, cx + 164, y0 + 144);
|
||
},
|
||
{ cx, y0 },
|
||
);
|
||
|
||
await expect(page.locator('.history')).toHaveCount(0);
|
||
});
|
||
|
||
test('a swipe-down on the zoomed-in board does not open the history (native scroll wins)', async ({ page }) => {
|
||
await openGame(page);
|
||
// 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 expect(page.locator('.viewport.zoomed')).toBeVisible();
|
||
const box = (await page.locator('.boardwrap').boundingBox())!;
|
||
|
||
await touchSwipeDown(page, box.y + 20, box.y + 180);
|
||
|
||
await expect(page.locator('.history')).toHaveCount(0);
|
||
});
|