Promote development → master (initial production release: pre-release line + Stage 18) #104
@@ -43,6 +43,29 @@ test('a swipe-down on the zoom-out board opens the history', async ({ page }) =>
|
|||||||
await expect(page.locator('.boardwrap.slid')).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 }) => {
|
test('the history is a per-seat grid with move scores but no names or running total', async ({ page }) => {
|
||||||
await openGame(page);
|
await openGame(page);
|
||||||
await page.locator('.scoreboard').click(); // open the history (gesture is covered separately)
|
await page.locator('.scoreboard').click(); // open the history (gesture is covered separately)
|
||||||
|
|||||||
+12
-2
@@ -378,6 +378,10 @@
|
|||||||
// While a placed (pending) board tile is dragged to relocate it, draggingPend is its cell —
|
// While a placed (pending) board tile is dragged to relocate it, draggingPend is its cell —
|
||||||
// hidden from the board (the ghost stands in) like a lifted rack tile.
|
// hidden from the board (the ghost stands in) like a lifted rack tile.
|
||||||
let draggingPend = $state<{ row: number; col: number } | null>(null);
|
let draggingPend = $state<{ row: number; col: number } | null>(null);
|
||||||
|
// A board tile dragged over the rack (a recall in progress, the rack showing a drop gap): the
|
||||||
|
// move's ✅ confirm and its preview caption are hidden so they don't sit over the opening gap;
|
||||||
|
// they return the moment the tile is dragged back out over the board.
|
||||||
|
const recallOverRack = $derived(draggingPend != null && reorderTo != null);
|
||||||
|
|
||||||
let dragPointerId = -1;
|
let dragPointerId = -1;
|
||||||
function beginDrag(src: DragSrc, e: PointerEvent) {
|
function beginDrag(src: DragSrc, e: PointerEvent) {
|
||||||
@@ -398,6 +402,7 @@
|
|||||||
window.removeEventListener('pointermove', onWinMove);
|
window.removeEventListener('pointermove', onWinMove);
|
||||||
window.removeEventListener('pointerup', onWinUp);
|
window.removeEventListener('pointerup', onWinUp);
|
||||||
window.removeEventListener('pointerdown', onExtraPointer);
|
window.removeEventListener('pointerdown', onExtraPointer);
|
||||||
|
boardPointers.delete(dragPointerId); // see onWinUp: don't leave a stale boardwrap pointer
|
||||||
clearHover();
|
clearHover();
|
||||||
clearReorder();
|
clearReorder();
|
||||||
downInfo = null;
|
downInfo = null;
|
||||||
@@ -516,6 +521,11 @@
|
|||||||
window.removeEventListener('pointermove', onWinMove);
|
window.removeEventListener('pointermove', onWinMove);
|
||||||
window.removeEventListener('pointerup', onWinUp);
|
window.removeEventListener('pointerup', onWinUp);
|
||||||
window.removeEventListener('pointerdown', onExtraPointer);
|
window.removeEventListener('pointerdown', onExtraPointer);
|
||||||
|
// A board-tile drag also registered this pointer on the boardwrap (the tile sits inside it),
|
||||||
|
// but it can be released off the boardwrap (e.g. a recall dropped on the rack), where the
|
||||||
|
// boardwrap's own pointerup never fires. Reconcile it here so no stale id is left to make the
|
||||||
|
// next swipe read as multi-touch and kill the shuffle / history-pull gesture.
|
||||||
|
boardPointers.delete(e.pointerId);
|
||||||
clearHover();
|
clearHover();
|
||||||
const di = downInfo;
|
const di = downInfo;
|
||||||
downInfo = null;
|
downInfo = null;
|
||||||
@@ -1205,7 +1215,7 @@
|
|||||||
<span class="turn-ind">{isMyTurn ? t('game.yourTurn') : turnLabel()}</span>
|
<span class="turn-ind">{isMyTurn ? t('game.yourTurn') : turnLabel()}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="scores">
|
<span class="scores">
|
||||||
{#if preview}{preview.legal ? t('game.previewWords', { words: preview.words.join(', '), n: preview.score }) : t('game.previewIllegal')}{:else if !view.game.multipleWordsPerTurn}<span class="oneword" title={t('game.oneWordRule')}>1️⃣</span>{/if}
|
{#if recallOverRack}{:else if preview}{preview.legal ? t('game.previewWords', { words: preview.words.join(', '), n: preview.score }) : t('game.previewIllegal')}{:else if !view.game.multipleWordsPerTurn}<span class="oneword" title={t('game.oneWordRule')}>1️⃣</span>{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1227,7 +1237,7 @@
|
|||||||
ondown={onRackDown}
|
ondown={onRackDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !gameOver && placement.pending.length > 0}
|
{#if !gameOver && placement.pending.length > 0 && !recallOverRack}
|
||||||
<button class="make" onclick={commit} disabled={busy || !isMyTurn || !connection.online || !preview?.legal} aria-label={t('game.makeMove')}>✅</button>
|
<button class="make" onclick={commit} disabled={busy || !isMyTurn || !connection.online || !preview?.legal} aria-label={t('game.makeMove')}>✅</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rack" class:reordering={draggingId != null} class:landscape data-rack>
|
<div class="rack" class:reordering={draggingId != null || dropIndex != null} class:landscape data-rack>
|
||||||
{#each shown as slot, i (slot.id)}
|
{#each shown as slot, i (slot.id)}
|
||||||
<button
|
<button
|
||||||
class="tile"
|
class="tile"
|
||||||
|
|||||||
Reference in New Issue
Block a user