From 7a6a0b549edd5a4621e2306aa80dd0189141ffc9 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Wed, 15 Jul 2026 22:18:37 -0400 Subject: [PATCH] Fixed startup restore regression --- docs/getting-started/pattern-demo.md | 26 +++++++++---------- src/editor/projectPersistence.ts | 18 +++---------- ...cloud-reload-preserves-latest-head.spec.ts | 26 ++++++++++++++++--- tests/editor/projectPersistence.test.ts | 16 ++++++++---- 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/docs/getting-started/pattern-demo.md b/docs/getting-started/pattern-demo.md index 85e16117..93d17f71 100644 --- a/docs/getting-started/pattern-demo.md +++ b/docs/getting-started/pattern-demo.md @@ -54,7 +54,7 @@ Success check: **NOTE:** This will create one text entity called 't'. -2. Rough-place the text entity (i.e., drag it) near where the top-left label will go. +2. Rough-place the text entity (i.e., drag it) near the top-left quarter of the viewport. 3. Hit the F2 key and rename the text entity to "Wave" @@ -69,11 +69,18 @@ Success check: 6. Select the `Wave` label. In the Inspector's Transform panel, set `X = 130` and `Y = 120`. This anchors the label row to the same center point the first top-row ship will use later. +Selection bar close-up + +

Figure 5. On-canvas selection bar for multi-selection actions.

+ +Layout popover close-up + +

Figure 6. Layout popover for spacing and set-position operations.

+ 7. Drag-select the top-row labels, and in the popup Selection Bar, use `Layout …`: - Under Spacing, click `Pixels`, then type `180` in the `Spacing X` box, and click `Apply Spacing X`. - Under Advanced, level the labels with `Match top edges`. - - Under Position Selection, type `120` under `Y`, then click `Set Y`. - Click the "Close" button at the bottom of the Layout popup, or just click in a blank area of the canvas to close it. 8. Click a blank space somewhere in the canvas to deselect the top-row labels. @@ -84,12 +91,11 @@ Success check: - Under Spacing, click `Pixels`, then type `180` in the `Spacing X` box, and click `Apply Spacing X`. - Under Advanced, level the labels with `Match top edges`. - - Under Position Selection, type `340` under `Y`, then click `Set Y`. - Click the "Close" button at the bottom of the Layout popup, or just click in a blank area of the canvas to close it. Titles lined up -

Figure 5. Titles lined up - success check.

+

Figure 7. Titles lined up - success check.

Success check: - There is one top row with four labels @@ -127,7 +133,7 @@ Success check: Scene graph Sprites list with the seven renamed ships -

Figure 6. Scene graph Sprites list after renaming all seven ships.

+

Figure 8. Scene graph Sprites list after renaming all seven ships.

Success check: - You can see seven separate sprite entities in the scene graph. @@ -136,15 +142,8 @@ Success check: ## Position the Ships with Selection Tools and Layout -Selection bar close-up - -

Figure 7. On-canvas selection bar for multi-selection actions.

- -Layout popover close-up - -

Figure 8. Layout popover for spacing and set-position operations.

