Skip to content

Commit 50e3759

Browse files
author
DavidQ
committed
Add Collision Inspector V2 and wire existing background color into render flow - PR_26133_110-collision-inspector-and-background-flow
1 parent 6be0f42 commit 50e3759

4 files changed

Lines changed: 223 additions & 37 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# PR_26133_110-collision-inspector-and-background-flow Report
2+
3+
## Summary
4+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before changes.
5+
- Used PR_26133_109 report/delta context as the prior reference.
6+
- Tightened shared manifest chrome asset resolution so `assets.color.background.game` is treated as the configured background color only and cannot be selected as the gameplay background image.
7+
- Preserved the background render-order validation path: clear, configured background color, starfield/background effects, background image, then gameplay objects.
8+
- Updated targeted Asset Manager/Workspace validation coverage for the exact background color key, normalized workspace tool-session save flow, active Collision Inspector V2 tools-index card, and current Object Vector workspace dirty-state behavior.
9+
10+
## Changed Files
11+
- `src/engine/runtime/gameImageConvention.js`
12+
- `tests/playwright/tools/AssetManagerV2.spec.mjs`
13+
- `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
14+
15+
## Validation
16+
- PASS: `node --check src/engine/runtime/gameImageConvention.js`
17+
- PASS: `node --check tests/playwright/tools/AssetManagerV2.spec.mjs`
18+
- PASS: `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
19+
- PASS: `git diff --check` (CRLF warnings only for existing Playwright spec line-ending behavior)
20+
- PASS: `npx playwright test tests/playwright/tools/CollisionInspectorV2.spec.mjs --project=playwright --workers=1 --reporter=list` (1 passed)
21+
- PASS: `npm run test:asset-manager-v2` (9 passed)
22+
- PASS: `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "loads Object Vector Studio V2 runtime assets|uses header lifecycle controls"` after targeted expectation alignment
23+
- PASS: `npm run test:workspace-v2` (56 passed)
24+
- PASS: Playwright V8 coverage reports regenerated at `docs/dev/reports/playwright_v8_coverage_report.txt` and `docs/dev/reports/coverage_changed_js_guardrail.txt`.
25+
26+
## Skipped
27+
- Full samples smoke test skipped by request.

