Skip to content

Commit 48849fc

Browse files
author
DavidQ
committed
Fix regroup behavior add group move support and restore delete state action - PR_26133_054-group-regroup-group-move-and-state-delete-restore
1 parent 91a4e32 commit 48849fc

4 files changed

Lines changed: 249 additions & 27 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_053 Playwright V8 Coverage Report
1+
# PR_26133_054 Playwright V8 Coverage Report
22

3-
Task: PR_26133_053-state-add-and-ungroup-enable-rules
3+
Task: PR_26133_054-group-regroup-group-move-and-state-delete-restore
44
Date: 2026-05-15
55

66
## Result
@@ -24,16 +24,15 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
2424
## Relevant Runtime Coverage
2525

2626
```text
27-
(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 5098/5098; executed functions 544/571
27+
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5213/5213; executed functions 564/591
2928
```
3029

3130
## Guardrail
3231

3332
```text
34-
(100%) no changed runtime JS coverage warnings blocked this PR
33+
(100%) none - no changed runtime JS coverage warnings
3534
```
3635

3736
## PR-Specific Note
3837

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.
38+
The Workspace V2 run exercised Object Vector Studio V2 regroup behavior, selected-group movement, single-shape movement, state add/delete controls, final-state delete prevention, dirty-state tracking, schema validation, and Asteroids runtime object-vector rendering.
Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_053 Workspace V2 Playwright Results
1+
# PR_26133_054 Workspace V2 Playwright Results
22

3-
Task: PR_26133_053-state-add-and-ungroup-enable-rules
3+
Task: PR_26133_054-group-regroup-group-move-and-state-delete-restore
44
Date: 2026-05-15
55

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

1515
## PR-Specific Coverage
1616

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.
17+
- Verified regrouping selected shapes moves only the directly selected shapes into the new group.
18+
- Verified old groups are pruned when fewer than two shapes remain and orphan group indicators disappear.
19+
- Verified unselected members from an old group do not move into the new group.
20+
- Verified Move moves all shapes in the selected group while preserving relative positions.
21+
- Verified Move affects only one shape when the selected shape is not grouped.
22+
- Verified Delete State renders, deletes only the selected state, refreshes state tiles/timeline immediately, and blocks deleting the final remaining state.
23+
- Verified Delete State marks Object Vector Studio V2 workspace state dirty after a successful persisted edit.
2324

