Skip to content

Commit 91a4e32

Browse files
author
DavidQ
committed
Add enablement rules for state creation and ungroup actions - PR_26133_053-state-add-and-ungroup-enable-rules
1 parent 0356442 commit 91a4e32

4 files changed

Lines changed: 59 additions & 35 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_052 Playwright V8 Coverage Report
1+
# PR_26133_053 Playwright V8 Coverage Report
22

3-
Task: PR_26133_052-group-cleanup-and-add-state-enable-fix
3+
Task: PR_26133_053-state-add-and-ungroup-enable-rules
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 5093/5093; executed functions 542/570
28+
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5098/5098; executed functions 544/571
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 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.
39+
The Workspace V2 run exercised Object Vector Studio V2 state-control ordering, Add State existing/new-state disabled rules, grouped and non-grouped Ungroup disabled rules, group cleanup after ungroup, Object Vector schema validation, and Asteroids runtime object-vector rendering.
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_052 Workspace V2 Playwright Results
1+
# PR_26133_053 Workspace V2 Playwright Results
22

3-
Task: PR_26133_052-group-cleanup-and-add-state-enable-fix
3+
Task: PR_26133_053-state-add-and-ungroup-enable-rules
44
Date: 2026-05-15
55

66
## Result
@@ -14,15 +14,15 @@ PASS - `npm run test:workspace-v2` completed successfully.
1414

1515
## PR-Specific Coverage
1616

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.
17+
- Verified state controls render in the requested order: state dropdown, Add, help.
18+
- Verified Add is disabled when the selected dropdown state already exists on the object.
19+
- Verified Add enables for a valid non-existing state and creates/selects the new state.
20+
- Verified Ungroup is disabled for non-grouped selected shapes.
21+
- Verified Ungroup enables for grouped selected shapes and updates immediately after selection/render changes.
22+
- Verified prior single-member group cleanup remains active after ungroup.
2323

