Skip to content

Commit 0356442

Browse files
author
DavidQ
committed
Clean single member groups and fix Object Vector Studio add state enablement - PR_26133_052-group-cleanup-and-add-state-enable-fix
1 parent d816830 commit 0356442

4 files changed

Lines changed: 161 additions & 19 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_051 Playwright V8 Coverage Report
1+
# PR_26133_052 Playwright V8 Coverage Report
22

3-
Task: PR_26133_051-shape-order-ui-reverse-sort-and-action-order
3+
Task: PR_26133_052-group-cleanup-and-add-state-enable-fix
44
Date: 2026-05-15
55

66
## Result
@@ -25,7 +25,7 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
2525

2626
```text
2727
(83%) tools/object-vector-studio-v2/js/bootstrap.js - executed lines 105/105; executed functions 5/6
28-
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5073/5073; executed functions 533/564
28+
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5093/5093; executed functions 542/570
2929
```
3030

3131
## Guardrail
@@ -36,4 +36,4 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
3636

3737
## PR-Specific Note
3838

39-
The Workspace V2 run exercised Object Vector Studio V2 shape-list rendering, reversed visual shape ordering, selected shape z-order actions, preserved render-order behavior, Object Vector schema validation, and Asteroids runtime object-vector rendering.
39+
The Workspace V2 run exercised Object Vector Studio V2 group cleanup, group icon removal, Add State enablement, duplicate state warnings, state tile/timeline refresh, workspace dirty tracking after state creation, Object Vector schema validation, and Asteroids runtime object-vector rendering.
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_051 Workspace V2 Playwright Results
1+
# PR_26133_052 Workspace V2 Playwright Results
22

3-
Task: PR_26133_051-shape-order-ui-reverse-sort-and-action-order
3+
Task: PR_26133_052-group-cleanup-and-add-state-enable-fix
44
Date: 2026-05-15
55

66
## Result
@@ -9,20 +9,20 @@ PASS - `npm run test:workspace-v2` completed successfully.
99

1010
- Command: `npm run test:workspace-v2`
1111
- Playwright target: `tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list`
12-
- Final result: 51 passed, 0 failed.
12+
- Final result: 52 passed, 0 failed.
1313
- Runtime/console guard: Workspace Manager V2, Object Vector Studio V2, and Asteroids runtime scenarios completed with no reported page errors.
1414

1515
## PR-Specific Coverage
1616

17-
- Verified Object Vector Studio V2 keeps underlying render/order semantics unchanged: lower order remains back and higher order remains front.
18-
- Verified the object tile shape list displays in reverse UI order, with the front-most rendered shape at the top and the back-most shape at the bottom.
19-
- Verified shape stacking controls render in the requested visual order: Send To Back, Move Backward, Move Forward, Bring To Front.
20-
- Verified the reordered controls keep the existing send/backward/forward/front icon mappings.
21-
- Verified Bring To Front moves the selected shape to the top of the displayed list.
22-
- Verified Send To Back moves the selected shape to the bottom of the displayed list.
17+
- Verified Ungroup removes selected group membership and prunes leftover single-member group metadata.
18+
- Verified group icons disappear when a group is removed.
19+
- Verified Add State is enabled for selected unlocked objects with valid state values.
20+
- Verified Add State creates a new state, selects it, and refreshes state tiles/timeline immediately.
21+
- Verified duplicate state creation is rejected with a visible warning.
22+
- Verified adding a state marks Object Vector Studio V2 workspace toolState dirty.
2323

2424
## Additional Validation
2525

26-
- Focused Object Vector Studio V2 slice passed before the full run:
27-
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --grep "Object Vector Studio V2"` completed with 12 passed, 0 failed.
26+
- Focused PR052 slice passed before the full run:
27+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --grep "single-member groups|dirty state"` completed with 2 passed, 0 failed.
2828
- `git diff --check` passed.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4214,6 +4214,114 @@ test.describe("Workspace Manager V2 bootstrap", () => {
42144214
}
42154215
});
42164216

