Skip to content

Commit 3081767

Browse files
author
DavidQ
committed
Fix Object Vector Studio V2 dirty state tracking for saveable edits - PR_26133_025-object-vector-studio-dirty-state-save-tracking
1 parent cbc5dd5 commit 3081767

8 files changed

Lines changed: 376 additions & 56 deletions

File tree

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
# PR_26133_023 Playwright V8 Coverage Report
1+
# PR_26133_025 Playwright V8 Coverage Report
22

3-
Task: PR_26133_023-font-assets-standardization
3+
Task: PR_26133_025-object-vector-studio-dirty-state-save-tracking
44
Date: 2026-05-13
55

66
## Result
77

88
PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
99

10-
- Test result: 48 passed.
10+
- Test result: 49 passed.
1111
- Coverage source: Playwright/Chromium built-in V8 coverage.
1212
- Thresholds: none enforced.
1313
- Coverage is advisory for this PR.
@@ -21,21 +21,19 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
2121
- Tool Template V2: not exercised by this Playwright run.
2222
- Workspace Manager: not exercised by this Playwright run.
2323

24-
## Changed Runtime JS Coverage Notes
24+
## Changed Runtime JS Coverage
2525

26-
PR_26133_023 is a font asset standardization change. The intended runtime surface is CSS, manifest path data, font files, and Playwright validation updates; no Object Vector Studio V2 runtime JavaScript implementation was changed by this PR.
27-
28-
The generated V8 coverage text still lists Object Vector Studio V2 JavaScript from the current HEAD comparison baseline:
29-
30-
- `tools/object-vector-studio-v2/js/bootstrap.js`: 83% function coverage, 107/107 reported lines executed.
31-
- `tools/object-vector-studio-v2/js/ToolStarterApp.js`: 93% function coverage, 4096/4096 reported lines executed.
32-
- `tools/object-vector-studio-v2/playwright.config.mjs`: advisory warning, not collected by browser V8 coverage.
26+
- `tools/object-vector-studio-v2/js/ToolStarterApp.js`: 94% function coverage, 4171/4171 reported lines executed, 450/481 reported functions executed.
3327

3428
## PR-Specific Coverage/Validation Relevance
3529

36-
- Object Vector Studio V2 Nerd Font loading is validated through Workspace V2 Playwright font fetch and UI flow coverage.
37-
- Asteroids Vector Battle font loading is validated through Workspace V2 Playwright CSS/font fetch checks plus `document.fonts.load()` and `document.fonts.check()`.
38-
- Legacy font path behavior is validated by direct path scans and Asteroids generated URL 404 coverage.
39-
- No additional V8 threshold was introduced for this asset-path-only PR.
30+
The new Workspace Manager V2 Playwright coverage exercises the Object Vector Studio V2 workspace dirty contract end to end:
31+
32+
- clean startup session state,
33+
- selection/preview actions remaining clean,
34+
- every requested persisted edit category marking the Object Vector session dirty,
35+
- Workspace Manager Save becoming enabled after return,
36+
- invalid save preserving dirty state without manifest writes,
37+
- successful save clearing dirty state only after verified write-back.
4038

4139
Generated source report: `docs/dev/reports/playwright_v8_coverage_report.txt`.
Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
1-
# PR_26133_023 Workspace V2 Validation
1+
# PR_26133_025 Workspace V2 Validation
22

3-
Task: PR_26133_023-font-assets-standardization
3+
Task: PR_26133_025-object-vector-studio-dirty-state-save-tracking
44
Date: 2026-05-13
55

66
## Result
77

88
PASS - `npm run test:workspace-v2`
99

10-
- 48 Playwright tests passed.
11-
- Object Vector Studio V2, Workspace Manager V2, Asset Manager V2, Asteroids, and related workspace flows completed with no reported runtime console errors.
10+
- 49 Playwright tests passed.
11+
- Focused Object Vector Studio V2 dirty-state test passed before the full run.
12+
- No console/runtime page errors were reported by the new dirty-state coverage.
1213
- No sample JSON files were changed.
1314

14-
## Targeted Checks
15+
## Targeted Object Vector Studio V2 Verification
1516

