Skip to content

Commit ced7949

Browse files
author
DavidQ
committed
Move UAT ownership to Workspace Manager V2 and add manifest menu tool tiles - PR_26126_119-workspace-manager-v2-manifest-menu-uat-and-tool-tiles
1 parent 3fc7bbc commit ced7949

21 files changed

Lines changed: 719 additions & 230 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
PR_26126_119 manifest import-export notes
2+
3+
Scope:
4+
- Add Workspace Manager V2 menu actions for Import Manifest and Export Manifest.
5+
- Validate imported and exported manifests against tools/schemas/workspace.manifest.schema.json.
6+
- Route import/export success and failure through Workspace Manager V2 Status.
7+
8+
Implementation notes:
9+
- Import Manifest uses a hidden file input controlled from the Workspace Manager V2 menu.
10+
- Imported JSON is parsed, validated through WorkspaceManagerV2ContextService.buildContextFromManifest, persisted to session context, and rendered as the active workspace.
11+
- Export Manifest refreshes the active manifest from session state before export, validates against tools/schemas/workspace.manifest.schema.json, then downloads the validated manifest JSON.
12+
- Export is blocked when schema validation fails; the exact validation failure is logged to Status.
13+
14+
Validation:
15+
- npm run test:workspace-v2 PASS.
16+
- Playwright coverage imports a modified Asteroids manifest, verifies the active workspace updates, exports it, and confirms the exported file reflects the imported schema-valid manifest.
17+
- Existing schema-failure coverage verifies Export Manifest blocks invalid session manifests and logs the schema failure.
18+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
PR_26126_119 manual validation notes
2+
3+
Requested validation:
4+
- npm run test:workspace-v2
5+
6+
Result:
7+
- PASS. The workspace-v2 aggregate gate completed successfully with 24 passing Playwright tests.
8+
- Playwright/V8 coverage report was regenerated at docs/dev/reports/playwright_v8_coverage_report.txt.
9+
- git diff --check PASS, with only repository line-ending warnings reported by Git.
10+
11+
Manual review checklist:
12+
- Workspace Manager V2 Import Manifest and Export Manifest menu actions are present.
13+
- No direct tool launch buttons are present in the Workspace Manager V2 menu.
14+
- UAT is owned by Workspace Manager V2; Asset Manager V2 direct ?workspace=UAT launch is blocked by the launch guard overlay.
15+
- UAT button seeds a temporary Asteroids manifest/session from Workspace Manager V2.
16+
- Asset Manager V2, Palette Manager V2, and Preview Generator V2 launch through Workspace Manager V2 tool tiles.
17+
- Workspace-launched tools show only Return to Workspace in their workspace nav.
18+
- Deprecated tools/workspace-v2 was not modified.
19+
- sample JSON files were not modified.
20+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
PR_26126_119 tool tile launch notes
2+
3+
Scope:
4+
- Remove direct tool launch buttons from the Workspace Manager V2 menu.
5+
- Render workspace-launchable V2 tools as fixed-size launch tiles.
6+
- Launch tools through Workspace Manager V2 session/context.
7+
- Workspace-launched tools show only Return to Workspace in navWorkspace.
8+
9+
Implementation notes:
10+
- Workspace Manager V2 now renders fixed 180px by 118px tiles for Asset Manager V2, Palette Manager V2, and Preview Generator V2.
11+
- Tiles show tool name, launch state, and useful context detail:
12+
- Asset Manager V2: managed asset count.
13+
- Palette Manager V2: active palette swatch count.
14+
- Preview Generator V2: manifest validation status.
15+
- Tile launch URLs include launch=workspace, fromTool=workspace-manager-v2, and hostContextId.
16+
- Asset Manager V2, Palette Manager V2, and Preview Generator V2 each expose workspace nav containing only Return to Workspace when launched by Workspace Manager V2.
17+
18+
Validation:
19+
- npm run test:workspace-v2 PASS.
20+
- Playwright coverage validates tile sizing, status text, Asset Manager V2 launch, Palette Manager V2 launch, Preview Generator V2 launch, and Return to Workspace restoring the active manifest/session.
21+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
PR_26126_119 UAT SSoT notes
2+
3+
Scope:
4+
- Temporary UAT ownership moved to Workspace Manager V2.
5+
- Asset Manager V2 no longer accepts direct ?workspace=UAT launch.
6+
- Asset Manager V2 production and UAT access now require Workspace Manager V2 session/context.
7+
8+
Implementation notes:
9+
- Workspace Manager V2 exposes a UAT menu button only while launched with ?workspace=uat.
10+
- The UAT button seeds an in-memory/session workspace manifest with gameRoot games/Asteroids/, assetsPath games/Asteroids/assets, a sample palette, and an empty Asset Manager V2 asset registry.
11+
- UAT session state is persisted through WorkspaceManagerV2ContextService and launched tools receive context through hostContextId.
12+
- Asset Manager V2 reports a launch guard overlay for direct ?workspace=uat and ?workspace=prod URLs.
13+
14+
Validation:
15+
- npm run test:workspace-v2 PASS.
16+
- Dedicated Workspace Manager V2 coverage validates UAT button visibility, UAT manifest seeding, Asset Manager V2 tile launch from UAT session, and direct Asset Manager V2 ?workspace=uat guard failure.
17+

