Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/editor-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ 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',
timeout: process.env.CI ? 120000 : 60000,
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,
Expand Down
125 changes: 125 additions & 0 deletions scripts/main-e2e-shards.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
41 changes: 41 additions & 0 deletions scripts/run-main-e2e-shard.cjs
Original file line number Diff line number Diff line change
@@ -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 <shard> [-- <playwright args...>]');
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);
2 changes: 1 addition & 1 deletion tests/e2e/formation-chevron-spacing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
6 changes: 1 addition & 5 deletions tests/e2e/project-picker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();
Expand Down
24 changes: 23 additions & 1 deletion tests/e2e/reload-recovers-latest-active-snapshot.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { expect, test } from '@playwright/test';
import { dismissViewHint, enablePersistenceDebug, expectPersistenceDebugEvents, expectProjectRestoreState, gotoStudio, openProjectScope } from './helpers';

async function expectStoredProjectTitle(page: Parameters<typeof test>[0]['page'], projectId: string, title: string): Promise<void> {
await expect.poll(async () => {
return page.evaluate(async (currentProjectId) => {
const openDb = () => new Promise<IDBDatabase>((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<any>((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 });
Expand All @@ -18,21 +38,23 @@ 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',
currentSceneId: 'scene-1',
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',
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/viewbar-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/wave-pattern-progress-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading