Skip to content

Commit 8e8b70d

Browse files
author
DavidQ
committed
Add full Workspace V2 Playwright coverage including lifecycle, validation, and roundtrip integrity - PR_11_323
1 parent 7baa5ac commit 8e8b70d

6 files changed

Lines changed: 324 additions & 1 deletion

File tree

docs/dev/codex_commands.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ PR_11_322
230230
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_322: Run Workspace V2 Playwright tests automatically via CI."
231231
```
232232

233+
---
234+
PR_11_323
235+
236+
```bash
237+
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_323: Add full Workspace V2 Playwright coverage beyond Asset Manager."
238+
```
239+
233240
---
234241
PR_11_321
235242

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add GitHub Actions Workspace V2 Playwright gate with artifact upload from tests/results - PR 11.322
1+
Add Workspace V2 Playwright validation coverage tests for lifecycle, palette contract, validation rejection, roundtrip integrity, and tool switching - PR 11.323
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# PR_11_323 Report
2+
3+
## Scope
4+
- Tests only (`tests/playwright/*`) for expanded Workspace V2 Playwright validation coverage.
5+
- No runtime code changes.
6+
- No schema changes.
7+
8+
## Files Changed
9+
- `tests/playwright/workspace-v2.validation.spec.js`
10+
- `docs/pr/PR_11_323_WORKSPACE_V2_PLAYWRIGHT_COVERAGE/PLAN_PR.md`
11+
- `docs/pr/PR_11_323_WORKSPACE_V2_PLAYWRIGHT_COVERAGE/BUILD_PR.md`
12+
- `docs/dev/reports/PR_11_323_report.md`
13+
- `docs/dev/codex_commands.md`
14+
- `docs/dev/commit_comment.txt`
15+
16+
## Coverage Added
17+
- Workspace lifecycle baseline/import/export/import.
18+
- Palette ownership contract checks (`tools.palette-browser`, single active palette key path).
19+
- Invalid manifest and invalid payload rejection behavior.
20+
- Roundtrip equality checks for load -> export -> import.
21+
- Tool-switch active session consistency checks.
22+
23+
## Validation Run
24+
- `node --check tests/playwright/workspace-v2.validation.spec.js`
25+
- `npm run test:workspace-v2`
26+
27+
## Validation Results
28+
- PASS: syntax check for new Playwright spec.
29+
- PASS: Workspace V2 Playwright gate command completed successfully (`passed=1 failed=0`).
30+
31+
## Full Samples Smoke
32+
- Skipped intentionally (targeted test-only PR; no runtime/shared sample framework changes).
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# BUILD_PR_11_323
2+
3+
## Implementation
4+
- Added new Playwright validation coverage spec:
5+
- `tests/playwright/workspace-v2.validation.spec.js`
6+
- Added 5 tests for Workspace V2:
7+
1. Lifecycle:
8+
- full reset baseline manifest
9+
- valid import success
10+
- export then import success
11+
2. Palette contract:
12+
- `tools.palette-browser` exists after reset
13+
- `swatches` is `[]`
14+
- no plural/duplicate palette keys (`palette` / `palettes`)
15+
3. Validation rejection:
16+
- invalid workspace JSON rejected
17+
- invalid `activeSession.payloadJson` rejected
18+
- no partial state progression (stays on workspace page, no recent sessions)
19+
4. Roundtrip integrity:
20+
- load fixture -> export -> import
21+
- object equality assertions (no mutation)
22+
5. Tool switching consistency:
23+
- fixture-load by tool updates `tools.workspace-v2.activeToolId`
24+
- `activeSession.toolId` stays aligned with selected fixture tool
25+
26+
## Validation
27+
- `node --check tests/playwright/workspace-v2.validation.spec.js`
28+
- `npm run test:workspace-v2`
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# PLAN_PR_11_323
2+
3+
## Purpose
4+
Add broader Workspace V2 Playwright coverage beyond Asset Manager-focused flow, using tests-only changes.
5+
6+
## Scope
7+
- `tests/playwright/workspace-v2.validation.spec.js`
8+
- `docs/pr/PR_11_323_WORKSPACE_V2_PLAYWRIGHT_COVERAGE/PLAN_PR.md`
9+
- `docs/pr/PR_11_323_WORKSPACE_V2_PLAYWRIGHT_COVERAGE/BUILD_PR.md`
10+
- `docs/dev/reports/PR_11_323_report.md`
11+
- `docs/dev/codex_commands.md`
12+
- `docs/dev/commit_comment.txt`
13+
14+
## Steps
15+
1. Add a deterministic Playwright spec under `tests/playwright/*`.
16+
2. Cover:
17+
- workspace lifecycle
18+
- palette contract
19+
- invalid JSON / invalid payload rejection
20+
- load/export/import roundtrip integrity
21+
- tool switching active session consistency
22+
3. Keep runtime/schemas unchanged.
23+
4. Run targeted validation:
24+
- `node --check` on changed test file
25+
- `npm run test:workspace-v2`
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { test, expect } from "@playwright/test";
2+
import fs from "node:fs/promises";
3+
import path from "node:path";
4+
import http from "node:http";
5+
import { fileURLToPath } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
11+
function contentTypeForPath(filePath) {
12+
const extension = path.extname(filePath).toLowerCase();
13+
if (extension === ".html") return "text/html; charset=utf-8";
14+
if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8";
15+
if (extension === ".json") return "application/json; charset=utf-8";
16+
if (extension === ".css") return "text/css; charset=utf-8";
17+
if (extension === ".svg") return "image/svg+xml";
18+
return "application/octet-stream";
19+
}
20+
21+
async function startRepoServer() {
22+
const server = http.createServer(async (request, response) => {
23+
try {
24+
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
25+
const decodedPath = decodeURIComponent(requestUrl.pathname);
26+
const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, "");
27+
const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`);
28+
if (!absolutePath.startsWith(repoRoot)) {
29+
response.statusCode = 403;
30+
response.end("Forbidden");
31+
return;
32+
}
33+
let targetPath = absolutePath;
34+
const stat = await fs.stat(targetPath).catch(() => null);
35+
if (stat && stat.isDirectory()) {
36+
targetPath = path.join(targetPath, "index.html");
37+
}
38+
const fileContents = await fs.readFile(targetPath);
39+
response.statusCode = 200;
40+
response.setHeader("Content-Type", contentTypeForPath(targetPath));
41+
response.end(fileContents);
42+
} catch {
43+
response.statusCode = 404;
44+
response.end("Not Found");
45+
}
46+
});
47+
await new Promise((resolve, reject) => {
48+
server.listen(0, "127.0.0.1", () => resolve());
49+
server.on("error", reject);
50+
});
51+
const address = server.address();
52+
if (!address || typeof address === "string") {
53+
throw new Error("Failed to start UI test server.");
54+
}
55+
return {
56+
baseUrl: `http://127.0.0.1:${address.port}`,
57+
close: async () => {
58+
await new Promise((resolve, reject) => {
59+
server.close((error) => {
60+
if (error) reject(error);
61+
else resolve();
62+
});
63+
});
64+
}
65+
};
66+
}
67+
68+
async function importManifestFromObject(page, manifest) {
69+
const chooserPromise = page.waitForEvent("filechooser");
70+
await page.getByRole("button", { name: "Import Workspace Session JSON" }).click();
71+
const chooser = await chooserPromise;
72+
const jsonText = JSON.stringify(manifest, null, 2);
73+
await chooser.setFiles({
74+
name: "workspace-v2-import.json",
75+
mimeType: "application/json",
76+
buffer: Buffer.from(jsonText, "utf8")
77+
});
78+
return jsonText;
79+
}
80+
81+
async function exportManifestFromTextarea(page) {
82+
await page.getByRole("button", { name: "Export Workspace Session JSON" }).click();
83+
const exportedText = await page.locator("#workspaceV2ImportJson").inputValue();
84+
return JSON.parse(exportedText);
85+
}
86+
87+
test.describe("Workspace V2 validation coverage", () => {
88+
test("workspace lifecycle reset -> valid import -> export -> import success", async ({ page }) => {
89+
const server = await startRepoServer();
90+
try {
91+
await page.goto(`${server.baseUrl}/tools/workspace-v2/index.html`);
92+
await page.getByRole("button", { name: "Full Reset" }).click();
93+
await expect(page.locator("#workspaceV2WorkspaceToolsSummary")).toContainText("palette-browser");
94+
await expect(page.locator("#workspaceV2WorkspaceToolsSummary")).toContainText("workspace-v2");
95+
96+
const baselineManifest = JSON.parse(await page.locator("#workspaceV2ImportJson").inputValue());
97+
expect(baselineManifest.documentKind).toBe("workspace-manifest");
98+
expect(baselineManifest.tools?.["palette-browser"]).toBeTruthy();
99+
expect(baselineManifest.tools?.["workspace-v2"]).toBeTruthy();
100+
101+
await importManifestFromObject(page, baselineManifest);
102+
await expect(page.locator("#workspaceV2ImportExportStatus")).toHaveText("Workspace session imported.");
103+
104+
const exportedManifest = await exportManifestFromTextarea(page);
105+
expect(exportedManifest.documentKind).toBe("workspace-manifest");
106+
107+
await importManifestFromObject(page, exportedManifest);
108+
await expect(page.locator("#workspaceV2ImportExportStatus")).toHaveText("Workspace session imported.");
109+
} finally {
110+
await server.close();
111+
}
112+
});
113+
114+
test("palette contract: palette-browser exists, swatches empty, one palette entry", async ({ page }) => {
115+
const server = await startRepoServer();
116+
try {
117+
await page.goto(`${server.baseUrl}/tools/workspace-v2/index.html`);
118+
await page.getByRole("button", { name: "Full Reset" }).click();
119+
120+
const manifest = JSON.parse(await page.locator("#workspaceV2ImportJson").inputValue());
121+
expect(manifest.tools?.["palette-browser"]).toBeTruthy();
122+
expect(Array.isArray(manifest.tools["palette-browser"].swatches)).toBe(true);
123+
expect(manifest.tools["palette-browser"].swatches).toEqual([]);
124+
expect(Object.prototype.hasOwnProperty.call(manifest.tools, "palettes")).toBe(false);
125+
expect(Object.prototype.hasOwnProperty.call(manifest.tools, "palette")).toBe(false);
126+
expect(Object.keys(manifest.tools).filter((key) => key === "palette-browser")).toHaveLength(1);
127+
} finally {
128+
await server.close();
129+
}
130+
});
131+
132+
test("validation rejection: invalid workspace JSON and invalid payloadJson are rejected", async ({ page }) => {
133+
const server = await startRepoServer();
134+
try {
135+
await page.goto(`${server.baseUrl}/tools/workspace-v2/index.html`);
136+
await page.getByRole("button", { name: "Full Reset" }).click();
137+
await expect(page.locator("#workspaceV2SessionHistoryEmptyState")).toHaveText("No recent sessions.");
138+
139+
await importManifestFromObject(page, {
140+
schema: "html-js-gaming.project",
141+
version: 1,
142+
id: "bad-manifest",
143+
name: "Bad Manifest",
144+
tools: {}
145+
});
146+
await expect(page.locator("#workspaceV2ImportExportStatus")).toContainText("Import error:");
147+
await expect(page.locator("#workspaceV2SessionHistoryEmptyState")).toHaveText("No recent sessions.");
148+
149+
const invalidPayloadManifest = {
150+
documentKind: "workspace-manifest",
151+
schema: "html-js-gaming.project",
152+
version: 1,
153+
id: "bad-payload",
154+
name: "Bad Payload",
155+
tools: {
156+
"palette-browser": {
157+
schema: "html-js-gaming.palette",
158+
version: 1,
159+
name: "Workspace Active Palette",
160+
swatches: []
161+
},
162+
"workspace-v2": {
163+
schema: "html-js-gaming.workspace-v2-session/1",
164+
game: {
165+
id: "game-bad",
166+
name: "Bad Game"
167+
},
168+
defaultToolId: "asset-manager-v2",
169+
activeToolId: "asset-manager-v2",
170+
activeHostContextId: "asset-manager-v2-bad-0001",
171+
activeSession: {
172+
version: "v2",
173+
toolId: "asset-manager-v2",
174+
payloadJson: null
175+
},
176+
savedSessions: {}
177+
}
178+
}
179+
};
180+
await importManifestFromObject(page, invalidPayloadManifest);
181+
await expect(page.locator("#workspaceV2ImportExportStatus")).toContainText("Import error:");
182+
await expect(page.locator("#workspaceV2SessionHistoryEmptyState")).toHaveText("No recent sessions.");
183+
await expect(page).toHaveURL(/\/tools\/workspace-v2\/index\.html/);
184+
} finally {
185+
await server.close();
186+
}
187+
});
188+
189+
test("roundtrip integrity: load -> export -> import preserves JSON", async ({ page }) => {
190+
const server = await startRepoServer();
191+
try {
192+
await page.goto(`${server.baseUrl}/tools/workspace-v2/index.html`);
193+
await page.getByRole("button", { name: "Full Reset" }).click();
194+
await page.locator("#workspaceV2ToolSelect").selectOption("asset-manager-v2");
195+
await page.getByRole("button", { name: "Load Fixture" }).click();
196+
197+
const loadedManifest = JSON.parse(await page.locator("#workspaceV2ImportJson").inputValue());
198+
const exportedManifest = await exportManifestFromTextarea(page);
199+
expect(exportedManifest).toEqual(loadedManifest);
200+
201+
await importManifestFromObject(page, exportedManifest);
202+
await expect(page.locator("#workspaceV2ImportExportStatus")).toHaveText("Workspace session imported.");
203+
const postImportManifest = JSON.parse(await page.locator("#workspaceV2ImportJson").inputValue());
204+
expect(postImportManifest).toEqual(exportedManifest);
205+
} finally {
206+
await server.close();
207+
}
208+
});
209+
210+
test("tool switching keeps activeSession consistent with selected fixture tool", async ({ page }) => {
211+
const server = await startRepoServer();
212+
try {
213+
await page.goto(`${server.baseUrl}/tools/workspace-v2/index.html`);
214+
await page.getByRole("button", { name: "Full Reset" }).click();
215+
216+
await page.locator("#workspaceV2ToolSelect").selectOption("tilemap-studio-v2");
217+
await page.getByRole("button", { name: "Load Fixture" }).click();
218+
let exportedManifest = await exportManifestFromTextarea(page);
219+
expect(exportedManifest.tools?.["workspace-v2"]?.activeToolId).toBe("tilemap-studio-v2");
220+
expect(exportedManifest.tools?.["workspace-v2"]?.activeSession?.toolId).toBe("tilemap-studio-v2");
221+
222+
await page.locator("#workspaceV2ToolSelect").selectOption("vector-map-editor-v2");
223+
await page.getByRole("button", { name: "Load Fixture" }).click();
224+
exportedManifest = await exportManifestFromTextarea(page);
225+
expect(exportedManifest.tools?.["workspace-v2"]?.activeToolId).toBe("vector-map-editor-v2");
226+
expect(exportedManifest.tools?.["workspace-v2"]?.activeSession?.toolId).toBe("vector-map-editor-v2");
227+
} finally {
228+
await server.close();
229+
}
230+
});
231+
});

0 commit comments

Comments
 (0)