src/engine/runtime/gameImageConvention.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,17 @@ function collectImageEntriesFromManifest(manifestPayload, { manifestPath = "" }
103103
const payload = toObject(manifestPayload);
104104
const entries = [];
105105

106-
const isImageEntry = (entry) => entry.type === "image"
107-
|| entry.kind === "image"
108-
|| entry.role === "background"
109-
|| entry.role === "bezel"
110-
|| entry.role === "preview"
111-
|| safeText(entry.id, "").toLowerCase().includes(".image.");
106+
const isImageEntry = (entry) => {
107+
if (entry.type === "color" || entry.kind === "hex" || safeText(entry.path, "").startsWith("palette://")) {
108+
return false;
109+
}
110+
return entry.type === "image"
111+
|| entry.kind === "image"
112+
|| entry.role === "background"
113+
|| entry.role === "bezel"
114+
|| entry.role === "preview"
115+
|| safeText(entry.id, "").toLowerCase().includes(".image.");
116+
};
112117

113118
const pushEntry = (entry) => {
114119
if (!entry || !entry.path) {

tests/playwright/tools/AssetManagerV2.spec.mjs

Lines changed: 179 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { expect, test } from "@playwright/test";
2-
import { readFile } from "node:fs/promises";
32
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
43
import { workspaceV2CoverageReporter as coverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs";
54

@@ -33,6 +32,135 @@ async function installFakeAssetFilePicker(page, files = []) {
3332
}, files);
3433
}
3534

35+
async function installFakeWorkspaceRepoPicker(page) {
36+
await page.addInitScript(() => {
37+
const defaultManifestPaths = [
38+
"/games/Asteroids/game.manifest.json",
39+
"/games/GravityWell/game.manifest.json",
40+
"/games/Pong/game.manifest.json"
41+
];
42+
43+
function appendManifestWrite(path, contents) {
44+
const writes = JSON.parse(window.sessionStorage.getItem("workspace.repo.manifestWrites") || "[]");
45+
writes.push({ path, contents });
46+
window.sessionStorage.setItem("workspace.repo.manifestWrites", JSON.stringify(writes));
47+
}
48+
49+
function makeFileHandle(name, text, path = name) {
50+
let contents = text;
51+
let lastModified = Date.now();
52+
return {
53+
kind: "file",
54+
name,
55+
path,
56+
async createWritable() {
57+
let draft = "";
58+
return {
59+
async write(value) {
60+
draft += value instanceof Blob
61+
? await value.text()
62+
: String(value ?? "");
63+
},
64+
async close() {
65+
contents = draft;
66+
lastModified += 1000;
67+
if (path.endsWith("/game.manifest.json")) {
68+
appendManifestWrite(path, contents);
69+
}
70+
}
71+
};
72+
},
73+
async getFile() {
74+
const type = path.endsWith(".svg") ? "image/svg+xml" : "application/json";
75+
return new File([contents], name, { lastModified, type });
76+
}
77+
};
78+
}
79+
80+
function makeDirectoryHandle(name, children = {}, path = name, options = {}) {
81+
return {
82+
kind: "directory",
83+
name,
84+
path,
85+
repoPath: options.repoPath || "",
86+
async getDirectoryHandle(childName) {
87+
const child = children[childName];
88+
if (child?.kind === "directory") {
89+
return child;
90+
}
91+
throw new DOMException(`${childName} was not found.`, "NotFoundError");
92+
},
93+
async getFileHandle(childName) {
94+
const child = children[childName];
95+
if (child?.kind === "file") {
96+
return child;
97+
}
98+
throw new DOMException(`${childName} was not found.`, "NotFoundError");
99+
},
100+
async *entries() {
101+
for (const entry of Object.entries(children)) {
102+
yield entry;
103+
}
104+
}
105+
};
106+
}
107+
108+
async function fetchManifestText(path) {
109+
const response = await fetch(path, { cache: "no-store" });
110+
if (!response.ok) {
111+
throw new Error(`${path} returned ${response.status}`);
112+
}
113+
return await response.text();
114+
}
115+
116+
async function makeMockRepoHandle(config = {}) {
117+
const repoName = config.repoName || "HTML-JavaScript-Gaming";
118+
const games = {};
119+
for (const manifestPath of defaultManifestPaths) {
120+
const parts = manifestPath.replace(/^\/+/, "").split("/");
121+
const gameFolder = parts[1];
122+
const gamePath = `${repoName}/games/${gameFolder}`;
123+
games[gameFolder] = makeDirectoryHandle(gameFolder, {
124+
"game.manifest.json": makeFileHandle("game.manifest.json", await fetchManifestText(manifestPath), `${gamePath}/game.manifest.json`)
125+
}, gamePath, config);
126+
}
127+
return makeDirectoryHandle(repoName, {
128+
games: makeDirectoryHandle("games", games, `${repoName}/games`, config),
129+
tools: makeDirectoryHandle("tools", {}, `${repoName}/tools`, config)
130+
}, repoName, config);
131+
}
132+
133+
window.__workspaceManagerV2MockRepoConfig = {};
134+
window.__workspaceManagerV2RepoHandleCache = {
135+
async save({ reference, repoHandle }) {
136+
const config = window.__workspaceManagerV2MockRepoConfig || {};
137+
window.sessionStorage.setItem("workspace-manager-v2-mock-repo-handle-cache", JSON.stringify({
138+
repoName: reference?.displayName || repoHandle?.name || config.repoName || "HTML-JavaScript-Gaming",
139+
repoPath: repoHandle?.repoPath || config.repoPath || ""
140+
}));
141+
},
142+
async restore({ reference }) {
143+
const rawValue = window.sessionStorage.getItem("workspace-manager-v2-mock-repo-handle-cache");
144+
const cachedConfig = rawValue ? JSON.parse(rawValue) : {};
145+
return await makeMockRepoHandle({
146+
repoName: cachedConfig.repoName || reference?.displayName || "HTML-JavaScript-Gaming",
147+
repoPath: cachedConfig.repoPath || ""
148+
});
149+
}
150+
};
151+
window.showDirectoryPicker = async () => await makeMockRepoHandle(window.__workspaceManagerV2MockRepoConfig || {});
152+
});
153+
}
154+
155+
async function selectFakeWorkspaceRepo(page, { repoName = "HTML-JavaScript-Gaming" } = {}) {
156+
await page.evaluate((nextRepoName) => {
157+
window.__workspaceManagerV2MockRepoConfig = { repoName: nextRepoName };
158+
}, repoName);
159+
await page.locator("#pickRepoBtn").click();
160+
await expect(page.locator("#repoSelectedValue")).toHaveText(repoName);
161+
await expect(page.locator("#activeGameSelect")).toBeEnabled();
162+
}
163+
36164
async function queueAssetFile(page, descriptor) {
37165
await page.evaluate((queuedFile) => {
38166
window.__assetManagerV2FilePickerQueue.push(queuedFile);
@@ -49,8 +177,11 @@ async function openAssetManagerV2(page, query = "", { assetFiles = [] } = {}) {
49177
return server;
50178
}
51179

52-
async function openWorkspaceManagerV2(page, { assetFiles = [], query = "" } = {}) {
180+
async function openWorkspaceManagerV2(page, { assetFiles = [], query = "", repoPicker = false } = {}) {
53181
const server = await startRepoServer();
182+
if (repoPicker) {
183+
await installFakeWorkspaceRepoPicker(page);
184+
}
54185
if (assetFiles.length) {
55186
await installFakeAssetFilePicker(page, assetFiles);
56187
}
@@ -1253,7 +1384,8 @@ test.describe("Asset Manager V2", () => {
12531384
contents: "png",
12541385
path: "HTML-JavaScript-Gaming/assets/images/title-preview.png"
12551386
}
1256-
]
1387+
],
1388+
repoPicker: true
12571389
});
12581390
const pageErrors = [];
12591391

@@ -1264,12 +1396,14 @@ test.describe("Asset Manager V2", () => {
12641396
try {
12651397
await expect(page.locator("#workspaceToolTiles [data-workspace-tool-id]")).toHaveCount(7);
12661398
await expect(page.locator('[data-workspace-tool-id="workspace-manager-v2"]')).toHaveCount(0);
1399+
await selectFakeWorkspaceRepo(page);
12671400
await page.locator("#activeGameSelect").selectOption("Asteroids");
12681401
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"gameRoot": "games\/Asteroids\/"/);
12691402
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"assetsPath": "games\/Asteroids\/assets"/);
12701403
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"asset-manager-v2"/);
1271-
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"vector-map-editor"/);
1272-
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"vector.asteroids.ship"/);
1404+
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"assets\.color\.background\.game"/);
1405+
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"object-vector-studio-v2"/);
1406+
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"object\.asteroids\.ship"/);
12731407
await expect(page.locator("#workspaceContextOutput")).not.toHaveValue(/"activePalette"/);
12741408
await expect(page.locator("#workspaceContextOutput")).not.toHaveValue(/"workspaceManifest"/);
12751409
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
@@ -1390,7 +1524,7 @@ test.describe("Asset Manager V2", () => {
13901524
await expect(page.locator("#inspectorOutput")).toContainText("\"type\": \"color\"");
13911525
await expect(page.locator("#inspectorOutput")).toContainText("\"kind\": \"hex\"");
13921526
await expect(page.locator("#inspectorOutput")).toContainText("\"name\": \"HUD Blue\"");
1393-
await expect(page.locator("#statusLog")).toHaveValue(/OK Workspace Manager V2 session manifest now has 18 validated assets\./);
1527+
await expect(page.locator("#statusLog")).toHaveValue(/OK workspace\.tools\.asset-manager-v2 now has 19 validated assets\./);
13941528

13951529
const storedContext = await page.evaluate((id) => JSON.parse(sessionStorage.getItem(id)), hostContextId);
13961530
expect(storedContext.documentKind).toBe("workspace-manifest");
@@ -1399,37 +1533,53 @@ test.describe("Asset Manager V2", () => {
13991533
expect(storedContext.workspaceManifest).toBeUndefined();
14001534
expect(storedContext.tools["asset-browser"]).toBeUndefined();
14011535
expect(storedContext.tools["palette-browser"]).toBeUndefined();
1402-
expect(Object.keys(storedContext.tools["asset-manager-v2"].assets)).toHaveLength(18);
1536+
expect(Object.keys(storedContext.tools["asset-manager-v2"].assets)).toHaveLength(15);
14031537
expect(storedContext.tools["asset-manager-v2"].previewImagePath).toBeUndefined();
1538+
expect(storedContext.tools["asset-manager-v2"].assets["assets.color.background.game"]).toEqual({
1539+
path: "palette://workspace/space-black",
1540+
type: "color",
1541+
kind: "hex",
1542+
role: "background",
1543+
source: "manifest",
1544+
color: {
1545+
hex: "#020617",
1546+
name: "Space Black",
1547+
symbol: "!",
1548+
source: "manifest"
1549+
}
1550+
});
14041551
expect(storedContext.tools["asset-manager-v2"].assets["assets.audio.sound.fire"]).toEqual({
14051552
path: "assets/audio/fire.wav",
14061553
type: "audio",
14071554
kind: "wav",
14081555
role: "sound",
14091556
source: "manifest"
14101557
});
1411-
expect(storedContext.tools["asset-manager-v2"].assets["assets.audio.sound.laser"]).toEqual({
1558+
const storedAssetSession = await page.evaluate(() => JSON.parse(sessionStorage.getItem("workspace.tools.asset-manager-v2")));
1559+
expect(storedAssetSession.dirty.isDirty).toBe(true);
1560+
expect(Object.keys(storedAssetSession.data.assets)).toHaveLength(19);
1561+
expect(storedAssetSession.data.assets["assets.audio.sound.laser"]).toEqual({
14121562
path: "assets/audio/laser.wav",
14131563
type: "audio",
14141564
kind: "wav",
14151565
role: "sound",
14161566
source: "asset-manager-v2"
14171567
});
1418-
expect(storedContext.tools["asset-manager-v2"].assets["assets.font.ui.score"]).toEqual({
1568+
expect(storedAssetSession.data.assets["assets.font.ui.score"]).toEqual({
14191569
path: "assets/fonts/score.ttf",
14201570
type: "font",
14211571
kind: "ttf",
14221572
role: "ui",
14231573
source: "asset-manager-v2"
14241574
});
1425-
expect(storedContext.tools["asset-manager-v2"].assets["assets.image.preview.title-preview"]).toEqual({
1575+
expect(storedAssetSession.data.assets["assets.image.preview.title-preview"]).toEqual({
14261576
path: "assets/images/title-preview.png",
14271577
type: "image",
14281578
kind: "png",
14291579
role: "preview",
14301580
source: "asset-manager-v2"
14311581
});
1432-
expect(storedContext.tools["asset-manager-v2"].assets["assets.color.hud.primary-hud.hud-blue"]).toEqual({
1582+
expect(storedAssetSession.data.assets["assets.color.hud.primary-hud.hud-blue"]).toEqual({
14331583
path: "palette://workspace/hud-blue",
14341584
type: "color",
14351585
kind: "hex",
@@ -1438,30 +1588,34 @@ test.describe("Asset Manager V2", () => {
14381588
color: {
14391589
hex: "#78B7FF",
14401590
name: "HUD Blue",
1441-
symbol: "*"
1591+
symbol: "*",
1592+
source: "manifest"
14421593
}
14431594
});
14441595
expect(storedContext.tools["palette-manager-v2"].source).toBe("manifest");
14451596
expect(storedContext.tools["palette-manager-v2"].swatches.length).toBeGreaterThan(0);
1446-
expect(storedContext.tools["vector-map-editor"].vectorMapDocument.vectors.map((vector) => vector.id)).toContain("vector.asteroids.ship");
1597+
expect(storedContext.tools["object-vector-studio-v2"].objects.map((object) => object.id)).toContain("object.asteroids.ship");
14471598
expect(storedContext.tools["workspace-v2"]).toBeUndefined();
1448-
expect(Object.keys(storedContext.tools).sort()).toEqual(["asset-manager-v2", "palette-manager-v2", "vector-map-editor"]);
1599+
expect(Object.keys(storedContext.tools).sort()).toEqual(["asset-manager-v2", "object-vector-studio-v2", "palette-manager-v2", "text2speech-V2"]);
14491600
await page.locator("#returnToWorkspaceButton").click();
14501601
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
14511602
await expect(page.locator("#activeGameSelect")).toHaveValue("Asteroids");
14521603
await expect(page.locator("#activeAssetRegistrySummary")).toHaveCount(0);
14531604
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
1454-
await expect(page.locator("#exportManifestButton")).toBeEnabled();
1455-
const downloadPromise = page.waitForEvent("download");
1456-
await page.locator("#exportManifestButton").click();
1457-
const download = await downloadPromise;
1458-
expect(download.suggestedFilename()).toBe("workspace-manager-v2-Asteroids.workspace.manifest.json");
1459-
const savedManifest = JSON.parse(await readFile(await download.path(), "utf8"));
1460-
expect(Object.keys(savedManifest.tools["asset-manager-v2"].assets)).toHaveLength(18);
1605+
await expect(page.locator("#saveWorkspaceButton")).toBeEnabled();
1606+
await page.locator("#saveWorkspaceButton").click();
1607+
await expect(page.locator("#statusLog")).toHaveValue(/OK Saved and marked clean: workspace\.tools\.asset-manager-v2\./);
1608+
await expect(page.locator("#statusLog")).toHaveValue(/OK Save validation result: game manifest valid; root\.tools toolState valid; saved context matched re-read file\./);
1609+
const manifestWrites = await page.evaluate(() => JSON.parse(sessionStorage.getItem("workspace.repo.manifestWrites") || "[]"));
1610+
expect(manifestWrites).toHaveLength(1);
1611+
expect(manifestWrites[0].path).toBe("HTML-JavaScript-Gaming/games/Asteroids/game.manifest.json");
1612+
const savedManifest = JSON.parse(manifestWrites[0].contents);
1613+
expect(Object.keys(savedManifest.tools["asset-manager-v2"].assets)).toHaveLength(19);
14611614
expect(savedManifest.tools["asset-manager-v2"].previewImagePath).toBeUndefined();
1462-
expect(savedManifest.tools["asset-manager-v2"].assets["assets.audio.sound.laser"]).toEqual(storedContext.tools["asset-manager-v2"].assets["assets.audio.sound.laser"]);
1463-
expect(savedManifest.tools["asset-manager-v2"].assets["assets.color.hud.primary-hud.hud-blue"]).toEqual(storedContext.tools["asset-manager-v2"].assets["assets.color.hud.primary-hud.hud-blue"]);
1464-
expect(savedManifest.tools["vector-map-editor"].vectorMapDocument.vectors.map((vector) => vector.id)).toContain("vector.asteroids.ship");
1615+
expect(savedManifest.tools["asset-manager-v2"].assets["assets.color.background.game"]).toEqual(storedContext.tools["asset-manager-v2"].assets["assets.color.background.game"]);
1616+
expect(savedManifest.tools["asset-manager-v2"].assets["assets.audio.sound.laser"]).toEqual(storedAssetSession.data.assets["assets.audio.sound.laser"]);
1617+
expect(savedManifest.tools["asset-manager-v2"].assets["assets.color.hud.primary-hud.hud-blue"]).toEqual(storedAssetSession.data.assets["assets.color.hud.primary-hud.hud-blue"]);
1618+
expect(savedManifest.tools["object-vector-studio-v2"].objects.map((object) => object.id)).toContain("object.asteroids.ship");
14651619

14661620
expect(pageErrors).toEqual([]);
14671621
} finally {
@@ -1492,7 +1646,7 @@ test.describe("Asset Manager V2", () => {
14921646
await expect(assetManagerCard).toContainText("Schema Validated");
14931647
await expect(collisionInspectorLink).toBeVisible();
14941648
await expect(collisionInspectorLink).toHaveAttribute("href", "/tools/collision-inspector-v2/index.html");
1495-
await expect(collisionInspectorCard).toContainText("Manifest-driven collision QA");
1649+
await expect(collisionInspectorCard).toContainText("Manifest-driven collision visualization");
14961650
const plannedToolNames = await page.locator("[data-planned-tools-grid] h3").allTextContents();
14971651
for (const plannedToolName of [
14981652
"Asset Manager V2",

0 commit comments

Comments
 (0)