|
| 1 | +import { test, expect } from "@playwright/test"; |
| 2 | +import fs from "node:fs/promises"; |
| 3 | +import path from "node:path"; |
| 4 | +import http from "node:http"; |
| 5 | +import { fileURLToPath } from "node:url"; |
| 6 | + |
| 7 | +const __filename = fileURLToPath(import.meta.url); |
| 8 | +const __dirname = path.dirname(__filename); |
| 9 | +const repoRoot = path.resolve(__dirname, "..", ".."); |
| 10 | + |
| 11 | +function contentTypeForPath(filePath) { |
| 12 | + const extension = path.extname(filePath).toLowerCase(); |
| 13 | + if (extension === ".html") return "text/html; charset=utf-8"; |
| 14 | + if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8"; |
| 15 | + if (extension === ".json") return "application/json; charset=utf-8"; |
| 16 | + if (extension === ".css") return "text/css; charset=utf-8"; |
| 17 | + if (extension === ".svg") return "image/svg+xml"; |
| 18 | + return "application/octet-stream"; |
| 19 | +} |
| 20 | + |
| 21 | +async function startRepoServer() { |
| 22 | + const server = http.createServer(async (request, response) => { |
| 23 | + try { |
| 24 | + const requestUrl = new URL(request.url || "/", "http://127.0.0.1"); |
| 25 | + const decodedPath = decodeURIComponent(requestUrl.pathname); |
| 26 | + const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, ""); |
| 27 | + const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`); |
| 28 | + if (!absolutePath.startsWith(repoRoot)) { |
| 29 | + response.statusCode = 403; |
| 30 | + response.end("Forbidden"); |
| 31 | + return; |
| 32 | + } |
| 33 | + let targetPath = absolutePath; |
| 34 | + const stat = await fs.stat(targetPath).catch(() => null); |
| 35 | + if (stat && stat.isDirectory()) { |
| 36 | + targetPath = path.join(targetPath, "index.html"); |
| 37 | + } |
| 38 | + const fileContents = await fs.readFile(targetPath); |
| 39 | + response.statusCode = 200; |
| 40 | + response.setHeader("Content-Type", contentTypeForPath(targetPath)); |
| 41 | + response.end(fileContents); |
| 42 | + } catch { |
| 43 | + response.statusCode = 404; |
| 44 | + response.end("Not Found"); |
| 45 | + } |
| 46 | + }); |
| 47 | + await new Promise((resolve, reject) => { |
| 48 | + server.listen(0, "127.0.0.1", () => resolve()); |
| 49 | + server.on("error", reject); |
| 50 | + }); |
| 51 | + const address = server.address(); |
| 52 | + if (!address || typeof address === "string") { |
| 53 | + throw new Error("Failed to start UI test server."); |
| 54 | + } |
| 55 | + return { |
| 56 | + baseUrl: `http://127.0.0.1:${address.port}`, |
| 57 | + close: async () => { |
| 58 | + await new Promise((resolve, reject) => { |
| 59 | + server.close((error) => { |
| 60 | + if (error) reject(error); |
| 61 | + else resolve(); |
| 62 | + }); |
| 63 | + }); |
| 64 | + } |
| 65 | + }; |
| 66 | +} |
| 67 | + |
| 68 | +test("workspace v2 launches asset manager and add/remove is reflected in export", async ({ page }) => { |
| 69 | + const server = await startRepoServer(); |
| 70 | + try { |
| 71 | + await page.goto(`${server.baseUrl}/tools/workspace-v2/index.html`); |
| 72 | + |
| 73 | + await page.getByRole("button", { name: "Full Reset" }).click(); |
| 74 | + await page.locator("#workspaceV2ToolSelect").selectOption("asset-manager-v2"); |
| 75 | + await page.getByRole("button", { name: "Load Fixture" }).click(); |
| 76 | + await page.getByRole("button", { name: "Create Session + Launch" }).click(); |
| 77 | + |
| 78 | + await expect(page).toHaveURL(/\/tools\/asset-manager-v2\/index\.html/); |
| 79 | + await expect(page).toHaveTitle("Asset Manager V2"); |
| 80 | + await expect(page.getByRole("button", { name: /Player Ship/ })).toBeVisible(); |
| 81 | + |
| 82 | + await page.locator("#assetManagerV2AddId").fill("asset-002"); |
| 83 | + await page.locator("#assetManagerV2AddLabel").fill("Enemy Ship"); |
| 84 | + await page.locator("#assetManagerV2AddKind").fill("svg"); |
| 85 | + await page.locator("#assetManagerV2AddPath").fill("assets/vectors/enemy-ship.svg"); |
| 86 | + await page.getByRole("button", { name: "Add Asset" }).click(); |
| 87 | + |
| 88 | + await expect(page.getByRole("button", { name: /Enemy Ship/ })).toBeVisible(); |
| 89 | + await page.getByRole("button", { name: "Remove asset-002" }).click(); |
| 90 | + await expect(page.getByRole("button", { name: /Enemy Ship/ })).toHaveCount(0); |
| 91 | + |
| 92 | + await page.getByRole("button", { name: /Back to Workspace V2/ }).click(); |
| 93 | + await expect(page).toHaveURL(/\/tools\/workspace-v2\/index\.html/); |
| 94 | + |
| 95 | + const downloadPromise = page.waitForEvent("download"); |
| 96 | + await page.getByRole("button", { name: "Export Workspace Session JSON" }).click(); |
| 97 | + const download = await downloadPromise; |
| 98 | + const downloadPath = await download.path(); |
| 99 | + if (!downloadPath) { |
| 100 | + throw new Error("Workspace export did not produce a downloadable file."); |
| 101 | + } |
| 102 | + const exportedJsonText = await fs.readFile(downloadPath, "utf8"); |
| 103 | + const exported = JSON.parse(exportedJsonText); |
| 104 | + const entries = exported?.tools?.["workspace-v2"]?.activeSession?.payloadJson?.assetCatalog?.entries; |
| 105 | + if (!Array.isArray(entries)) { |
| 106 | + throw new Error("Exported manifest is missing tools.workspace-v2.activeSession.payloadJson.assetCatalog.entries."); |
| 107 | + } |
| 108 | + expect(entries.some((entry) => entry?.id === "asset-001")).toBe(true); |
| 109 | + expect(entries.some((entry) => entry?.id === "asset-002")).toBe(false); |
| 110 | + } finally { |
| 111 | + await server.close(); |
| 112 | + } |
| 113 | +}); |
0 commit comments