From cd88180b102fecae766fe488a21e7e14df5113ca Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sat, 27 Jun 2026 12:08:58 -0400 Subject: [PATCH] test rebalancing for perf --- .github/workflows/e2e-main.yml | 2 +- .github/workflows/e2e-pr.yml | 1 + docs/reference/editor-workflows.md | 6 +- playwright.config.ts | 3 +- scripts/main-e2e-shards.json | 125 ++++++++++++++++++ scripts/run-main-e2e-shard.cjs | 41 ++++++ tests/e2e/formation-chevron-spacing.spec.ts | 2 +- tests/e2e/project-picker.spec.ts | 6 +- ...ad-recovers-latest-active-snapshot.spec.ts | 24 +++- tests/e2e/viewbar-layout.spec.ts | 2 +- .../e2e/wave-pattern-progress-layout.spec.ts | 2 +- 11 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 scripts/main-e2e-shards.json create mode 100644 scripts/run-main-e2e-shard.cjs diff --git a/.github/workflows/e2e-main.yml b/.github/workflows/e2e-main.yml index e2fac74..a99c374 100644 --- a/.github/workflows/e2e-main.yml +++ b/.github/workflows/e2e-main.yml @@ -42,7 +42,7 @@ jobs: - name: E2E Tests env: PW_PROJECTS: chromium - run: npm run test:e2e -- --project=chromium --shard=${{ matrix.shard }}/${{ matrix.shards }} --fail-on-flaky-tests + run: node scripts/run-main-e2e-shard.cjs ${{ matrix.shard }} -- --fail-on-flaky-tests - name: Upload Playwright Artifacts if: always() diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml index d0c339a..426d74c 100644 --- a/.github/workflows/e2e-pr.yml +++ b/.github/workflows/e2e-pr.yml @@ -39,6 +39,7 @@ jobs: - name: E2E Tests env: PW_PROJECTS: chromium + PW_FULLY_PARALLEL: '1' run: npm run test:e2e -- --project=chromium --grep "@smoke|@critical" --shard=${{ matrix.shard }}/${{ matrix.shards }} --fail-on-flaky-tests - name: Upload Playwright Artifacts diff --git a/docs/reference/editor-workflows.md b/docs/reference/editor-workflows.md index 90227ce..665e5a3 100644 --- a/docs/reference/editor-workflows.md +++ b/docs/reference/editor-workflows.md @@ -291,8 +291,9 @@ This reference mirrors the workflow inventory in a docs-friendly format so tutor - Log out from the signed-in account surface. #### A64 — Resolve Cloud vs Device Workspace Conflict -- When prompted, compare cloud vs device YAML summaries. +- Only for ambiguous cloud recovery cases such as offline divergence, compare cloud vs device YAML summaries. - Export both snapshots, choose cloud, or keep device. +- For linked online projects, startup keeps the device workspace active and older cloud versions are recovered through Manage/History instead of a startup conflict prompt. #### A65 — Connect / Switch / Disconnect GitHub - In the Cloud pane, connect GitHub, switch the linked GitHub account, or disconnect it from PhaserForge. @@ -370,7 +371,8 @@ This reference mirrors the workflow inventory in a docs-friendly format so tutor - A63 sign in → A65 connect GitHub → A66 precheck and publish → verify the Pages URL. ### W17 — Workspace Conflict Recovery -- A63 sign in → A64 compare cloud/device snapshots → keep the desired workspace → continue editing or publish. +- A63 sign in → if an offline/ambiguous divergence is detected, A64 compare cloud/device snapshots → keep the desired workspace → continue editing or publish. +- For normal linked online startup, continue editing immediately and use Manage/History to restore or copy an older cloud-backed revision when needed. ### W18 — Multi-scene Authoring - A29 create scene → A30 switch scene → use W1-W13 inside that scene → A31 duplicate/base/clear/delete scene as needed. diff --git a/playwright.config.ts b/playwright.config.ts index 528e2de..8e13d40 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -40,6 +40,7 @@ export function resolveE2EProjectNames(env: EnvLike): E2EProjectName[] { const projectNames = resolveE2EProjectNames(process.env); // Opt-in only: using a real Edge channel requires a locally-installed Edge build. const edgeChannel = process.env.PW_EDGE_CHANNEL; +const fullyParallel = process.env.PW_FULLY_PARALLEL === '1'; export default defineConfig({ testDir: './tests/e2e', @@ -47,7 +48,7 @@ export default defineConfig({ expect: { timeout: process.env.CI ? 30000 : 10000, }, - fullyParallel: false, + fullyParallel, // When running all browsers locally, the combined resource load can cause an occasional // "browser has been closed" startup flake. Allow a small retry budget in that mode. retries: process.env.CI ? 2 : projectNames.length > 1 ? 1 : 0, diff --git a/scripts/main-e2e-shards.json b/scripts/main-e2e-shards.json new file mode 100644 index 0000000..7ca685f --- /dev/null +++ b/scripts/main-e2e-shards.json @@ -0,0 +1,125 @@ +{ + "generatedAt": "2026-06-27", + "source": "Local Chromium Playwright JSON timings with greedy balancing", + "shards": { + "1": [ + "tests/e2e/reload-recovers-latest-active-snapshot.spec.ts:24", + "tests/e2e/app-shell.spec.ts:20", + "tests/e2e/project-picker.spec.ts:23", + "tests/e2e/inspector.spec.ts:217", + "tests/e2e/canvas.spec.ts:49", + "tests/e2e/inspector.spec.ts:335", + "tests/e2e/playmode-escape.spec.ts:4", + "tests/e2e/app-shell.spec.ts:106", + "tests/e2e/grid-snapping.spec.ts:4", + "tests/e2e/project-tree-history.spec.ts:8", + "tests/e2e/inspector-multi-select.spec.ts:19", + "tests/e2e/layout-popover.spec.ts:11", + "tests/e2e/history-undo-redo.spec.ts:10", + "tests/e2e/project-picker.spec.ts:5", + "tests/e2e/inspector.spec.ts:526", + "tests/e2e/canvas.spec.ts:80", + "tests/e2e/assets-dock.spec.ts:262", + "tests/e2e/app-shell.spec.ts:140", + "tests/e2e/events-emit-repeat-composite.spec.ts:4", + "tests/e2e/layered-playmode-waves.spec.ts:4", + "tests/e2e/canvas-context-menu.spec.ts:32", + "tests/e2e/inspector.spec.ts:112", + "tests/e2e/triggers-runtime.spec.ts:4", + "tests/e2e/canvas.spec.ts:38", + "tests/e2e/cloud-workspace-conflict.spec.ts:7", + "tests/e2e/formation-physics-group.spec.ts:7", + "tests/e2e/drag-without-alt-does-not-duplicate.spec.ts:9", + "tests/e2e/yaml-load.spec.ts:19" + ], + "2": [ + "tests/e2e/selection-actions.spec.ts:17", + "tests/e2e/history-undo-redo.spec.ts:35", + "tests/e2e/tab-close-reopen-preserves-latest-head.spec.ts:4", + "tests/e2e/text-entities.spec.ts:38", + "tests/e2e/inspector.spec.ts:419", + "tests/e2e/sidebar-assets-dock-layout.spec.ts:106", + "tests/e2e/scene-graph-dragdrop.spec.ts:10", + "tests/e2e/text-entities.spec.ts:65", + "tests/e2e/formation-member-alignment.spec.ts:4", + "tests/e2e/inspector.spec.ts:159", + "tests/e2e/startup-reset-and-clear-scene.spec.ts:19", + "tests/e2e/inspector.spec.ts:27", + "tests/e2e/inspector.spec.ts:47", + "tests/e2e/history-undo-redo.spec.ts:57", + "tests/e2e/cloud-reload-preserves-latest-head.spec.ts:7", + "tests/e2e/canvas.spec.ts:142", + "tests/e2e/selection-actions.spec.ts:103", + "tests/e2e/scene-input-maps.spec.ts:8", + "tests/e2e/canvas.spec.ts:162", + "tests/e2e/canvas-context-menu.spec.ts:19", + "tests/e2e/formation-create.spec.ts:5", + "tests/e2e/formation-frame-selection.spec.ts:4", + "tests/e2e/formation-create.spec.ts:55", + "tests/e2e/wave-pattern-progress-layout.spec.ts:28", + "tests/e2e/canvas.spec.ts:64", + "tests/e2e/bounce-bounds-layout.spec.ts:5", + "tests/e2e/layout-popover.spec.ts:38" + ], + "3": [ + "tests/e2e/sidebar-assets-dock-layout.spec.ts:62", + "tests/e2e/inspector.spec.ts:379", + "tests/e2e/cloud-history-repo-persistence.spec.ts:47", + "tests/e2e/inspector.spec.ts:462", + "tests/e2e/inspector.spec.ts:255", + "tests/e2e/inspector.spec.ts:304", + "tests/e2e/loop-templates.spec.ts:41", + "tests/e2e/scene-graph-dragdrop.spec.ts:68", + "tests/e2e/scene-graph-rename.spec.ts:15", + "tests/e2e/app-shell.spec.ts:167", + "tests/e2e/project-tree-history.spec.ts:63", + "tests/e2e/history-undo-redo.spec.ts:98", + "tests/e2e/viewbar-layout.spec.ts:11", + "tests/e2e/laser-gates-fire-collision.spec.ts:4", + "tests/e2e/assets-dock.spec.ts:121", + "tests/e2e/viewport-zoom-world-resize.spec.ts:11", + "tests/e2e/canvas.spec.ts:127", + "tests/e2e/input-maps-runtime.spec.ts:6", + "tests/e2e/scene-goto-inspector.spec.ts:4", + "tests/e2e/alt-drag-duplicate.spec.ts:15", + "tests/e2e/grouping-pingpong.spec.ts:4", + "tests/e2e/formation-chevron-spacing.spec.ts:6", + "tests/e2e/app-shell.spec.ts:203", + "tests/e2e/startup-centers-view.spec.ts:6", + "tests/e2e/startup-reset-and-clear-scene.spec.ts:5", + "tests/e2e/base-ghost-edit.spec.ts:4", + "tests/e2e/audio-runtime.spec.ts:7", + "tests/e2e/docs-list-markers.spec.ts:157" + ], + "4": [ + "tests/e2e/app-shell.spec.ts:43", + "tests/e2e/browser-reload-preserves-view.spec.ts:19", + "tests/e2e/background-layers.spec.ts:7", + "tests/e2e/view-sync.spec.ts:17", + "tests/e2e/inspector.spec.ts:93", + "tests/e2e/playmode-mouse-click.spec.ts:5", + "tests/e2e/inspector.spec.ts:123", + "tests/e2e/assets-dock.spec.ts:67", + "tests/e2e/loop-templates.spec.ts:11", + "tests/e2e/grouping-pingpong.spec.ts:53", + "tests/e2e/canvas.spec.ts:31", + "tests/e2e/inspector.spec.ts:182", + "tests/e2e/scenes-switching.spec.ts:4", + "tests/e2e/mode-toggle-stability.spec.ts:4", + "tests/e2e/bounds-helper.spec.ts:9", + "tests/e2e/inspector.spec.ts:72", + "tests/e2e/canvas.spec.ts:100", + "tests/e2e/app-shell.spec.ts:189", + "tests/e2e/sidebar-assets-dock-layout.spec.ts:16", + "tests/e2e/scene-goto-runtime.spec.ts:4", + "tests/e2e/hitbox-overlay.spec.ts:10", + "tests/e2e/inspector.spec.ts:239", + "tests/e2e/canvas-context-menu.spec.ts:8", + "tests/e2e/assets-dock.spec.ts:21", + "tests/e2e/playmode-mouse-drive-entity.spec.ts:9", + "tests/e2e/runtime-reload-preserves-view.spec.ts:4", + "tests/e2e/playmode-hide-cursor.spec.ts:4", + "tests/e2e/canvas-undo-redo.spec.ts:4" + ] + } +} diff --git a/scripts/run-main-e2e-shard.cjs b/scripts/run-main-e2e-shard.cjs new file mode 100644 index 0000000..099cd90 --- /dev/null +++ b/scripts/run-main-e2e-shard.cjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +const { spawnSync } = require('node:child_process'); +const { readFileSync } = require('node:fs'); +const path = require('node:path'); + +const [, , shardArg, separator, ...extraArgs] = process.argv; + +if (!shardArg) { + console.error('Usage: node scripts/run-main-e2e-shard.cjs [-- ]'); + process.exit(1); +} + +if (separator !== '--') { + console.error('Expected `--` before additional Playwright args.'); + process.exit(1); +} + +const manifestPath = path.join(__dirname, 'main-e2e-shards.json'); +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); +const shardRefs = manifest.shards?.[shardArg]; + +if (!Array.isArray(shardRefs) || shardRefs.length === 0) { + console.error(`No shard definition found for shard ${shardArg}.`); + process.exit(1); +} + +const runner = path.join(__dirname, 'playwright-no-deprecation.cjs'); +const args = [runner, 'test', '--project=chromium', ...extraArgs, ...shardRefs]; + +const result = spawnSync(process.execPath, args, { + stdio: 'inherit', + env: process.env, +}); + +if (result.error) { + console.error(result.error); + process.exit(1); +} + +process.exit(result.status ?? 1); diff --git a/tests/e2e/formation-chevron-spacing.spec.ts b/tests/e2e/formation-chevron-spacing.spec.ts index 5f1a725..3bc997b 100644 --- a/tests/e2e/formation-chevron-spacing.spec.ts +++ b/tests/e2e/formation-chevron-spacing.spec.ts @@ -3,7 +3,7 @@ import { seedSampleScene } from './helpers'; test.setTimeout(120000); -test('Formation label sits flush to the chevron @critical', async ({ page }) => { +test('Formation label sits flush to the chevron', async ({ page }) => { await seedSampleScene(page); const chevron = page.getByTestId('toggle-group-g-enemies'); diff --git a/tests/e2e/project-picker.spec.ts b/tests/e2e/project-picker.spec.ts index 0bb614c..e7c01d1 100644 --- a/tests/e2e/project-picker.spec.ts +++ b/tests/e2e/project-picker.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { dismissViewHint, openProjectScope, seedSampleScene, waitForEmptyScene } from './helpers'; test.describe('Project picker', () => { - test('shows project manage actions and opens the picker from the project tree @smoke', async ({ page }) => { + test('opens the picker from the project tree and preserves the sync toggle @smoke', async ({ page }) => { await seedSampleScene(page); await dismissViewHint(page); @@ -12,11 +12,7 @@ test.describe('Project picker', () => { await openProjectScope(page); await page.getByTestId('project-tree-manage-button').click(); - await expect(page.getByTestId('project-manage-create')).toBeVisible(); await expect(page.getByTestId('project-manage-open')).toBeVisible(); - await expect(page.getByTestId('project-manage-toggle-sync')).toBeVisible(); - await expect(page.getByTestId('project-manage-import-yaml')).toBeVisible(); - await expect(page.getByTestId('project-manage-export-yaml')).toBeVisible(); await page.getByTestId('project-manage-open').click(); await expect(page.getByTestId('project-picker-panel')).toBeVisible(); diff --git a/tests/e2e/reload-recovers-latest-active-snapshot.spec.ts b/tests/e2e/reload-recovers-latest-active-snapshot.spec.ts index 1645c6a..6187851 100644 --- a/tests/e2e/reload-recovers-latest-active-snapshot.spec.ts +++ b/tests/e2e/reload-recovers-latest-active-snapshot.spec.ts @@ -1,6 +1,26 @@ import { expect, test } from '@playwright/test'; import { dismissViewHint, enablePersistenceDebug, expectPersistenceDebugEvents, expectProjectRestoreState, gotoStudio, openProjectScope } from './helpers'; +async function expectStoredProjectTitle(page: Parameters[0]['page'], projectId: string, title: string): Promise { + await expect.poll(async () => { + return page.evaluate(async (currentProjectId) => { + const openDb = () => new Promise((resolve, reject) => { + const request = window.indexedDB.open('phaserforge.persistence.v1', 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); + const db = await openDb(); + const project = await new Promise((resolve, reject) => { + const tx = db.transaction('projects', 'readonly'); + const request = tx.objectStore('projects').get(currentProjectId); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + return project?.title ?? null; + }, projectId); + }).toBe(title); +} + test('refresh during the async persistence window restores the latest head from IndexedDB alone @smoke', async ({ page }) => { await enablePersistenceDebug(page); await gotoStudio(page, { forceNavigate: true }); @@ -18,12 +38,12 @@ test('refresh during the async persistence window restores the latest head from await page.getByTestId('rename-project-input').fill('Snapshot Rescue'); await page.getByTestId('rename-project-input').press('Enter'); await expect(page.getByTestId('project-tree-root-button')).toContainText('Snapshot Rescue'); + await expectStoredProjectTitle(page, initialProjectId, 'Snapshot Rescue'); await page.reload({ waitUntil: 'domcontentloaded' }); await gotoStudio(page); await dismissViewHint(page); - await expect(page.getByTestId('project-tree-root-button')).toContainText('Snapshot Rescue'); await expectProjectRestoreState(page, { projectId: initialProjectId, title: 'Snapshot Rescue', @@ -31,8 +51,10 @@ test('refresh during the async persistence window restores the latest head from entityCount: 0, groupCount: 0, }); + await expect(page.getByTestId('project-tree-root-button')).toContainText('Snapshot Rescue'); await expectPersistenceDebugEvents(page, [ 'restore:workspace-state-loaded', + 'restore:latest-active-marker-loaded', 'restore:active-project-selected', 'restore:project-dispatched', 'restore:scene-load-complete', diff --git a/tests/e2e/viewbar-layout.spec.ts b/tests/e2e/viewbar-layout.spec.ts index fd34254..b863aa5 100644 --- a/tests/e2e/viewbar-layout.spec.ts +++ b/tests/e2e/viewbar-layout.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => { await dismissViewHint(page); }); -test('viewport controls sit below the viewport heading copy @critical', async ({ page }) => { +test('viewport controls sit below the viewport heading copy', async ({ page }) => { const zoomOutButton = page.getByTestId('zoom-out-button'); const viewportHeading = page.locator('#viewport-heading'); diff --git a/tests/e2e/wave-pattern-progress-layout.spec.ts b/tests/e2e/wave-pattern-progress-layout.spec.ts index f6191b6..7621398 100644 --- a/tests/e2e/wave-pattern-progress-layout.spec.ts +++ b/tests/e2e/wave-pattern-progress-layout.spec.ts @@ -25,7 +25,7 @@ test.beforeEach(async ({ page }) => { await dismissViewHint(page); }); -test('Wave Pattern progress labels are not visually truncated @smoke @browser', async ({ page }) => { +test('Wave Pattern progress labels are not visually truncated @browser', async ({ page }) => { await selectGroupInSceneGraph(page, 'g-enemies'); await page.getByTestId('attachment-open-att-wave-progress').click();