Skip to content

Commit 9be065c

Browse files
author
DavidQ
committed
Build Object Vector Studio V2 layout shell and schema-loading foundation - PR_26132_004-object-vector-studio-v2-layout-shell
1 parent 287e2ed commit 9be065c

10 files changed

Lines changed: 973 additions & 284 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# PR_26132_004-object-vector-studio-v2-layout-shell
2+
3+
## Purpose
4+
5+
Build the Object Vector Studio V2 layout shell without implementing full object editing.
6+
7+
## Scope
8+
9+
- Added the Object Vector Studio V2 workspace nav with Return to Workspace.
10+
- Added the standalone tool nav with Import, Copy JSON, and Export actions.
11+
- Replaced the copied template content with the requested left, center, and right layout shell.
12+
- Added left column accordions for Object, Shape/Tools, and Objects.
13+
- Added right column accordions for Palette, Object Details, JSON Details, and Status Log.
14+
- Added equal accordion space sharing, collapsed-state height adjustment, and scrollable control content.
15+
- Added placeholder object tiles and shape/tool icon/text toggle buttons.
16+
- Added schema-only payload loading foundation that requires a palette before rendering.
17+
- Preserved Object Vector Studio V2 scope only. World Vector Studio V2 was not modified.
18+
19+
## Validation
20+
21+
Playwright impacted: Yes.
22+
23+
Command run:
24+
25+
```powershell
26+
npm run test:workspace-v2
27+
```
28+
29+
Result:
30+
31+
```text
32+
39 passed
33+
```
34+
35+
Additional checks:
36+
37+
```powershell
38+
node --check tools/object-vector-studio-v2/js/bootstrap.js
39+
node --check tools/object-vector-studio-v2/js/ToolStarterApp.js
40+
node --check tools/object-vector-studio-v2/js/controls/ActionNavControl.js
41+
node --check tools/object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js
42+
node --check tools/object-vector-studio-v2/tests/playwright/FirstClassToolStarter.spec.mjs
43+
node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs
44+
git diff --check
45+
```
46+
47+
All checks passed.
48+
49+
## Playwright Coverage
50+
51+
The Workspace Manager V2 suite now validates:
52+
53+
- Object Vector Studio V2 launch from the tools surface.
54+
- Tool nav actions for Import, Copy JSON, and Export.
55+
- Workspace launch nav with Return to Workspace and preserved `hostContextId`.
56+
- Requested left and right accordion shell labels.
57+
- Schema-only palette gate and invalid payload rejection before render.
58+
- Valid palette-backed payload render into object tiles, selected object details, JSON details, and status log.
59+
- Shape/tool toggle state and status logging.
60+
- Equal accordion space sharing and collapsed-state adjustment.
61+
- Scrollable object tile content.
62+
- Hide Header and Details fullscreen-style layout with side columns and filled center work area.
63+
64+
Expected pass behavior:
65+
66+
- A palette-backed Object Vector Studio V2 payload renders shell details and enables JSON actions.
67+
- A payload missing `palette` fails visibly and leaves the empty state rendered.
68+
- Workspace launch shows Return to Workspace only.
69+
70+
Expected fail behavior:
71+
72+
- Missing palette, invalid JSON, or missing workspace toolState data is rejected before render and logged as actionable status.
73+
74+
## V8 Coverage
75+
76+
The required Playwright V8 coverage report was produced at:
77+
78+
```text
79+
docs/dev/reports/playwright_v8_coverage_report.txt
80+
```
81+
82+
Changed runtime JavaScript coverage includes:
83+
84+
```text
85+
(86%) tools/object-vector-studio-v2/js/ToolStarterApp.js
86+
(91%) tools/object-vector-studio-v2/js/controls/ActionNavControl.js
87+
(92%) tools/object-vector-studio-v2/js/controls/ToolStarterShellControl.js
88+
(100%) tools/object-vector-studio-v2/js/bootstrap.js
89+
(100%) tools/object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js
90+
```
91+
92+
Guardrail:
93+
94+
```text
95+
docs/dev/reports/coverage_changed_js_guardrail.txt
96+
```
97+
98+
No changed-runtime-JS warnings were reported.
99+
100+
## Full Samples Smoke Test
101+
102+
Skipped. This PR is limited to Object Vector Studio V2 layout shell behavior and targeted Workspace Manager V2 launch/layout coverage. It does not modify shared sample loading or broad sample runtime behavior.
103+
104+
## Manual Test Steps
105+
106+
1. Open `tools/object-vector-studio-v2/index.html`.
107+
2. Confirm the header shows Object Vector Studio V2 and the standalone nav shows Import, Copy JSON, Export.
108+
3. Confirm left accordions are Object, Shape/Tools, Objects.
109+
4. Confirm right accordions are Palette, Object Details, JSON Details, Status Log.
110+
5. Import JSON without `palette` and confirm it is rejected before render.
111+
6. Import JSON with `palette.swatches` and `objects[]` containing `id` and `name`, then confirm object tiles render and JSON actions enable.
112+
7. Launch with `?launch=workspace&fromTool=workspace-manager-v2&hostContextId=manual-check` and confirm Return to Workspace appears.
113+
114+
## Out Of Scope
115+
116+
- Full vector object editing.
117+
- Shape editing, path editing, animation structures, and palette editing.
118+
- World Vector Studio V2 runtime behavior.
119+
- Full samples smoke test.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from "@playwright/test";
2-
import { readFile } from "node:fs/promises";
2+
import { readFile, writeFile } from "node:fs/promises";
33
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
44
import { workspaceV2CoverageReporter as coverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs";
55

@@ -1142,10 +1142,16 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11421142
await expect(page.locator("[data-tool-starter-header]")).toContainText(tool.description);
11431143
await expect(page.locator(".tool-starter__tool__menu")).toBeVisible();
11441144
await expect(page.locator(".tool-starter__workspace__menu")).toBeHidden();
1145-
await page.locator("#sourceInput").fill(`${tool.name} launch coverage`);
1146-
await page.locator("#toolExportButton").click();
1147-
await expect(page.locator("#inspectorOutput")).toContainText(`"toolId": "${tool.id}"`);
1148-
await expect(page.locator("#statusLog")).toHaveValue(new RegExp(`Processed source value: ${tool.name} launch coverage`));
1145+
if (tool.id === "world-vector-studio-v2") {
1146+
await page.locator("#sourceInput").fill(`${tool.name} launch coverage`);
1147+
await page.locator("#toolExportButton").click();
1148+
await expect(page.locator("#inspectorOutput")).toContainText(`"toolId": "${tool.id}"`);
1149+
await expect(page.locator("#statusLog")).toHaveValue(new RegExp(`Processed source value: ${tool.name} launch coverage`));
1150+
} else {
1151+
await expect(page.locator('[data-launch-mode-nav="tool"] button')).toHaveText(["Import", "Copy JSON", "Export"]);
1152+
await expect(page.locator("#objectVectorStudioV2LoadStatus")).toContainText("Schema-only loading is idle");
1153+
await expect(page.locator("#objectVectorStudioV2ObjectTiles")).toContainText("No objects loaded");
1154+
}
11491155
}
11501156
expect(pageErrors).toEqual([]);
11511157
} finally {
@@ -1154,6 +1160,138 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11541160
}
11551161
});
11561162

