Compare commits
2 Commits
f3914af793
...
553152e195
| Author | SHA1 | Date | |
|---|---|---|---|
| 553152e195 | |||
| 1cc6c0d56e |
@@ -76,3 +76,17 @@ test('quick game: a foreground resync recovers a join shed while the stream stay
|
|||||||
await expect(page.getByText('Robo')).toBeVisible();
|
await expect(page.getByText('Robo')).toBeVisible();
|
||||||
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
|
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression: an opponent joining must not freeze the screen. The in-game live-event effect tracked
|
||||||
|
// the `view` it also writes, so opponent_joined re-invalidated itself in a tight loop (opponent_moved
|
||||||
|
// was spared only by its delta's move-count idempotency). The board must still respond afterwards.
|
||||||
|
test('quick game: the board stays interactive after the opponent joins', async ({ page }) => {
|
||||||
|
await enterOpenGame(page);
|
||||||
|
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
|
||||||
|
await expect(page.getByText('Robo')).toBeVisible();
|
||||||
|
|
||||||
|
// Place a rack tile on the centre star: a frozen screen renders no pending tile.
|
||||||
|
await page.locator('.rack .tile').first().click();
|
||||||
|
await page.locator('[data-cell]:not(.filled)').nth(112).click(); // row 7, col 7
|
||||||
|
await expect(page.locator('[data-cell].pending')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -69,12 +69,16 @@
|
|||||||
// toggle or an explicit recenter request (the hint) moves it. A recenter with no zoom change
|
// toggle or an explicit recenter request (the hint) moves it. A recenter with no zoom change
|
||||||
// pans straight to `focus`, since the board width is stable and the width-driven tween below
|
// pans straight to `focus`, since the board width is stable and the width-driven tween below
|
||||||
// has nothing to ride (the case the owner hit: a hint taken while already zoomed in).
|
// has nothing to ride (the case the owner hit: a hint taken while already zoomed in).
|
||||||
// The previous zoom state, to tell a zoom toggle from a recenter request. The board always mounts
|
// prevZoom/prevRecenter let the effect tell a zoom toggle from a recenter request. The board
|
||||||
// zoomed-out, so it starts false and is updated to the live zoom on each effect run.
|
// always mounts zoomed-out, so prevZoom starts false; both are updated on each effect run.
|
||||||
let prevZoom = false;
|
let prevZoom = false;
|
||||||
|
let prevRecenter = 0;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const on = zoomed;
|
const on = zoomed;
|
||||||
void recenter; // re-run on an explicit recenter request even when the zoom state is unchanged
|
// An explicit "scroll to focus now" request (the hint). Read into a variable that is USED below,
|
||||||
|
// so the reactive dependency survives minification (a bare `void recenter` could be dropped) and
|
||||||
|
// an incidental re-run never pans on its own.
|
||||||
|
const rc = recenter;
|
||||||
const vp = viewport;
|
const vp = viewport;
|
||||||
if (!vp) return;
|
if (!vp) return;
|
||||||
const f = untrack(() => focus);
|
const f = untrack(() => focus);
|
||||||
@@ -91,11 +95,13 @@
|
|||||||
finalST = Math.max(0, Math.min((f.row + 0.5) * cell - clientH / 2, fullW - clientH));
|
finalST = Math.max(0, Math.min((f.row + 0.5) * cell - clientH / 2, fullW - clientH));
|
||||||
}
|
}
|
||||||
const toggled = on !== prevZoom;
|
const toggled = on !== prevZoom;
|
||||||
|
const recentered = rc !== prevRecenter;
|
||||||
prevZoom = on;
|
prevZoom = on;
|
||||||
|
prevRecenter = rc;
|
||||||
if (!toggled) {
|
if (!toggled) {
|
||||||
// No zoom change: an explicit recenter (the hint) pans straight to the focused word; a stale
|
// No zoom change: pan straight to the focused word only on an explicit recenter request (the
|
||||||
// run with nothing focused leaves the scroll alone.
|
// hint), never on an incidental re-run such as a viewport resize.
|
||||||
if (on && f) {
|
if (recentered && on && f) {
|
||||||
vp.scrollLeft = finalSL;
|
vp.scrollLeft = finalSL;
|
||||||
vp.scrollTop = finalST;
|
vp.scrollTop = finalST;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount, untrack } from 'svelte';
|
||||||
import Screen from '../components/Screen.svelte';
|
import Screen from '../components/Screen.svelte';
|
||||||
import TabBar from '../components/TabBar.svelte';
|
import TabBar from '../components/TabBar.svelte';
|
||||||
import TapConfirm from '../components/TapConfirm.svelte';
|
import TapConfirm from '../components/TapConfirm.svelte';
|
||||||
@@ -222,6 +222,14 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const e = app.lastEvent;
|
const e = app.lastEvent;
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
// Depend only on app.lastEvent: process each event once. The branches below READ and WRITE
|
||||||
|
// view/moves/placement, so tracking those reads would re-run this effect from its own
|
||||||
|
// `view = …` write — with app.lastEvent unchanged it re-enters the same branch, a tight loop.
|
||||||
|
// opponent_moved self-limits via the delta's move-count idempotency, but opponent_joined (and
|
||||||
|
// game_over) do not, so an opponent joining froze the whole game screen. (Tracking placement
|
||||||
|
// likewise re-fired the handler on every tile a player placed.) untrack scopes the body's reads
|
||||||
|
// out of the effect's dependencies.
|
||||||
|
untrack(() => {
|
||||||
if (e.kind === 'opponent_moved' && e.gameId === id) {
|
if (e.kind === 'opponent_moved' && e.gameId === id) {
|
||||||
// While composing, reload so a draft overlapping the new move is reconciled; otherwise apply
|
// While composing, reload so a draft overlapping the new move is reconciled; otherwise apply
|
||||||
// the move as a delta with no fetch.
|
// the move as a delta with no fetch.
|
||||||
@@ -246,6 +254,7 @@
|
|||||||
void loadFriends();
|
void loadFriends();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// The live-event hub is best-effort and never replays (see lib/app.svelte.ts): an event dropped
|
// The live-event hub is best-effort and never replays (see lib/app.svelte.ts): an event dropped
|
||||||
// while the stream is down — or shed from a full subscriber buffer — is gone for good.
|
// while the stream is down — or shed from a full subscriber buffer — is gone for good.
|
||||||
|
|||||||
Reference in New Issue
Block a user