2425
## Additional Validation
2526

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.
27+
- Focused regroup/state slice passed:
28+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "single-member groups"` completed with 1 passed, 0 failed.
29+
- Focused grouped authoring slice passed:
30+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "asset authoring controls"` completed with 1 passed, 0 failed.
31+
- Focused dirty-state slice passed:
32+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "dirty state through persisted edits"` completed with 1 passed, 0 failed.
2833
- `git diff --check` passed.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3902,13 +3902,31 @@ test.describe("Workspace Manager V2 bootstrap", () => {
39023902
await page.locator("#objectVectorStudioV2MoveXInput").fill("13");
39033903
await page.locator("#objectVectorStudioV2MoveYInput").fill("7");
39043904
await page.locator("#objectVectorStudioV2MoveShapeButton").click();
3905-
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved shape row 0 by 10, 10\./);
3905+
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved group group-1 \(2 shapes\) by 10, 10\./);
39063906
await expect(page.locator("#objectVectorStudioV2ObjectTransform .object-vector-studio-v2__transform-summary")).toHaveText("x 10, y 10, rot 0, scale 1");
3907+
const groupedMoveForwardTransforms = await page.evaluate(() => {
3908+
const app = window.__objectVectorStudioV2App;
3909+
const frame = app.activeFrame();
3910+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
3911+
const transform = app.effectiveShapeForFrame(shape, frame, shapeIndex).transform;
3912+
return { x: transform.x, y: transform.y };
3913+
});
3914+
});
3915+
expect(groupedMoveForwardTransforms).toEqual([{ x: 10, y: 10 }, { x: 10, y: 10 }]);
39073916
await page.locator("#objectVectorStudioV2MoveXInput").fill("-5");
39083917
await page.locator("#objectVectorStudioV2MoveYInput").fill("-5");
39093918
await page.locator("#objectVectorStudioV2MoveShapeButton").click();
3910-
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved shape row 0 by -10, -10\./);
3919+
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved group group-1 \(2 shapes\) by -10, -10\./);
39113920
await expect(page.locator("#objectVectorStudioV2ObjectTransform .object-vector-studio-v2__transform-summary")).toHaveText("x 0, y 0, rot 0, scale 1");
3921+
const groupedMoveBackTransforms = await page.evaluate(() => {
3922+
const app = window.__objectVectorStudioV2App;
3923+
const frame = app.activeFrame();
3924+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
3925+
const transform = app.effectiveShapeForFrame(shape, frame, shapeIndex).transform;
3926+
return { x: transform.x, y: transform.y };
3927+
});
3928+
});
3929+
expect(groupedMoveBackTransforms).toEqual([{ x: 0, y: 0 }, { x: 0, y: 0 }]);
39123930

39133931
await page.locator("#objectVectorStudioV2CopyJsonButton").click();
39143932
const copiedPayload = await page.evaluate(() => JSON.parse(sessionStorage.getItem("object-vector-studio-v2.authoring-copied-json")));
@@ -4053,16 +4071,17 @@ test.describe("Workspace Manager V2 bootstrap", () => {
40534071
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-frame-state-select]")).toHaveCount(0);
40544072
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-frame-state-help]")).toHaveCount(0);
40554073
const selectedObjectStatePanel = page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.animation.ship-template'] .object-vector-studio-v2__object-state-panel");
4056-
await expect(selectedObjectStatePanel.locator(".object-vector-studio-v2__object-state-controls button")).toHaveText(["Add", "?"]);
4074+
await expect(selectedObjectStatePanel.locator(".object-vector-studio-v2__object-state-controls button")).toHaveText(["Add", "Delete", "?"]);
40574075
const stateControlLayout = await selectedObjectStatePanel.locator(".object-vector-studio-v2__object-state-controls").evaluate((controls) => Array.from(controls.children).map((element) => {
40584076
if (element.tagName.toLowerCase() === "select") {
40594077
return `select:${element.dataset.objectStateSelect}`;
40604078
}
40614079
return `${element.dataset.objectStateAction || element.dataset.objectStateHelp}:${element.textContent.trim()}`;
40624080
}));
4063-
expect(stateControlLayout).toEqual(["select:object.animation.ship-template", "add:Add", "all:?"]);
4081+
expect(stateControlLayout).toEqual(["select:object.animation.ship-template", "add:Add", "delete:Delete", "all:?"]);
40644082
await expect(selectedObjectStatePanel.locator("[data-object-state-select='object.animation.ship-template']")).toHaveValue("idle");
40654083
await expect(selectedObjectStatePanel.locator("[data-object-state-action='add']")).toBeDisabled();
4084+
await expect(selectedObjectStatePanel.locator("[data-object-state-action='delete']")).toBeEnabled();
40664085
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\./);
40674086
await expect(selectedObjectStatePanel.locator("[data-object-state-tile]")).toHaveText(["idle", "move"]);
40684087
await expect(selectedObjectStatePanel.locator("[data-object-state-tile='idle']")).toHaveAttribute("aria-pressed", "true");
@@ -4275,6 +4294,24 @@ test.describe("Workspace Manager V2 bootstrap", () => {
42754294
tool: "rectangle",
42764295
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
42774296
visible: true
4297+
},
4298+
{
4299+
geometry: { x: 36, y: -20, width: 16, height: 16 },
4300+
locked: false,
4301+
order: 3,
4302+
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#6fd3ff", strokeOpacity: 1, strokeWidth: 2 },
4303+
tool: "rectangle",
4304+
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
4305+
visible: true
4306+
},
4307+
{
4308+
geometry: { x: 64, y: -20, width: 16, height: 16 },
4309+
locked: false,
4310+
order: 4,
4311+
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#6fd3ff", strokeOpacity: 1, strokeWidth: 2 },
4312+
tool: "rectangle",
4313+
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
4314+
visible: true
42784315
}
42794316
],
42804317
states: [
@@ -4297,15 +4334,17 @@ test.describe("Workspace Manager V2 bootstrap", () => {
42974334
const objectTile = page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.test.group-state']");
42984335
const statePanel = objectTile.locator(".object-vector-studio-v2__object-state-panel");
42994336
const addStateButton = statePanel.locator("[data-object-state-action='add']");
4337+
const deleteStateButton = statePanel.locator("[data-object-state-action='delete']");
43004338
const stateSelect = statePanel.locator("[data-object-state-select='object.test.group-state']");
43014339
const stateControlLayout = await statePanel.locator(".object-vector-studio-v2__object-state-controls").evaluate((controls) => Array.from(controls.children).map((element) => {
43024340
if (element.tagName.toLowerCase() === "select") {
43034341
return "select";
43044342
}
43054343
return element.dataset.objectStateAction || element.dataset.objectStateHelp;
43064344
}));
4307-
expect(stateControlLayout).toEqual(["select", "add", "all"]);
4345+
expect(stateControlLayout).toEqual(["select", "add", "delete", "all"]);
43084346
await expect(addStateButton).toBeDisabled();
4347+
await expect(deleteStateButton).toBeDisabled();
43094348
await expect(statePanel.locator("[data-object-state-tile]")).toHaveText(["idle"]);
43104349

43114350
await stateSelect.selectOption("move");
@@ -4317,22 +4356,73 @@ test.describe("Workspace Manager V2 bootstrap", () => {
43174356
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-state-id='move']")).toHaveCount(1);
43184357
await expect(page.locator("#statusLog")).toHaveValue(/OK Created state Move with frame frame-1\./);
43194358
await expect(addStateButton).toBeDisabled();
4359+
await expect(deleteStateButton).toBeEnabled();
43204360
await expect(statePanel.locator("[data-object-state-tile='move']")).toHaveCount(1);
4361+
await deleteStateButton.click();
4362+
await expect(statePanel.locator("[data-object-state-tile]")).toHaveText(["idle"]);
4363+
await expect(statePanel.locator("[data-object-state-tile='idle']")).toHaveAttribute("aria-pressed", "true");
4364+
await expect(page.locator("#objectVectorStudioV2FrameTimeline [data-state-id='idle']")).toHaveCount(1);
4365+
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted state move from Group State\./);
4366+
await expect(deleteStateButton).toBeDisabled();
43214367

43224368
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(2);
4369+
await page.evaluate(() => {
4370+
const app = window.__objectVectorStudioV2App;
4371+
app.selectShape(0, "regroup existing shape");
4372+
app.selectShape(2, "regroup ungrouped shape", { additive: true });
4373+
});
4374+
const selectedShapeActions = objectTile.locator(".object-vector-studio-v2__shape-list-actions");
4375+
await expect(selectedShapeActions.locator("[data-shape-list-action='group']")).toBeEnabled();
4376+
await selectedShapeActions.locator("[data-shape-list-action='group']").click();
4377+
await expect(page.locator("#statusLog")).toHaveValue(/OK Grouped 2 shapes into group-2\./);
4378+
const groupsAfterRegroup = await page.evaluate(() => window.__objectVectorStudioV2App.selectedObject().shapes.map((shape) => shape.groupId || ""));
4379+
expect(groupsAfterRegroup).toEqual(["group-2", "", "group-2", ""]);
4380+
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(0);
4381+
await expect(objectTile.locator("[data-shape-group-id='group-2']")).toHaveCount(2);
4382+
4383+
await page.evaluate(() => window.__objectVectorStudioV2App.selectShape(0, "group move probe"));
4384+
await page.locator("#objectVectorStudioV2MoveXInput").fill("5");
4385+
await page.locator("#objectVectorStudioV2MoveYInput").fill("5");
4386+
await page.locator("#objectVectorStudioV2MoveShapeButton").click();
4387+
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved group group-2 \(2 shapes\) by 5, 5\./);
4388+
const transformsAfterGroupMove = await page.evaluate(() => {
4389+
const app = window.__objectVectorStudioV2App;
4390+
const frame = app.activeFrame();
4391+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
4392+
const transform = app.effectiveShapeForFrame(shape, frame, shapeIndex).transform;
4393+
return { x: transform.x, y: transform.y };
4394+
});
4395+
});
4396+
expect(transformsAfterGroupMove).toEqual([{ x: 5, y: 5 }, { x: 0, y: 0 }, { x: 5, y: 5 }, { x: 0, y: 0 }]);
4397+
4398+
await page.evaluate(() => window.__objectVectorStudioV2App.selectShape(1, "single move probe"));
4399+
await page.locator("#objectVectorStudioV2MoveXInput").fill("-2");
4400+
await page.locator("#objectVectorStudioV2MoveYInput").fill("-3");
4401+
await page.locator("#objectVectorStudioV2MoveShapeButton").click();
4402+
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved shape row 1 by -2, -3\./);
4403+
const transformsAfterSingleMove = await page.evaluate(() => {
4404+
const app = window.__objectVectorStudioV2App;
4405+
const frame = app.activeFrame();
4406+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
4407+
const transform = app.effectiveShapeForFrame(shape, frame, shapeIndex).transform;
4408+
return { x: transform.x, y: transform.y };
4409+
});
4410+
});
4411+
expect(transformsAfterSingleMove).toEqual([{ x: 5, y: 5 }, { x: -2, y: -3 }, { x: 5, y: 5 }, { x: 0, y: 0 }]);
4412+
43234413
await page.evaluate(() => {
43244414
const app = window.__objectVectorStudioV2App;
43254415
app.selectedShapeIndex = 0;
43264416
app.selectedShapeIndexes = new Set([0]);
4417+
app.directSelectedShapeIndexes = new Set([0]);
43274418
app.renderPayload();
43284419
});
4329-
const selectedShapeActions = objectTile.locator(".object-vector-studio-v2__shape-list-actions");
43304420
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeEnabled();
43314421
await selectedShapeActions.locator("[data-shape-list-action='ungroup']").click();
4332-
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"groupId": "group-1"');
4333-
await expect(objectTile.locator("[data-shape-group-id='group-1']")).toHaveCount(0);
4422+
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"groupId": "group-2"');
4423+
await expect(objectTile.locator("[data-shape-group-id='group-2']")).toHaveCount(0);
43344424
await expect(selectedShapeActions.locator("[data-shape-list-action='ungroup']")).toBeDisabled();
4335-
await expect(page.locator("#statusLog")).toHaveValue(/OK Ungrouped 1 selected shapes from group-1\./);
4425+
await expect(page.locator("#statusLog")).toHaveValue(/OK Ungrouped 1 selected shapes from group-2\./);
43364426

43374427
expect(pageErrors).toEqual([]);
43384428
} finally {
@@ -8260,6 +8350,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
82608350
await expect(selectedObjectTile.locator("[data-object-state-action='add']")).toBeEnabled();
82618351
await selectedObjectTile.locator("[data-object-state-action='add']").click();
82628352
});
8353+
await expectObjectVectorDirtyAfter("object state delete edit", async () => {
8354+
const selectedObjectId = await page.evaluate(() => window.__objectVectorStudioV2App.selectedObjectId);
8355+
const selectedObjectTile = page.locator(`.object-vector-studio-v2__object-tile[data-object-id="${selectedObjectId}"]`);
8356+
await expect(selectedObjectTile.locator("[data-object-state-action='delete']")).toBeEnabled();
8357+
await selectedObjectTile.locator("[data-object-state-action='delete']").click();
8358+
});
82638359
await expectObjectVectorDirtyAfter("object geometry edit", async () => {
82648360
await page.locator("#objectVectorStudioV2ObjectDetails [data-shape-geometry-field='points'][data-polygon-point-index='0'][data-polygon-point-axis='x']").fill("11");
82658361
await page.locator("#objectVectorStudioV2ApplyGeometryButton").click();

0 commit comments

Comments
 (0)