1163+
test("shows Object Vector Studio V2 layout shell and schema-only palette gate", async ({ page }, testInfo) => {
1164+
const server = await startRepoServer();
1165+
const pageErrors = [];
1166+
1167+
page.on("pageerror", (error) => {
1168+
pageErrors.push(error.message);
1169+
});
1170+
1171+
await coverageReporter.start(page);
1172+
try {
1173+
await page.goto(`${server.baseUrl}/tools/object-vector-studio-v2/index.html`, { waitUntil: "networkidle" });
1174+
await expect(page.locator("body.tools-platform-tool-page[data-tool-id='object-vector-studio-v2']")).toBeVisible();
1175+
await expect(page.locator("[data-tool-starter-header]")).toContainText("Object Vector Studio V2");
1176+
await expect(page.locator('[data-launch-mode-nav="tool"]')).toBeVisible();
1177+
await expect(page.locator('[data-launch-mode-nav="tool"] button')).toHaveText(["Import", "Copy JSON", "Export"]);
1178+
await expect(page.locator('[data-launch-mode-nav="workspace"]')).toBeHidden();
1179+
await expect(page.locator("#objectVectorStudioV2CopyJsonButton")).toBeDisabled();
1180+
await expect(page.locator("#objectVectorStudioV2ExportJsonButton")).toBeDisabled();
1181+
1182+
await expect(page.locator(".tool-starter__panel--left > .accordion-v2 > .accordion-v2__header > span:first-child")).toHaveText(["Object", "Shape/Tools", "Objects"]);
1183+
await expect(page.locator(".tool-starter__panel--right > .accordion-v2 > .accordion-v2__header > span:first-child")).toHaveText(["Palette", "Object Details", "JSON Details", "Status Log"]);
1184+
await expect(page.locator("#objectVectorStudioV2PaletteGate")).toHaveValue("Required before render");
1185+
await expect(page.locator("#objectVectorStudioV2ObjectTiles")).toContainText("No objects loaded");
1186+
await expect(page.locator(".object-vector-studio-v2__tool-toggle")).toHaveText(["+ Select", "Sh Shape", "P Path", "M Move", "R Rotate", "G Group"]);
1187+
1188+
await page.locator('[data-shape-tool="path"]').click();
1189+
await expect(page.locator('[data-shape-tool="path"]')).toHaveAttribute("aria-pressed", "true");
1190+
await expect(page.locator("#statusLog")).toHaveValue(/INFO Shape\/Tools shell toggle selected: path/);
1191+
1192+
const invalidPayloadPath = testInfo.outputPath("object-vector-invalid.json");
1193+
await writeFile(invalidPayloadPath, JSON.stringify({ objects: [] }, null, 2), "utf8");
1194+
await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles(invalidPayloadPath);
1195+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Object Vector Studio V2 schema validation failed from import:object-vector-invalid\.json: root\.palette is required\./);
1196+
await expect(page.locator("#objectVectorStudioV2ObjectTiles")).toContainText("No objects loaded");
1197+
1198+
const validPayload = {
1199+
palette: {
1200+
id: "arcade-primary",
1201+
swatches: [
1202+
{ id: "white", value: "#ffffff" },
1203+
{ id: "cyan", value: "#6fd3ff" }
1204+
]
1205+
},
1206+
objects: Array.from({ length: 18 }, (_, index) => ({
1207+
id: `object-${index + 1}`,
1208+
name: index === 0 ? "Asteroids Ship" : `Object ${index + 1}`,
1209+
type: index === 1 ? "enemy" : "ship"
1210+
}))
1211+
};
1212+
const validPayloadPath = testInfo.outputPath("object-vector-valid.json");
1213+
await writeFile(validPayloadPath, JSON.stringify(validPayload, null, 2), "utf8");
1214+
await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles(validPayloadPath);
1215+
await expect(page.locator("#objectVectorStudioV2PaletteGate")).toHaveValue("Palette loaded");
1216+
await expect(page.locator("#objectVectorStudioV2PaletteSummary")).toContainText("Palette arcade-primary: 2 swatches.");
1217+
await expect(page.locator("#objectVectorStudioV2ObjectTiles .object-vector-studio-v2__object-tile")).toHaveCount(18);
1218+
await expect(page.locator("#objectVectorStudioV2ObjectDetails")).toContainText("Asteroids Ship");
1219+
await expect(page.locator("#objectVectorStudioV2SelectedItemVisibility")).toContainText("Selected item visible: Asteroids Ship");
1220+
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"palette"');
1221+
await expect(page.locator("#objectVectorStudioV2CopyJsonButton")).toBeEnabled();
1222+
await expect(page.locator("#objectVectorStudioV2ExportJsonButton")).toBeEnabled();
1223+
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded Object Vector Studio V2 schema payload from import:object-vector-valid\.json: 18 objects, 2 palette swatches\./);
1224+
1225+
await page.locator('[data-object-id="object-2"]').click();
1226+
await expect(page.locator('[data-object-id="object-2"]')).toHaveAttribute("aria-pressed", "true");
1227+
await expect(page.locator("#objectVectorStudioV2ObjectDetails")).toContainText("Object 2");
1228+
1229+
const leftOpenHeights = await page.locator(".tool-starter__panel--left .accordion-v2.is-open").evaluateAll((sections) => (
1230+
sections.map((section) => Math.round(section.getBoundingClientRect().height))
1231+
));
1232+
expect(Math.max(...leftOpenHeights) - Math.min(...leftOpenHeights)).toBeLessThanOrEqual(18);
1233+
1234+
const tileScrollState = await page.locator("#objectVectorStudioV2ObjectTiles").evaluate((element) => ({
1235+
clientHeight: Math.round(element.clientHeight),
1236+
scrollHeight: Math.round(element.scrollHeight)
1237+
}));
1238+
expect(tileScrollState.scrollHeight).toBeGreaterThan(tileScrollState.clientHeight);
1239+
1240+
await page.getByRole("button", { name: "Shape/Tools" }).click();
1241+
await expect(page.locator("#objectVectorStudioV2ShapeToolsContent")).toBeHidden();
1242+
const collapsedLayout = await page.locator(".tool-starter__panel--left .accordion-v2").evaluateAll((sections) => (
1243+
sections.map((section) => ({
1244+
isOpen: section.classList.contains("is-open"),
1245+
height: Math.round(section.getBoundingClientRect().height)
1246+
}))
1247+
));
1248+
const collapsedHeight = collapsedLayout.find((entry) => !entry.isOpen)?.height || 0;
1249+
expect(collapsedHeight).toBeLessThan(64);
1250+
const remainingHeights = collapsedLayout.filter((entry) => entry.isOpen).map((entry) => entry.height);
1251+
expect(Math.max(...remainingHeights) - Math.min(...remainingHeights)).toBeLessThanOrEqual(18);
1252+
1253+
const summary = page.locator("[data-tool-starter-summary]");
1254+
await summary.click();
1255+
await expect(page.locator(".is-collapsible")).not.toHaveAttribute("open", "");
1256+
const fullscreenActive = await page.locator("body").evaluate((body) => body.classList.contains("tools-platform-fullscreen-active"));
1257+
if (!fullscreenActive) {
1258+
await page.evaluate(() => {
1259+
window.__objectVectorStudioV2App.shell.applyFullscreenState(true);
1260+
window.__objectVectorStudioV2App.shell.updateSummary();
1261+
});
1262+
}
1263+
await expect(page.locator("body")).toHaveClass(/tools-platform-fullscreen-active/);
1264+
const fullscreenLayout = await page.evaluate(() => {
1265+
const left = document.querySelector(".tool-starter__panel--left").getBoundingClientRect();
1266+
const center = document.querySelector(".tool-starter__panel--center").getBoundingClientRect();
1267+
const right = document.querySelector(".tool-starter__panel--right").getBoundingClientRect();
1268+
return {
1269+
centerHeight: Math.round(center.height),
1270+
centerWidth: Math.round(center.width),
1271+
leftBeforeCenter: left.right <= center.left,
1272+
rightAfterCenter: right.left >= center.right
1273+
};
1274+
});
1275+
expect(fullscreenLayout.leftBeforeCenter).toBe(true);
1276+
expect(fullscreenLayout.rightAfterCenter).toBe(true);
1277+
expect(fullscreenLayout.centerWidth).toBeGreaterThan(300);
1278+
expect(fullscreenLayout.centerHeight).toBeGreaterThan(300);
1279+
1280+
await page.goto(`${server.baseUrl}/tools/object-vector-studio-v2/index.html?launch=workspace&fromTool=workspace-manager-v2&hostContextId=object-vector-v2-shell&workspaceMode=uat`, { waitUntil: "networkidle" });
1281+
await expect(page.locator('[data-launch-mode-nav="tool"]')).toBeHidden();
1282+
await expect(page.locator('[data-launch-mode-nav="workspace"] button')).toHaveText(["Return to Workspace"]);
1283+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Schema-only workspace loading blocked: workspace\.tools\.object-vector-studio-v2 is missing/);
1284+
await page.locator("#returnToWorkspaceButton").click();
1285+
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html.*hostContextId=object-vector-v2-shell/);
1286+
await expect(page).toHaveURL(/workspace=uat/);
1287+
1288+
expect(pageErrors).toEqual([]);
1289+
} finally {
1290+
await coverageReporter.stop(page);
1291+
await server.close();
1292+
}
1293+
});
1294+
11571295
test("uses First-Class Tool V2 theme contract", async ({ page }) => {
11581296
const server = await openWorkspaceManagerV2(page);
11591297
const pageErrors = [];

0 commit comments

Comments
 (0)