16-
- PASS - Nerd Font assets were moved into the shared font asset tree at `src/assets/fonts/0xProtoNerdFont`.
17-
- PASS - Object Vector Studio V2 CSS now loads `0xProtoNerdFontMono-Regular.ttf` from the shared font asset tree.
18-
- PASS - Workspace V2 Playwright coverage includes a direct Nerd Font fetch check at the new URL and verifies the response succeeds.
19-
- PASS - `vector_battle.ttf` was moved into the shared font asset tree at `src/assets/fonts/vector_battle/vector_battle.ttf`.
20-
- PASS - Asteroids manifest data now points at the shared `vector_battle.ttf` path.
21-
- PASS - Shared Vector Battle CSS now loads the font from `/src/assets/fonts/vector_battle/vector_battle.ttf`.
22-
- PASS - Workspace V2 Playwright validation fetches the Asteroids font CSS and font file, waits for `VectorBattle` to load, and confirms the legacy generated asset URL 404s.
23-
- PASS - Direct legacy font path scans returned no active matches outside generated PR report artifacts.
17+
- PASS - Object Vector Studio V2 workspace launches start with `workspace.tools.object-vector-studio-v2.dirty.isDirty=false`.
18+
- PASS - Selection-only changes do not mark the workspace tool session dirty.
19+
- PASS - Preview-only actions tested through zoom, pan, and grid visibility do not change persisted Object Vector data or dirty state.
20+
- PASS - Persisted object edits mark `workspace.tools.object-vector-studio-v2` dirty.
21+
- PASS - Persisted object geometry edits mark dirty.
22+
- PASS - Persisted object transform edits mark dirty.
23+
- PASS - Persisted palette/color application marks dirty.
24+
- PASS - Shape add, visibility, lock/unlock, and delete edits mark dirty.
25+
- PASS - Object add, rename, duplicate, and delete edits mark dirty.
26+
- PASS - Returning to Workspace Manager V2 enables Save and marks the Object Vector Studio V2 tile dirty.
27+
- PASS - Failed invalid save keeps dirty state active and does not write a manifest.
28+
- PASS - Successful save clears dirty state only after verified manifest write-back.
2429

25-
## Additional Validation
30+
## Commands
2631

27-
- PASS - `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
28-
- PASS - `node --check tests/playwright/tools/AssetManagerV2.spec.mjs`
2932
- PASS - `node --check tools/object-vector-studio-v2/js/ToolStarterApp.js`
30-
- PASS - `node --check tools/object-vector-studio-v2/js/bootstrap.js`
31-
- PASS - `node --check games/Asteroids/entities/Asteroid.js`
32-
- PASS - `node -e "JSON.parse(require('fs').readFileSync('games/Asteroids/game.manifest.json','utf8'))"`
33-
- PASS - `git diff --check` completed with line-ending warnings only and no whitespace errors.
33+
- PASS - `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
34+
- PASS - `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "tracks Object Vector Studio V2 dirty state"`
35+
- PASS - `npm run test:workspace-v2`
36+
- PASS - `git diff --check HEAD` completed with line-ending warnings only.
3437

3538
## Notes
3639

37-
The validation run generated temporary Asteroids file noise during test execution; those generated edits were cleaned before final reporting. The final Asteroids manifest diff is limited to the shared Vector Battle font path change.
40+
The full workspace-v2 run produced transient Asteroids manifest write-back output from existing save tests. That generated file noise was restored after validation; the final tracked diff contains only Object Vector Studio V2 dirty tracking and its Playwright test coverage.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,31 @@ async function dirtyPaletteToolState(page, swatch) {
576576
}, swatch);
577577
}
578578