2424
## Additional Validation
2525

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.
26+
- Focused PR053 slice passed before the full run:
27+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --grep "single-member groups|animation states|layout shell"` completed with 3 passed, 0 failed.
2828
- `git diff --check` passed.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,12 +2515,15 @@ test.describe("Workspace Manager V2 bootstrap", () => {
25152515
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-tile-shape-index='0']").click();
25162516
await expect(selectedShapeActions.locator("[data-shape-list-action='group']")).toBeDisabled();
25172517
await expect(selectedShapeActions.locator("[data-shape-list-action='group']")).toHaveAttribute("data-disabled-reason", /Shift-click shapes/);
2518+
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeDisabled();
2519+
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toHaveAttribute("data-disabled-reason", /belongs to a group/);
25182520
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-tile-shape-index='1']").click({ modifiers: ["Shift"] });
25192521
await expect(selectedShapeActions.locator("[data-shape-list-action='group']")).toBeEnabled();
25202522
await expect(selectedShapeActions.locator("[data-shape-list-action='group']")).toHaveAttribute("title", /Shift-click shapes to select more than one/);
25212523
await selectedShapeActions.locator("[data-shape-list-action='group']").click();
25222524
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"groupId": "group-1"');
25232525
await expect(page.locator("#statusLog")).toHaveValue(/OK Grouped 2 shapes into group-1\./);
2526+
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeEnabled();
25242527
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-shape-group-id='group-1']")).toHaveCount(2);
25252528
const groupIconColors = await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-shape-group-id='group-1']").evaluateAll((icons) => icons.map((icon) => getComputedStyle(icon).color));
25262529
expect(new Set(groupIconColors).size).toBe(1);
@@ -2532,6 +2535,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
25322535
await selectedShapeActions.locator("[data-shape-list-action='ungroup']").click();
25332536
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"groupId": "group-1"');
25342537
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-shape-group-id='group-1']")).toHaveCount(0);
2538+
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeDisabled();
25352539
await expect(page.locator("#statusLog")).toHaveValue(/OK Ungrouped 2 selected shapes from group-1\./);
25362540
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-tile-shape-index='0']").click();
25372541

@@ -4049,8 +4053,16 @@ test.describe("Workspace Manager V2 bootstrap", () => {
40494053
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-frame-state-select]")).toHaveCount(0);
40504054
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-frame-state-help]")).toHaveCount(0);
40514055
const selectedObjectStatePanel = page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.animation.ship-template'] .object-vector-studio-v2__object-state-panel");
4052-
await expect(selectedObjectStatePanel.locator(".object-vector-studio-v2__object-state-controls button")).toHaveText(["Add", "Delete", "?"]);
4056+
await expect(selectedObjectStatePanel.locator(".object-vector-studio-v2__object-state-controls button")).toHaveText(["Add", "?"]);
4057+
const stateControlLayout = await selectedObjectStatePanel.locator(".object-vector-studio-v2__object-state-controls").evaluate((controls) => Array.from(controls.children).map((element) => {
4058+
if (element.tagName.toLowerCase() === "select") {
4059+
return `select:${element.dataset.objectStateSelect}`;
4060+
}
4061+
return `${element.dataset.objectStateAction || element.dataset.objectStateHelp}:${element.textContent.trim()}`;
4062+
}));
4063+
expect(stateControlLayout).toEqual(["select:object.animation.ship-template", "add:Add", "all:?"]);
40534064
await expect(selectedObjectStatePanel.locator("[data-object-state-select='object.animation.ship-template']")).toHaveValue("idle");
4065+
await expect(selectedObjectStatePanel.locator("[data-object-state-action='add']")).toBeDisabled();
40544066
await expect(selectedObjectStatePanel.locator("[data-object-state-help='all']")).toHaveAttribute("title", /idle\nDefault stationary state\.\nNo movement or action animation active\.\n\nmove\nMovement\/action state\.\nUsed for thrusting, walking, flying, or active movement visuals\./);
40554067
await expect(selectedObjectStatePanel.locator("[data-object-state-tile]")).toHaveText(["idle", "move"]);
40564068
await expect(selectedObjectStatePanel.locator("[data-object-state-tile='idle']")).toHaveAttribute("aria-pressed", "true");
@@ -4119,6 +4131,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
41194131
await selectedObjectStatePanel.locator("[data-object-state-select='object.animation.ship-template']").selectOption("move");
41204132
await expect(page.locator('[data-object-id="object.animation.ship-template"]')).toHaveAttribute("aria-pressed", "true");
41214133
await expect(selectedObjectStatePanel.locator("[data-object-state-select='object.animation.ship-template']")).toHaveValue("move");
4134+
await expect(selectedObjectStatePanel.locator("[data-object-state-action='add']")).toBeDisabled();
41224135
await expect(selectedObjectStatePanel.locator("[data-object-state-help='all']")).toHaveAttribute("title", /active\nObject is enabled and participating in gameplay\.\nTypically the default active runtime state\./);
41234136
await expect(selectedObjectStatePanel.locator("[data-object-state-tile='move']")).toHaveAttribute("aria-pressed", "true");
41244137
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-state-id='move']")).toHaveCount(1);
@@ -4285,9 +4298,14 @@ test.describe("Workspace Manager V2 bootstrap", () => {
42854298
const statePanel = objectTile.locator(".object-vector-studio-v2__object-state-panel");
42864299
const addStateButton = statePanel.locator("[data-object-state-action='add']");
42874300
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\./);
4301+
const stateControlLayout = await statePanel.locator(".object-vector-studio-v2__object-state-controls").evaluate((controls) => Array.from(controls.children).map((element) => {
4302+
if (element.tagName.toLowerCase() === "select") {
4303+
return "select";
4304+
}
4305+
return element.dataset.objectStateAction || element.dataset.objectStateHelp;
4306+
}));
4307+
expect(stateControlLayout).toEqual(["select", "add", "all"]);
4308+
await expect(addStateButton).toBeDisabled();
42914309
await expect(statePanel.locator("[data-object-state-tile]")).toHaveText(["idle"]);
42924310

42934311
await stateSelect.selectOption("move");
@@ -4298,8 +4316,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
42984316
await expect(statePanel.locator("[data-object-state-tile='move']")).toHaveAttribute("aria-pressed", "true");
42994317
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-state-id='move']")).toHaveCount(1);
43004318
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\./);
4319+
await expect(addStateButton).toBeDisabled();
43034320
await expect(statePanel.locator("[data-object-state-tile='move']")).toHaveCount(1);
43044321

43054322
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(2);
@@ -4310,9 +4327,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
43104327
app.renderPayload();
43114328
});
43124329
const selectedShapeActions = objectTile.locator(".object-vector-studio-v2__shape-list-actions");
4330+
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeEnabled();
43134331
await selectedShapeActions.locator("[data-shape-list-action='ungroup']").click();
43144332
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"groupId": "group-1"');
43154333
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(0);
4334+
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeDisabled();
43164335
await expect(page.locator("#statusLog")).toHaveValue(/OK Ungrouped 1 selected shapes from group-1\./);
43174336

43184337
expect(pageErrors).toEqual([]);

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

Lines changed: 20 additions & 15 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 will report a duplicate warning." : `Add ${selectedStateId} state`;
1316-
addButton.disabled = isLocked || !OBJECT_STATE_IDS.includes(selectedStateId);
1315+
addButton.title = stateExists ? "Selected state already exists." : `Add ${selectedStateId} state`;
1316+
addButton.disabled = isLocked || stateExists || !OBJECT_STATE_IDS.includes(selectedStateId);
13171317
addButton.addEventListener("click", (event) => {
13181318
event.stopPropagation();
13191319
this.createSelectedState(selectedStateId);
@@ -1341,17 +1341,6 @@ export class ToolStarterApp {
13411341
this.statusLog.write(`INFO State ${select.value} is ready to add for ${object.name}.`);
13421342
});
13431343

1344-
const deleteButton = document.createElement("button");
1345-
deleteButton.type = "button";
1346-
deleteButton.dataset.objectStateAction = "delete";
1347-
deleteButton.textContent = "Delete";
1348-
deleteButton.title = stateExists ? `Delete ${selectedStateId} state` : "Selected state has not been added.";
1349-
deleteButton.disabled = isLocked || !stateExists || this.objectStates(object).length <= 1;
1350-
deleteButton.addEventListener("click", (event) => {
1351-
event.stopPropagation();
1352-
this.deleteSelectedState(selectedStateId);
1353-
});
1354-
13551344
const helpButton = document.createElement("button");
13561345
helpButton.className = "object-vector-studio-v2__object-state-help";
13571346
helpButton.type = "button";
@@ -1361,7 +1350,7 @@ export class ToolStarterApp {
13611350
helpButton.dataset.objectStateHelp = "all";
13621351
helpButton.setAttribute("aria-label", `State help for all states: ${allHelpText.replace(/\s+/gu, " ")}`);
13631352

1364-
controls.append(addButton, select, deleteButton, helpButton);
1353+
controls.append(select, addButton, helpButton);
13651354
panel.append(controls, this.createObjectStateTileList(object));
13661355
return panel;
13671356
}
@@ -1520,7 +1509,13 @@ export class ToolStarterApp {
15201509
actions.append(groupButton);
15211510

15221511
const ungroupButton = this.createShapeActionButton("Ungroup", "ungroup", "ungroup", "Ungroup selected shapes.", () => this.ungroupSelectedShapes());
1523-
this.setControlDisabled(ungroupButton, !shape || isLocked, isLocked ? `Disabled because ${object.name} is locked for this runtime session.` : noShapeReason, "Ungroup selected shapes.");
1512+
const hasValidGroup = this.shapeBelongsToValidGroup(object, this.selectedShapeIndex);
1513+
this.setControlDisabled(
1514+
ungroupButton,
1515+
!shape || isLocked || !hasValidGroup,
1516+
isLocked ? `Disabled because ${object.name} is locked for this runtime session.` : "Disabled until the selected shape belongs to a group with at least two shapes.",
1517+
"Ungroup selected shapes."
1518+
);
15241519
actions.append(ungroupButton);
15251520
return actions;
15261521
}
@@ -3035,6 +3030,16 @@ export class ToolStarterApp {
30353030
.filter((index) => index >= 0);
30363031
}
30373032

3033+
shapeBelongsToValidGroup(object, shapeIndex) {
3034+
const shapes = sortedShapes(object);
3035+
const shape = shapes[normalizeShapeIndex(shapeIndex)];
3036+
const groupId = String(shape?.groupId || "").trim();
3037+
if (!groupId) {
3038+
return false;
3039+
}
3040+
return shapes.filter((candidate) => String(candidate?.groupId || "").trim() === groupId).length >= 2;
3041+
}
3042+
30383043
selectState(stateId, sourceLabel) {
30393044
const object = this.selectedObject();
30403045
if (!object) {

0 commit comments

Comments
 (0)