- 1. For the top row, drag-select the top four ships and click the `Layout…` button to change their positions as a group: + **NOTE:** A Layout pop-up window will appear. - Under Spacing, click `Pixels`, then type `180` in the `Spacing X` box, and hit `Apply Spacing X` - Under Align Selection, center the ships with `Center X` @@ -203,7 +202,6 @@ Success check: 10. Set the `Loop Body` step properties to the following: - `Type = Wave` - The other defaults are fine. -11. Click the Back arrow in the properties panel to return to the Actions list. Success check: - Click the `Play Mode` button in the upper right of the canvas to check that the ship moves in a wave pattern. diff --git a/src/editor/projectPersistence.ts b/src/editor/projectPersistence.ts index 8e181cfb..840fe81d 100644 --- a/src/editor/projectPersistence.ts +++ b/src/editor/projectPersistence.ts @@ -797,25 +797,13 @@ async function loadIndexedDbSnapshot(): Promise { const existingIndex = localProjects.findIndex((record) => record.id === latestActiveSnapshot.recordId); const snapshotRecord = existingIndex >= 0 ? localProjects[existingIndex] : null; const currentActiveRecord = localProjects.find((record) => record.id === workspace.activeProjectId) ?? null; - const snapshotMs = Date.parse(latestActiveSnapshot.updatedAt || latestActiveSnapshot.savedAt); - const currentActiveMs = Date.parse(currentActiveRecord?.updatedAt ?? ''); const snapshotLooksPlaceholder = isPlaceholderProjectRecord(snapshotRecord); - const currentActiveLooksPlaceholder = isPlaceholderProjectRecord(currentActiveRecord); const currentActiveLooksMeaningful = currentActiveRecord != null && !isPlaceholderProjectRecord(currentActiveRecord); const snapshotMatchesWorkspace = latestActiveSnapshot.recordId === workspace.activeProjectId; - const workspaceHasExplicitMeaningfulActiveProject = - Boolean(workspace.activeProjectId && currentActiveLooksMeaningful); - const shouldUseTimestampTiebreak = - !(snapshotLooksPlaceholder && currentActiveLooksMeaningful); const shouldPreferSnapshot = - !currentActiveRecord - || !workspace.activeProjectId - || snapshotMatchesWorkspace - || (!snapshotLooksPlaceholder && currentActiveLooksPlaceholder) - || (!workspaceHasExplicitMeaningfulActiveProject && shouldUseTimestampTiebreak && ( - !Number.isFinite(currentActiveMs) - || (Number.isFinite(snapshotMs) && snapshotMs >= currentActiveMs) - )); + snapshotMatchesWorkspace + || !snapshotLooksPlaceholder + || !currentActiveLooksMeaningful; if (shouldPreferSnapshot && snapshotRecord) { if (existingIndex > 0) { localProjects.splice(existingIndex, 1); diff --git a/tests/e2e/cloud-reload-preserves-latest-head.spec.ts b/tests/e2e/cloud-reload-preserves-latest-head.spec.ts index 8066f3e9..1befada5 100644 --- a/tests/e2e/cloud-reload-preserves-latest-head.spec.ts +++ b/tests/e2e/cloud-reload-preserves-latest-head.spec.ts @@ -49,8 +49,21 @@ test('cloud-backed active project reload restores the latest IndexedDB head and id: 'cloud:g1', projectId: latestProject.id, }; + const olderProject = structuredClone(sampleProject); + olderProject.title = 'Older Cloud Game'; + const olderRecord = { + ...buildStoredProjectRecord(olderProject, { + id: 'cloud:g-old', + updatedAt: staleUpdatedAt, + origin: 'cloud-cache', + syncStatus: 'cloud', + cloudProjectId: 'g-old', + }), + id: 'cloud:g-old', + projectId: olderProject.id, + }; - await page.addInitScript(async ({ record }) => { + await page.addInitScript(async ({ record, staleRecord }) => { const openDb = () => new Promise((resolve, reject) => { const request = window.indexedDB.open('phaserforge.persistence.v1', 1); request.onerror = () => reject(request.error); @@ -69,14 +82,21 @@ test('cloud-backed active project reload restores the latest IndexedDB head and tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.objectStore('projects').put(record); + tx.objectStore('projects').put(staleRecord); tx.objectStore('workspaceState').put({ - activeProjectId: 'cloud:g1', + activeProjectId: 'cloud:g-old', syncMode: 'online', }, 'workspace'); + tx.objectStore('workspaceState').put({ + recordId: 'cloud:g1', + updatedAt: record.updatedAt, + syncMode: 'online', + savedAt: record.updatedAt, + }, 'latestActiveSnapshot'); tx.objectStore('workspaceState').put('1', 'legacyMigrated'); }); window.localStorage.setItem('phaserforge.startupMode.v1', 'new_empty_scene'); - }, { record: latestRecord }); + }, { record: latestRecord, staleRecord: olderRecord }); await page.route('**/api/v1/auth/me', async (route) => { await route.fulfill({ diff --git a/tests/editor/projectPersistence.test.ts b/tests/editor/projectPersistence.test.ts index 9f033d3c..83a48d4a 100644 --- a/tests/editor/projectPersistence.test.ts +++ b/tests/editor/projectPersistence.test.ts @@ -972,14 +972,14 @@ describe('projectPersistence steady-state storage', () => { expect(snapshot.localProjects[0]?.title).toBe('Recovered Title'); }); - it('does not let a newer snapshot for a different saved project override the explicit workspace active project', async () => { + it('restores the newer durable active snapshot when the workspace pointer still names an older cloud project', async () => { const patternDemoProject = createEmptyProject(); patternDemoProject.id = 'project-pattern-demo'; - patternDemoProject.title = 'Pattern Demo'; + patternDemoProject.title = 'Older Cloud Game'; const newerDraftProject = createEmptyProject(); newerDraftProject.id = 'project-newer-draft'; - newerDraftProject.title = 'Scratch Draft'; + newerDraftProject.title = 'Pattern Demo'; const db = await openPersistenceDb(); try { @@ -990,10 +990,16 @@ describe('projectPersistence steady-state storage', () => { tx.objectStore('projects').put(buildStoredProjectRecord(patternDemoProject, { id: patternDemoProject.id, updatedAt: '2026-06-22T12:00:00.000Z', + origin: 'cloud-cache', + syncStatus: 'cloud', + cloudProjectId: 'game-older', })); tx.objectStore('projects').put(buildStoredProjectRecord(newerDraftProject, { id: newerDraftProject.id, updatedAt: '2026-06-22T12:05:00.000Z', + origin: 'cloud-cache', + syncStatus: 'cloud', + cloudProjectId: 'game-latest', })); tx.objectStore('workspaceState').put({ activeProjectId: patternDemoProject.id, @@ -1013,8 +1019,8 @@ describe('projectPersistence steady-state storage', () => { const snapshot = await projectPersistence.load(); - expect(snapshot.workspace.activeProjectId).toBe('project-pattern-demo'); - expect(snapshot.localProjects[0]?.id).toBe('project-pattern-demo'); + expect(snapshot.workspace.activeProjectId).toBe('project-newer-draft'); + expect(snapshot.localProjects[0]?.id).toBe('project-newer-draft'); expect(snapshot.localProjects[0]?.title).toBe('Pattern Demo'); });