4217+
test("cleans Object Vector Studio V2 single-member groups and adds selected object states", async ({ page }, testInfo) => {
4218+
const server = await startRepoServer();
4219+
const pageErrors = [];
4220+
4221+
page.on("pageerror", (error) => {
4222+
pageErrors.push(error.message);
4223+
});
4224+
4225+
await coverageReporter.start(page);
4226+
try {
4227+
await page.goto(`${server.baseUrl}/tools/object-vector-studio-v2/index.html`, { waitUntil: "networkidle" });
4228+
await page.evaluate(() => {
4229+
sessionStorage.setItem("object-vector-studio-v2.runtimePalette", JSON.stringify({
4230+
id: "group-cleanup-palette",
4231+
swatches: [
4232+
{ id: "white", value: "#ffffff" },
4233+
{ id: "cyan", value: "#6fd3ff" }
4234+
]
4235+
}));
4236+
});
4237+
4238+
const payloadPath = testInfo.outputPath("object-vector-group-state.json");
4239+
await writeFile(payloadPath, JSON.stringify({
4240+
name: "Group Cleanup Payload",
4241+
objects: [
4242+
{
4243+
id: "object.test.group-state",
4244+
name: "Group State",
4245+
shapes: [
4246+
{
4247+
geometry: { x: -20, y: -20, width: 16, height: 16 },
4248+
groupId: "group-1",
4249+
locked: false,
4250+
order: 1,
4251+
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#6fd3ff", strokeOpacity: 1, strokeWidth: 2 },
4252+
tool: "rectangle",
4253+
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
4254+
visible: true
4255+
},
4256+
{
4257+
geometry: { x: 8, y: -20, width: 16, height: 16 },
4258+
groupId: "group-1",
4259+
locked: false,
4260+
order: 2,
4261+
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#6fd3ff", strokeOpacity: 1, strokeWidth: 2 },
4262+
tool: "rectangle",
4263+
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
4264+
visible: true
4265+
}
4266+
],
4267+
states: [
4268+
{
4269+
frames: [
4270+
{ durationFrames: 1, id: "frame-1", order: 1, shapeOverrides: [] }
4271+
],
4272+
id: "idle",
4273+
name: "Idle"
4274+
}
4275+
],
4276+
tags: []
4277+
}
4278+
],
4279+
toolId: "object-vector-studio-v2",
4280+
version: 1
4281+
}, null, 2), "utf8");
4282+
await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles(payloadPath);
4283+
4284+
const objectTile = page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.test.group-state']");
4285+
const statePanel = objectTile.locator(".object-vector-studio-v2__object-state-panel");
4286+
const addStateButton = statePanel.locator("[data-object-state-action='add']");
4287+
const stateSelect = statePanel.locator("[data-object-state-select='object.test.group-state']");
4288+
await expect(addStateButton).toBeEnabled();
4289+
await addStateButton.click();
4290+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Create state skipped: Idle already exists for Group State\./);
4291+
await expect(statePanel.locator("[data-object-state-tile]")).toHaveText(["idle"]);
4292+
4293+
await stateSelect.selectOption("move");
4294+
await expect(page.locator("#statusLog")).toHaveValue(/INFO State move is ready to add for Group State\./);
4295+
await expect(addStateButton).toBeEnabled();
4296+
await addStateButton.click();
4297+
await expect(statePanel.locator("[data-object-state-tile]")).toHaveText(["idle", "move"]);
4298+
await expect(statePanel.locator("[data-object-state-tile='move']")).toHaveAttribute("aria-pressed", "true");
4299+
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-state-id='move']")).toHaveCount(1);
4300+
await expect(page.locator("#statusLog")).toHaveValue(/OK Created state Move with frame frame-1\./);
4301+
await addStateButton.click();
4302+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Create state skipped: Move already exists for Group State\./);
4303+
await expect(statePanel.locator("[data-object-state-tile='move']")).toHaveCount(1);
4304+
4305+
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(2);
4306+
await page.evaluate(() => {
4307+
const app = window.__objectVectorStudioV2App;
4308+
app.selectedShapeIndex = 0;
4309+
app.selectedShapeIndexes = new Set([0]);
4310+
app.renderPayload();
4311+
});
4312+
const selectedShapeActions = objectTile.locator(".object-vector-studio-v2__shape-list-actions");
4313+
await selectedShapeActions.locator("[data-shape-list-action='ungroup']").click();
4314+
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"groupId": "group-1"');
4315+
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(0);
4316+
await expect(page.locator("#statusLog")).toHaveValue(/OK Ungrouped 1 selected shapes from group-1\./);
4317+
4318+
expect(pageErrors).toEqual([]);
4319+
} finally {
4320+
await coverageReporter.stop(page);
4321+
await server.close();
4322+
}
4323+
});
4324+
42174325
test("supports Object Vector Studio V2 asset library inheritance foundation", async ({ page }, testInfo) => {
42184326
const server = await startRepoServer();
42194327
const pageErrors = [];
@@ -8120,6 +8228,19 @@ test.describe("Workspace Manager V2 bootstrap", () => {
81208228
await page.locator("#objectVectorStudioV2ObjectTagInput").fill("dirty-state");
81218229
await page.locator("#objectVectorStudioV2AddTagButton").click();
81228230
});
8231+
await expectObjectVectorDirtyAfter("object state add edit", async () => {
8232+
const selectedObjectId = await page.evaluate(() => window.__objectVectorStudioV2App.selectedObjectId);
8233+
const stateId = await page.evaluate(() => {
8234+
const app = window.__objectVectorStudioV2App;
8235+
const existing = new Set(app.objectStates(app.selectedObject()).map((state) => state.id));
8236+
return ["idle", "move", "active", "inactive", "damaged", "destroyed"].find((candidate) => !existing.has(candidate)) || "";
8237+
});
8238+
expect(stateId).toBeTruthy();
8239+
const selectedObjectTile = page.locator(`.object-vector-studio-v2__object-tile[data-object-id="${selectedObjectId}"]`);
8240+
await selectedObjectTile.locator(`[data-object-state-select="${selectedObjectId}"]`).selectOption(stateId);
8241+
await expect(selectedObjectTile.locator("[data-object-state-action='add']")).toBeEnabled();
8242+
await selectedObjectTile.locator("[data-object-state-action='add']").click();
8243+
});
81238244
await expectObjectVectorDirtyAfter("object geometry edit", async () => {
81248245
await page.locator("#objectVectorStudioV2ObjectDetails [data-shape-geometry-field='points'][data-polygon-point-index='0'][data-polygon-point-axis='x']").fill("11");
81258246
await page.locator("#objectVectorStudioV2ApplyGeometryButton").click();

tools/object-vector-studio-v2/js/ToolStarterApp.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,8 +1312,8 @@ export class ToolStarterApp {
13121312
addButton.type = "button";
13131313
addButton.dataset.objectStateAction = "add";
13141314
addButton.textContent = "Add";
1315-
addButton.title = stateExists ? "Selected state already exists." : `Add ${selectedStateId} state`;
1316-
addButton.disabled = isLocked || stateExists;
1315+
addButton.title = stateExists ? "Selected state already exists; Add will report a duplicate warning." : `Add ${selectedStateId} state`;
1316+
addButton.disabled = isLocked || !OBJECT_STATE_IDS.includes(selectedStateId);
13171317
addButton.addEventListener("click", (event) => {
13181318
event.stopPropagation();
13191319
this.createSelectedState(selectedStateId);
@@ -3099,7 +3099,6 @@ export class ToolStarterApp {
30993099
}
31003100
if (this.objectStates(object).some((state) => state.id === stateId)) {
31013101
this.statusLog.write(`WARN Create state skipped: ${OBJECT_STATE_LABELS[stateId] || stateId} already exists for ${object.name}.`);
3102-
this.selectState(stateId, "existing state");
31033102
return;
31043103
}
31053104

@@ -4333,11 +4332,29 @@ export class ToolStarterApp {
43334332
delete shape.groupId;
43344333
}
43354334
});
4335+
this.pruneSingleMemberShapeGroups(nextObject);
43364336
this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Ungrouped ${selectedIndexes.size} selected shapes from ${Array.from(selectedGroupIds).join(", ")}.`, "Ungroup shapes failed schema validation", {
43374337
selectedShapeIndexes: selectedIndexes
43384338
});
43394339
}
43404340

4341+
pruneSingleMemberShapeGroups(object) {
4342+
const groupCounts = new Map();
4343+
sortedShapes(object).forEach((shape) => {
4344+
const groupId = String(shape.groupId || "").trim();
4345+
if (!groupId) {
4346+
return;
4347+
}
4348+
groupCounts.set(groupId, (groupCounts.get(groupId) || 0) + 1);
4349+
});
4350+
sortedShapes(object).forEach((shape) => {
4351+
const groupId = String(shape.groupId || "").trim();
4352+
if (groupId && groupCounts.get(groupId) < 2) {
4353+
delete shape.groupId;
4354+
}
4355+
});
4356+
}
4357+
43414358
applyShapeGeometryEdits() {
43424359
const selected = this.selectedShape();
43434360
if (!selected) {
@@ -4693,6 +4710,7 @@ export class ToolStarterApp {
46934710
const deleteIndex = this.selectedShapeIndex;
46944711
object.shapes = sortedShapes(object).filter((shape, shapeIndex) => shapeIndex !== deleteIndex)
46954712
.map((shape, index) => ({ ...shape, order: index + 1 }));
4713+
this.pruneSingleMemberShapeGroups(object);
46964714
this.removeDeletedShapeReferences(object, deleteIndex);
46974715
this.removeDanglingShapeOverrideReferences(nextPayload);
46984716
const selectedShapeIndex = sortedShapes(object).length ? 0 : -1;
@@ -4720,6 +4738,7 @@ export class ToolStarterApp {
47204738
const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id);
47214739
nextObject.shapes = sortedShapes(nextObject).filter((candidate, index) => index !== deleteIndex)
47224740
.map((candidate, index) => ({ ...candidate, order: index + 1 }));
4741+
this.pruneSingleMemberShapeGroups(nextObject);
47234742
this.removeDeletedShapeReferences(nextObject, deleteIndex);
47244743
this.removeDanglingShapeOverrideReferences(nextPayload);
47254744
const selectedShapeStillExists = this.selectedShapeIndex >= 0 && this.selectedShapeIndex < sortedShapes(nextObject).length;
@@ -5107,7 +5126,9 @@ export class ToolStarterApp {
51075126
const states = this.objectStates(object);
51085127
const state = states.find((candidate) => candidate.id === this.selectedStateId) || states[0] || null;
51095128
this.selectedStateId = state?.id || "";
5110-
this.stateControlStateId = this.selectedStateId || this.stateControlStateId;
5129+
if (!OBJECT_STATE_IDS.includes(this.stateControlStateId)) {
5130+
this.stateControlStateId = this.selectedStateId || OBJECT_STATE_IDS[0];
5131+
}
51115132
if (!state) {
51125133
this.selectedFrameId = "";
51135134
return;

0 commit comments

Comments
 (0)