579+
async function readObjectVectorWorkspaceSession(page) {
580+
return await page.evaluate(() => {
581+
const session = JSON.parse(sessionStorage.getItem("workspace.tools.object-vector-studio-v2"));
582+
return {
583+
dataText: JSON.stringify(session.data),
584+
dirty: session.dirty,
585+
objectCount: Array.isArray(session.data?.objects) ? session.data.objects.length : 0,
586+
session
587+
};
588+
});
589+
}
590+
591+
async function resetObjectVectorWorkspaceDirty(page) {
592+
await page.evaluate(() => {
593+
const session = JSON.parse(sessionStorage.getItem("workspace.tools.object-vector-studio-v2"));
594+
session.dirty = {
595+
isDirty: false,
596+
reason: null,
597+
changedAt: null,
598+
changedKeys: []
599+
};
600+
sessionStorage.setItem("workspace.tools.object-vector-studio-v2", JSON.stringify(session));
601+
});
602+
}
603+
579604
async function installMissingGamePreviewRepoPicker(page) {
580605
await page.addInitScript(() => {
581606
function makeDirectoryHandle(name, children = {}, path = name) {
@@ -7191,6 +7216,173 @@ test.describe("Workspace Manager V2 bootstrap", () => {
71917216
}
71927217
});
71937218

7219+
test("tracks Object Vector Studio V2 dirty state through persisted edits and save outcomes", async ({ page }) => {
7220+
const server = await openWorkspaceManagerV2(page);
7221+
const pageErrors = [];
7222+
7223+
page.on("pageerror", (error) => {
7224+
pageErrors.push(error.message);
7225+
});
7226+
7227+
try {
7228+
await selectMockRepo(page);
7229+
await page.locator("#activeGameSelect").selectOption("Asteroids");
7230+
await expectWorkspaceReturnRehydrated(page);
7231+
await expect(page.locator("#saveWorkspaceButton")).toBeDisabled();
7232+
7233+
await page.locator('[data-workspace-tool-id="object-vector-studio-v2"]').click();
7234+
await expect(page).toHaveURL(/object-vector-studio-v2\/index\.html.*launch=workspace/);
7235+
await expect(page.locator("#objectVectorStudioV2ObjectsCount")).toHaveText("(6 obj, 3 shapes)");
7236+
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded Object Vector Studio V2 schema payload from workspace\.tools\.object-vector-studio-v2: 6 objects\./);
7237+
7238+
let objectVectorSession = await readObjectVectorWorkspaceSession(page);
7239+
expect(objectVectorSession.dirty).toEqual({
7240+
isDirty: false,
7241+
reason: null,
7242+
changedAt: null,
7243+
changedKeys: []
7244+
});
7245+
let lastDataText = objectVectorSession.dataText;
7246+
7247+
async function expectObjectVectorDirtyAfter(label, action, { resetDirty = true } = {}) {
7248+
await action();
7249+
await expect.poll(async () => {
7250+
const session = await readObjectVectorWorkspaceSession(page);
7251+
return session.dirty.isDirty === true && session.dataText !== lastDataText;
7252+
}, { message: `${label} should mark Object Vector Studio V2 workspace data dirty` }).toBe(true);
7253+
const dirtySession = await readObjectVectorWorkspaceSession(page);
7254+
expect(dirtySession.dirty).toMatchObject({
7255+
isDirty: true,
7256+
reason: "object-vector-updated"
7257+
});
7258+
expect(Date.parse(dirtySession.dirty.changedAt)).not.toBeNaN();
7259+
expect(dirtySession.dirty.changedKeys).toEqual(expect.arrayContaining(["data.objects"]));
7260+
lastDataText = dirtySession.dataText;
7261+
if (resetDirty) {
7262+
await resetObjectVectorWorkspaceDirty(page);
7263+
}
7264+
return dirtySession;
7265+
}
7266+
7267+
await page.locator('.object-vector-studio-v2__object-tile[data-object-id="object.asteroids.asteroid.large"] .object-vector-studio-v2__object-select').click();
7268+
await page.locator("#objectVectorStudioV2ZoomInButton").click();
7269+
await page.locator("#objectVectorStudioV2PanDownButton").click();
7270+
await page.locator("#objectVectorStudioV2GridRenderButton").click();
7271+
objectVectorSession = await readObjectVectorWorkspaceSession(page);
7272+
expect(objectVectorSession.dataText).toBe(lastDataText);
7273+
expect(objectVectorSession.dirty.isDirty).toBe(false);
7274+
7275+
await expectObjectVectorDirtyAfter("object tag edit", async () => {
7276+
await page.locator("#objectVectorStudioV2ObjectTagInput").fill("dirty-state");
7277+
await page.locator("#objectVectorStudioV2AddTagButton").click();
7278+
});
7279+
await expectObjectVectorDirtyAfter("object geometry edit", async () => {
7280+
await page.locator("#objectVectorStudioV2ObjectDetails [data-shape-geometry-field='points'][data-polygon-point-index='0'][data-polygon-point-axis='x']").fill("11");
7281+
await page.locator("#objectVectorStudioV2ApplyGeometryButton").click();
7282+
});
7283+
await expectObjectVectorDirtyAfter("object transform edit", async () => {
7284+
await page.locator("#objectVectorStudioV2MoveXInput").fill("5");
7285+
await page.locator("#objectVectorStudioV2MoveYInput").fill("-5");
7286+
await page.locator("#objectVectorStudioV2MoveShapeButton").click();
7287+
});
7288+
await expectObjectVectorDirtyAfter("palette color edit", async () => {
7289+
await page.locator("#objectVectorStudioV2PaletteSummary [data-palette-color]").first().click();
7290+
});
7291+
await expectObjectVectorDirtyAfter("shape add edit", async () => {
7292+
await page.locator("[data-shape-tool='rectangle']").click();
7293+
});
7294+
await expectObjectVectorDirtyAfter("shape visibility edit", async () => {
7295+
const selectedShapeId = await page.evaluate(() => window.__objectVectorStudioV2App.selectedShapeId);
7296+
await page.locator(`[data-shape-visibility-id="${selectedShapeId}"]`).click();
7297+
});
7298+
await expectObjectVectorDirtyAfter("shape lock edit", async () => {
7299+
await page.evaluate(() => window.__objectVectorStudioV2App.toggleSelectedShapeLock());
7300+
});
7301+
await expectObjectVectorDirtyAfter("shape unlock edit", async () => {
7302+
await page.evaluate(() => window.__objectVectorStudioV2App.toggleSelectedShapeLock());
7303+
});
7304+
await expectObjectVectorDirtyAfter("shape delete edit", async () => {
7305+
const selectedShapeId = await page.evaluate(() => window.__objectVectorStudioV2App.selectedShapeId);
7306+
await page.locator(`[data-shape-delete-id="${selectedShapeId}"]`).click();
7307+
});
7308+
await expectObjectVectorDirtyAfter("object add edit", async () => {
7309+
await page.locator("#objectVectorStudioV2ObjectNameInput").fill("Dirty Probe");
7310+
await page.locator("#objectVectorStudioV2AddObjectButton").click();
7311+
});
7312+
await expectObjectVectorDirtyAfter("object rename edit", async () => {
7313+
await page.locator("#objectVectorStudioV2ObjectNameInput").fill("Dirty Probe Renamed");
7314+
await page.locator("#objectVectorStudioV2RenameObjectButton").click();
7315+
});
7316+
await expectObjectVectorDirtyAfter("object duplicate edit", async () => {
7317+
await page.locator("#objectVectorStudioV2DuplicateObjectButton").click();
7318+
});
7319+
const validDirtyObjectVectorSession = await expectObjectVectorDirtyAfter("object delete edit", async () => {
7320+
const selectedObjectId = await page.evaluate(() => window.__objectVectorStudioV2App.selectedObjectId);
7321+
await page.locator(`[data-object-control="delete"][data-object-control-id="${selectedObjectId}"]`).click();
7322+
}, { resetDirty: false });
7323+
7324+
await page.locator("#returnToWorkspaceButton").click();
7325+
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
7326+
await expectWorkspaceReturnedFromTool(page, { dirty: true });
7327+
const objectVectorTile = page.locator('[data-workspace-tool-id="object-vector-studio-v2"]');
7328+
await expect(objectVectorTile).toHaveAttribute("data-workspace-tool-dirty", "true");
7329+
await expect(page.locator("#saveWorkspaceButton")).toBeEnabled();
7330+
7331+
await page.evaluate(() => {
7332+
const session = JSON.parse(sessionStorage.getItem("workspace.tools.object-vector-studio-v2"));
7333+
session.data.unexpected = "blocked";
7334+
session.dirty = {
7335+
isDirty: true,
7336+
reason: "object-vector-invalid-save",
7337+
changedAt: new Date().toISOString(),
7338+
changedKeys: ["data.objects.invalid"]
7339+
};
7340+
sessionStorage.setItem("workspace.tools.object-vector-studio-v2", JSON.stringify(session));
7341+
window.__workspaceManagerV2App.syncLifecycleControls();
7342+
});
7343+
await page.locator("#saveWorkspaceButton").click();
7344+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Save blocked: Generated Workspace Manager V2 manifest failed schema validation:/);
7345+
const failedSaveState = await page.evaluate(() => ({
7346+
dirty: JSON.parse(sessionStorage.getItem("workspace.tools.object-vector-studio-v2")).dirty,
7347+
writes: JSON.parse(sessionStorage.getItem("workspace.repo.manifestWrites") || "[]")
7348+
}));
7349+
expect(failedSaveState.dirty).toMatchObject({
7350+
isDirty: true,
7351+
reason: "object-vector-invalid-save"
7352+
});
7353+
expect(failedSaveState.writes).toEqual([]);
7354+
await expect(page.locator("#saveWorkspaceButton")).toBeEnabled();
7355+
7356+
await page.evaluate((session) => {
7357+
sessionStorage.setItem("workspace.tools.object-vector-studio-v2", JSON.stringify(session));
7358+
window.__workspaceManagerV2App.syncLifecycleControls();
7359+
}, validDirtyObjectVectorSession.session);
7360+
await page.locator("#saveWorkspaceButton").click();
7361+
await expect(page.locator("#statusLog")).toHaveValue(/OK Saved and marked clean: workspace\.tools\.object-vector-studio-v2\./);
7362+
await expect(page.locator("#statusLog")).toHaveValue(/OK Save dirty\/clean validation: 1 dirty toolState payload persisted; 1 toolState key marked clean\./);
7363+
await expect(objectVectorTile).toHaveAttribute("data-workspace-tool-dirty", "false");
7364+
await expect(page.locator("#saveWorkspaceButton")).toBeDisabled();
7365+
await expect(page.locator("#closeWorkspaceButton")).toBeEnabled();
7366+
const savedState = await page.evaluate(() => ({
7367+
session: JSON.parse(sessionStorage.getItem("workspace.tools.object-vector-studio-v2")),
7368+
writes: JSON.parse(sessionStorage.getItem("workspace.repo.manifestWrites") || "[]")
7369+
}));
7370+
expect(savedState.session.dirty).toEqual({
7371+
isDirty: false,
7372+
reason: null,
7373+
changedAt: null,
7374+
changedKeys: []
7375+
});
7376+
const writtenManifest = JSON.parse(savedState.writes.at(-1).contents);
7377+
expect(writtenManifest.game.workspace.tools["object-vector-studio-v2"].objects.some((object) => object.name === "Dirty Probe Renamed")).toBe(true);
7378+
expect(writtenManifest.game.workspace.tools["object-vector-studio-v2"].objects.find((object) => object.id === "object.asteroids.asteroid.large").tags).toContain("dirty-state");
7379+
expect(pageErrors).toEqual([]);
7380+
} finally {
7381+
await coverageReporter.stop(page);
7382+
await server.close();
7383+
}
7384+
});
7385+
71947386
test("syncs Workspace Manager V2 dirty lifecycle buttons and closes clean toolState data", async ({ page }) => {
71957387
const server = await openWorkspaceManagerV2(page);
71967388
const pageErrors = [];

tests/tools/AssetUsageIntegration.test.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export async function run() {
5454
const validAsset = createAssetHandoff({
5555
assetId: "asset-vector-player",
5656
assetType: "vector",
57-
sourcePath: "../../games/Asteroids/game.manifest.json#tools.svg-asset-studio.vectors.vector.asteroids.ship",
57+
sourcePath: "../../games/Asteroids/game.manifest.json#tools.vector-map-editor.vectorMapDocument.vectors.vector.asteroids.ship",
5858
displayName: "Asteroids Ship Vector",
5959
metadata: { category: "Vector Assets" },
6060
sourceToolId: "tile-map-editor"
@@ -63,7 +63,7 @@ export async function run() {
6363
const storedAsset = readSharedAssetHandoff();
6464
assert.equal(storedAsset.assetId, "asset-vector-player");
6565
assert.equal(storedAsset.assetType, "vector");
66-
assert.equal(storedAsset.sourcePath, "../../games/Asteroids/game.manifest.json#tools.svg-asset-studio.vectors.vector.asteroids.ship");
66+
assert.equal(storedAsset.sourcePath, "../../games/Asteroids/game.manifest.json#tools.vector-map-editor.vectorMapDocument.vectors.vector.asteroids.ship");
6767
assert.equal(storedAsset.displayName, "Asteroids Ship Vector");
6868
assert.equal(storedAsset.sourceToolId, "tile-map-editor");
6969
assert.equal(typeof storedAsset.selectedAt, "string");

0 commit comments

Comments
 (0)