tests/playwright/tools/AssetManagerV2.spec.mjs

Lines changed: 62 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ async function openAssetManagerV2(page, query = "", { assetFiles = [] } = {}) {
4949
return server;
5050
}
5151

52-
async function openWorkspaceManagerV2(page, { assetFiles = [] } = {}) {
52+
async function openWorkspaceManagerV2(page, { assetFiles = [], query = "" } = {}) {
5353
const server = await startRepoServer();
5454
if (assetFiles.length) {
5555
await installFakeAssetFilePicker(page, assetFiles);
5656
}
5757
await coverageReporter.start(page);
58-
await page.goto(`${server.baseUrl}/tools/workspace-manager-v2/index.html`, { waitUntil: "networkidle" });
58+
await page.goto(`${server.baseUrl}/tools/workspace-manager-v2/index.html${query}`, { waitUntil: "networkidle" });
5959
return server;
6060
}
6161

@@ -162,7 +162,28 @@ test.describe("Asset Manager V2", () => {
162162
try {
163163
await expect(page.locator("#assetLaunchGuard")).toBeVisible();
164164
await expect(page.locator("#assetLaunchGuardMessage")).toHaveText("Asset Manager V2 is only available through Workspace Manager with a game workspace and palette.");
165-
await expect(page.locator("#assetLaunchGuardReason")).toContainText("Temporary workspace prod is not supported.");
165+
await expect(page.locator("#assetLaunchGuardReason")).toContainText("Temporary workspace query launches are no longer supported; launch through Workspace Manager V2.");
166+
await expect(page.locator(".asset-manager-v2.app-shell")).toHaveCount(1);
167+
await expect(page.locator("body")).toHaveClass(/asset-manager-v2--launch-blocked/);
168+
expect(pageErrors).toEqual([]);
169+
} finally {
170+
await coverageReporter.stop(page);
171+
await server.close();
172+
}
173+
});
174+
175+
test("blocks direct Asset Manager V2 temporary UAT launch", async ({ page }) => {
176+
const server = await openAssetManagerV2(page, "?workspace=uat");
177+
const pageErrors = [];
178+
179+
page.on("pageerror", (error) => {
180+
pageErrors.push(error.message);
181+
});
182+
183+
try {
184+
await expect(page.locator("#assetLaunchGuard")).toBeVisible();
185+
await expect(page.locator("#assetLaunchGuardMessage")).toHaveText("Asset Manager V2 is only available through Workspace Manager with a game workspace and palette.");
186+
await expect(page.locator("#assetLaunchGuardReason")).toContainText("Temporary workspace query launches are no longer supported; launch through Workspace Manager V2.");
166187
await expect(page.locator(".asset-manager-v2.app-shell")).toHaveCount(1);
167188
await expect(page.locator("body")).toHaveClass(/asset-manager-v2--launch-blocked/);
168189
expect(pageErrors).toEqual([]);
@@ -173,7 +194,8 @@ test.describe("Asset Manager V2", () => {
173194
});
174195

175196
test("launches Asset Manager V2 with temporary UAT context and schema-complete asset controls", async ({ page }) => {
176-
const server = await openAssetManagerV2(page, "?workspace=uat", {
197+
const server = await openWorkspaceManagerV2(page, {
198+
query: "?workspace=uat",
177199
assetFiles: [{
178200
name: "nebula-background.png",
179201
mimeType: "image/png",
@@ -188,10 +210,17 @@ test.describe("Asset Manager V2", () => {
188210
});
189211

190212
try {
213+
await expect(page.locator("#seedUatManifestButton")).toBeVisible();
214+
await page.locator("#seedUatManifestButton").click();
215+
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
216+
await page.locator('[data-workspace-tool-id="asset-manager-v2"]').click();
217+
await expect(page).toHaveURL(/asset-manager-v2\/index\.html.*launch=workspace/);
218+
await expect(page).not.toHaveURL(/workspace=uat/i);
191219
await expect(page.locator("body.tools-platform-tool-page[data-tool-id='asset-manager-v2']")).toBeVisible();
192220
await expect(page.locator(".asset-manager-v2.app-shell")).toBeVisible();
193-
await expect(page.locator(".asset-manager-v2__tool__menu")).toBeVisible();
194-
await expect(page.locator(".asset-manager-v2__workspace__menu")).toBeHidden();
221+
await expect(page.locator(".asset-manager-v2__tool__menu")).toBeHidden();
222+
await expect(page.locator(".asset-manager-v2__workspace__menu")).toBeVisible();
223+
await expect(page.locator(".asset-manager-v2__workspace__menu button")).toHaveText(["Return to Workspace"]);
195224
await expect(page.locator('fieldset[aria-label="Asset type"] legend')).toHaveText("Type");
196225
await expect(page.locator('input[name="assetKind"]')).toHaveCount(8);
197226
await expect(page.locator(".asset-manager-v2__kind-controls label span")).toHaveText([
@@ -876,79 +905,6 @@ test.describe("Asset Manager V2", () => {
876905
expect(statusReexpandedLayout.statusExpanded).toBe(true);
877906
expect(statusReexpandedLayout.statusBottomDelta).toBeLessThan(20);
878907

879-
const invalidImportChooserPromise = page.waitForEvent("filechooser");
880-
await page.locator("#navImportJsonButton").click();
881-
const invalidImportChooser = await invalidImportChooserPromise;
882-
await invalidImportChooser.setFiles({
883-
name: "invalid-asset-manager-v2.json",
884-
mimeType: "application/json",
885-
buffer: Buffer.from(JSON.stringify({ invalid: true }))
886-
});
887-
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Import JSON failed schema validation:/);
888-
await expect(page.locator("#inspectorOutput")).not.toContainText("Import JSON failed");
889-
890-
const importedPayload = {
891-
assets: {
892-
"assets.font.ui.vector-battle": {
893-
path: "assets/fonts/vector_battle.ttf",
894-
type: "font",
895-
kind: "ttf",
896-
role: "ui",
897-
source: "asset-manager-v2"
898-
},
899-
"assets.video.cutscene.8-mile": {
900-
path: "assets/video/8 mile.mp4",
901-
type: "video",
902-
kind: "mp4",
903-
role: "cutscene",
904-
source: "asset-manager-v2"
905-
}
906-
}
907-
};
908-
const validImportChooserPromise = page.waitForEvent("filechooser");
909-
await page.locator("#navImportJsonButton").click();
910-
const validImportChooser = await validImportChooserPromise;
911-
await validImportChooser.setFiles({
912-
name: "asset-manager-v2-assets.json",
913-
mimeType: "application/json",
914-
buffer: Buffer.from(JSON.stringify(importedPayload))
915-
});
916-
await expect(page.locator("#statusLog")).toHaveValue(/OK Imported JSON with 2 validated assets\./);
917-
await expect(page.locator("#statusLog")).not.toHaveValue(/Missing referenced file for assets\.font\.ui\.vector-battle/);
918-
await expect(page.locator("#statusLog")).toHaveValue(/INFO File availability warning: Missing referenced file for assets\.video\.cutscene\.8-mile: assets\/video\/8 mile\.mp4\./);
919-
const missingFileTileState = await page.locator(".asset-manager-v2__asset-tile").evaluateAll((tiles) => Object.fromEntries(tiles.map((tile) => {
920-
const id = tile.querySelector("button[data-asset-id]")?.dataset.assetId || "";
921-
const typeRole = tile.querySelector(".asset-manager-v2__asset-type-role");
922-
return [id, {
923-
isMissingFile: tile.classList.contains("is-missing-file"),
924-
typeRoleColor: getComputedStyle(typeRole).color
925-
}];
926-
})));
927-
expect(missingFileTileState["assets.font.ui.vector-battle"].isMissingFile).toBe(false);
928-
expect(missingFileTileState["assets.font.ui.vector-battle"].typeRoleColor).not.toBe("rgb(255, 180, 180)");
929-
expect(missingFileTileState["assets.video.cutscene.8-mile"]).toEqual({
930-
isMissingFile: true,
931-
typeRoleColor: "rgb(255, 180, 180)"
932-
});
933-
await expect(page.locator("#assetList")).toContainText("assets.font.ui.vector-battle");
934-
await expect(page.locator("#assetList")).toContainText("assets.video.cutscene.8-mile");
935-
await expect(page.locator("#selectedAssetDetails")).toContainText("assets.font.ui.vector-battle");
936-
await expect(page.locator("#selectedAssetDetails")).toContainText("assets/fonts/vector_battle.ttf");
937-
await expect(page.locator("#inspectorOutput")).not.toContainText("Imported JSON");
938-
const importedOutput = JSON.parse(await page.locator("#inspectorOutput").textContent());
939-
expect(importedOutput.assets.find((asset) => asset.id === "assets.font.ui.vector-battle").path).toBe("assets/fonts/vector_battle.ttf");
940-
expect(importedOutput.assets.find((asset) => asset.id === "assets.video.cutscene.8-mile").path).toBe("assets/video/8 mile.mp4");
941-
942-
const downloadPromise = page.waitForEvent("download");
943-
await page.locator("#navExportJsonButton").click();
944-
const download = await downloadPromise;
945-
expect(download.suggestedFilename()).toBe("asset-manager-v2-assets.json");
946-
const exportedPath = await download.path();
947-
const exportedPayload = JSON.parse(await readFile(exportedPath, "utf8"));
948-
expect(exportedPayload).toEqual(importedPayload);
949-
await expect(page.locator("#statusLog")).toHaveValue(/OK Exported JSON with 2 validated assets\./);
950-
await expect(page.locator("#inspectorOutput")).not.toContainText("Exported JSON");
951-
952908
await page.locator("#assetKindImage").check();
953909
await queueAssetFile(page, {
954910
name: "notes.exe",
@@ -970,7 +926,7 @@ test.describe("Asset Manager V2", () => {
970926
}
971927
});
972928

973-
test("loads Asset Manager V2 temporary UAT workspace context from query", async ({ page }) => {
929+
test("loads Asset Manager V2 temporary UAT workspace context from Workspace Manager V2", async ({ page }) => {
974930
await page.addInitScript(() => {
975931
const NativeFontFace = window.FontFace;
976932
window.__assetManagerV2FontFaceLoads = [];
@@ -994,7 +950,8 @@ test.describe("Asset Manager V2", () => {
994950
}
995951
};
996952
});
997-
const server = await openAssetManagerV2(page, "?workspace=UAT", {
953+
const server = await openWorkspaceManagerV2(page, {
954+
query: "?workspace=uat",
998955
assetFiles: [
999956
{
1000957
name: "uat-preview.png",
@@ -1023,17 +980,26 @@ test.describe("Asset Manager V2", () => {
1023980
});
1024981

1025982
try {
1026-
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded temporary UAT-only sample palette from \?workspace=UAT \(3 colors\)\./);
1027-
await expect(page.locator("#statusLog")).toHaveValue(/OK Temporary UAT-only Asset Manager V2 session context set to games\/Asteroids\/ with assetsPath games\/Asteroids\/assets\./);
983+
await expect(page.locator("#seedUatManifestButton")).toBeVisible();
984+
await page.locator("#seedUatManifestButton").click();
985+
await expect(page.locator("#activePaletteSummary")).toContainText("Workspace Manager V2 UAT Sample Palette has 3 active colors.");
986+
await expect(page.locator("#activeAssetRegistrySummary")).toHaveText("Schema-ready Asset Manager V2 manifest payload contains 0 managed assets.");
987+
await page.locator('[data-workspace-tool-id="asset-manager-v2"]').click();
988+
await expect(page).toHaveURL(/asset-manager-v2\/index\.html.*launch=workspace/);
989+
await expect(page).not.toHaveURL(/workspace=uat/i);
990+
await expect(page.locator("#statusLog")).toHaveValue(/OK Workspace Manager V2 loaded 0 validated assets from tools\.asset-manager-v2\.assets\./);
991+
await expect(page.locator("#statusLog")).toHaveValue(/OK Workspace Manager V2 loaded 3 palette colors from active palette context\./);
1028992
const uatContext = await page.evaluate(async () => {
1029-
const { readTemporaryUatWorkspaceContext } = await import("/tools/asset-manager-v2/js/services/TemporaryUatWorkspace.js");
1030-
const result = readTemporaryUatWorkspaceContext(window.location);
993+
const { WorkspaceBridge } = await import("/tools/asset-manager-v2/js/services/WorkspaceBridge.js");
994+
const bridge = new WorkspaceBridge({ windowRef: window });
995+
const contextResult = bridge.readContext();
996+
const previewContext = bridge.readWorkspacePreviewContext();
1031997
return {
1032-
assetsPath: result.assetsPath,
1033-
gameRoot: result.gameRoot,
1034-
ok: result.ok,
1035-
sourceId: result.palette?.sourceId,
1036-
swatchCount: result.palette?.swatches?.length || 0
998+
assetsPath: previewContext.workspaceAssetsPath,
999+
gameRoot: previewContext.workspaceGameRoot,
1000+
ok: contextResult.ok,
1001+
sourceId: contextResult.activePalette?.sourceId,
1002+
swatchCount: contextResult.activePalette?.swatches?.length || 0
10371003
};
10381004
});
10391005
expect(uatContext).toEqual({
@@ -1277,7 +1243,7 @@ test.describe("Asset Manager V2", () => {
12771243
});
12781244

12791245
try {
1280-
await expect(page.locator("#launchAssetManagerV2Button")).toBeDisabled();
1246+
await expect(page.locator("#workspaceToolTiles [data-workspace-tool-id]")).toHaveCount(3);
12811247
await page.locator("#activeGameSelect").selectOption("Asteroids");
12821248
await expect(page.locator("#workspaceContextOutput")).toContainText('"gameRoot": "games/Asteroids/"');
12831249
await expect(page.locator("#workspaceContextOutput")).toContainText('"assetsPath": "games/Asteroids/assets"');
@@ -1286,8 +1252,8 @@ test.describe("Asset Manager V2", () => {
12861252
await expect(page.locator("#workspaceContextOutput")).toContainText('"vector.asteroids.ship"');
12871253
await expect(page.locator("#workspaceContextOutput")).not.toContainText('"activePalette"');
12881254
await expect(page.locator("#workspaceContextOutput")).not.toContainText('"workspaceManifest"');
1289-
await expect(page.locator("#launchAssetManagerV2Button")).toBeEnabled();
1290-
await page.locator("#launchAssetManagerV2Button").click();
1255+
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
1256+
await page.locator('[data-workspace-tool-id="asset-manager-v2"]').click();
12911257
await expect(page).toHaveURL(/asset-manager-v2\/index\.html.*launch=workspace/);
12921258
await expect(page).toHaveURL(/fromTool=workspace-manager-v2/);
12931259
await expect(page).not.toHaveURL(/gameId=Asteroids/);
@@ -1462,10 +1428,10 @@ test.describe("Asset Manager V2", () => {
14621428
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html\?hostContextId=workspace-manager-v2-/);
14631429
await expect(page.locator("#activeGameSelect")).toHaveValue("Asteroids");
14641430
await expect(page.locator("#activeAssetRegistrySummary")).toHaveText("Schema-ready Asset Manager V2 manifest payload contains 17 managed assets.");
1465-
await expect(page.locator("#launchAssetManagerV2Button")).toBeEnabled();
1466-
await expect(page.locator("#saveWorkspaceManifestButton")).toBeEnabled();
1431+
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
1432+
await expect(page.locator("#exportManifestButton")).toBeEnabled();
14671433
const downloadPromise = page.waitForEvent("download");
1468-
await page.locator("#saveWorkspaceManifestButton").click();
1434+
await page.locator("#exportManifestButton").click();
14691435
const download = await downloadPromise;
14701436
expect(download.suggestedFilename()).toBe("workspace-manager-v2-Asteroids.workspace.manifest.json");
14711437
const savedManifest = JSON.parse(await readFile(await download.path(), "utf8"));

0 commit comments

Comments
 (0)