Skip to content

Commit 08f9906

Browse files
committed
Add Sprite Creator shell
1 parent 98729af commit 08f9906

6 files changed

Lines changed: 1042 additions & 21 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { expect, test } from "@playwright/test";
2+
import fs from "node:fs/promises";
3+
import http from "node:http";
4+
import path from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs";
7+
import { getToolRegistrySnapshot } from "../../../../toolbox/toolRegistry.js";
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = path.dirname(__filename);
11+
const repoRoot = path.resolve(__dirname, "..", "..", "..", "..");
12+
13+
function contentTypeForPath(filePath) {
14+
const extension = path.extname(filePath).toLowerCase();
15+
if (extension === ".html") return "text/html; charset=utf-8";
16+
if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8";
17+
if (extension === ".json") return "application/json";
18+
if (extension === ".css") return "text/css; charset=utf-8";
19+
if (extension === ".svg") return "image/svg+xml";
20+
if (extension === ".png") return "image/png";
21+
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
22+
if (extension === ".webp") return "image/webp";
23+
if (extension === ".woff2") return "font/woff2";
24+
if (extension === ".woff") return "font/woff";
25+
return "application/octet-stream";
26+
}
27+
28+
function isInsideRepoRoot(absolutePath) {
29+
const relativePath = path.relative(repoRoot, absolutePath);
30+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
31+
}
32+
33+
function jsonResponse(response, payload) {
34+
response.statusCode = 200;
35+
response.setHeader("Content-Type", "application/json");
36+
response.end(JSON.stringify(payload));
37+
}
38+
39+
async function startSpriteShellTestServer() {
40+
const server = http.createServer(async (request, response) => {
41+
try {
42+
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
43+
const requestOrigin = `http://${request.headers.host}`;
44+
45+
if (requestUrl.pathname === "/api/public/config") {
46+
jsonResponse(response, {
47+
data: {
48+
publicConfig: {
49+
apiUrl: `${requestOrigin}/api`,
50+
environmentLabel: "Playwright",
51+
siteUrl: requestOrigin,
52+
},
53+
},
54+
ok: true,
55+
});
56+
return;
57+
}
58+
59+
if (requestUrl.pathname === "/api/toolbox/registry/snapshot") {
60+
jsonResponse(response, {
61+
data: getToolRegistrySnapshot(),
62+
ok: true,
63+
});
64+
return;
65+
}
66+
67+
if (requestUrl.pathname === "/api/session/current") {
68+
jsonResponse(response, {
69+
data: {
70+
authenticated: false,
71+
session: null,
72+
user: null,
73+
},
74+
ok: true,
75+
});
76+
return;
77+
}
78+
79+
if (requestUrl.pathname === "/api/platform-settings/banner") {
80+
jsonResponse(response, {
81+
data: {
82+
banner: null,
83+
enabled: false,
84+
},
85+
ok: true,
86+
});
87+
return;
88+
}
89+
90+
if (requestUrl.pathname === "/api/toolbox/game-hub/repositories") {
91+
jsonResponse(response, {
92+
data: {
93+
repositoryId: "sprites-shell-playwright",
94+
},
95+
ok: true,
96+
});
97+
return;
98+
}
99+
100+
if (/^\/api\/toolbox\/game-hub\/repositories\/[^/]+\/methods\//.test(requestUrl.pathname)) {
101+
jsonResponse(response, {
102+
data: {
103+
result: null,
104+
},
105+
ok: true,
106+
});
107+
return;
108+
}
109+
110+
if (requestUrl.pathname === "/api/game-journey/completion-metrics") {
111+
jsonResponse(response, {
112+
data: {
113+
metrics: [],
114+
},
115+
ok: true,
116+
});
117+
return;
118+
}
119+
120+
const decodedPath = decodeURIComponent(requestUrl.pathname);
121+
const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, "");
122+
const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`);
123+
if (!isInsideRepoRoot(absolutePath)) {
124+
response.statusCode = 403;
125+
response.end("Forbidden");
126+
return;
127+
}
128+
129+
let targetPath = absolutePath;
130+
const stat = await fs.stat(targetPath).catch(() => null);
131+
if (stat && stat.isDirectory()) {
132+
targetPath = path.join(targetPath, "index.html");
133+
}
134+
135+
const responseContents = await fs.readFile(targetPath);
136+
response.statusCode = 200;
137+
response.setHeader("Content-Type", contentTypeForPath(targetPath));
138+
response.end(responseContents);
139+
} catch {
140+
response.statusCode = 404;
141+
response.end("Not Found");
142+
}
143+
});
144+
145+
await new Promise((resolve, reject) => {
146+
server.listen(0, "127.0.0.1", () => resolve());
147+
server.on("error", reject);
148+
});
149+
150+
const address = server.address();
151+
if (!address || typeof address === "string") {
152+
throw new Error("Failed to start Sprite Creator shell test server.");
153+
}
154+
155+
return {
156+
baseUrl: `http://127.0.0.1:${address.port}`,
157+
close: async () => {
158+
await new Promise((resolve, reject) => {
159+
const forceClose = setTimeout(() => {
160+
server.closeAllConnections?.();
161+
}, 250);
162+
server.close((error) => {
163+
clearTimeout(forceClose);
164+
if (error) reject(error);
165+
else resolve();
166+
});
167+
server.closeIdleConnections?.();
168+
});
169+
},
170+
};
171+
}
172+
173+
function collectPageFailures(page) {
174+
const failedRequests = [];
175+
const pageErrors = [];
176+
const consoleErrors = [];
177+
178+
page.on("response", (response) => {
179+
if (response.status() >= 400) {
180+
failedRequests.push(`${response.status()} ${response.url()}`);
181+
}
182+
});
183+
page.on("requestfailed", (request) => {
184+
failedRequests.push(`FAILED ${request.url()}`);
185+
});
186+
page.on("pageerror", (error) => {
187+
const text = error.stack || error.message;
188+
if (!isBrowserExtensionNoise(text)) {
189+
pageErrors.push(error.message);
190+
}
191+
});
192+
page.on("console", (message) => {
193+
if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) {
194+
consoleErrors.push(message.text());
195+
}
196+
});
197+
198+
return { consoleErrors, failedRequests, pageErrors };
199+
}
200+
201+
test("Sprite Creator shell loads with visible tool, canvas, details, and status regions", async ({ page }) => {
202+
const server = await startSpriteShellTestServer();
203+
const failures = collectPageFailures(page);
204+
205+
try {
206+
await page.goto(`${server.baseUrl}/toolbox/sprites/index.html`, { waitUntil: "networkidle" });
207+
208+
await expect(page).toHaveTitle(/Sprite Creator - GameFoundryStudio/);
209+
await expect(page.getByRole("heading", { level: 1, name: "Sprite Creator" })).toBeVisible();
210+
await expect(page.locator("[data-sprites-tools-panel]")).toBeVisible();
211+
await expect(page.locator("[data-sprites-work-area]")).toBeVisible();
212+
await expect(page.locator("[data-sprites-details-panel]")).toBeVisible();
213+
await expect(page.locator("[data-sprites-footer-status]")).toBeVisible();
214+
await expect(page.locator("[data-sprites-tools-panel]")).toContainText("Sprite Tools");
215+
await expect(page.getByText("Drawing Tools")).toBeVisible();
216+
await expect(page.getByText("Canvas Setup")).toBeVisible();
217+
await expect(page.getByRole("heading", { level: 2, name: "Pixel Work Area" })).toBeVisible();
218+
await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible();
219+
await expect(page.getByRole("button", { name: "Pencil" })).toBeDisabled();
220+
await expect(page.getByRole("button", { name: "Eraser" })).toBeDisabled();
221+
await expect(page.getByRole("button", { name: "Fill" })).toBeDisabled();
222+
await expect(page.locator("[data-sprites-pixel-grid]")).toBeVisible();
223+
await expect(page.locator("[data-sprites-shell-status]")).toContainText("Shell ready");
224+
await expect(page.locator("main")).toContainText("Palette/Colors keys only");
225+
await expect(page.locator("main")).not.toContainText(/Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation/i);
226+
await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);
227+
228+
expect(failures.failedRequests).toEqual([]);
229+
expect(failures.pageErrors).toEqual([]);
230+
expect(failures.consoleErrors).toEqual([]);
231+
} finally {
232+
await server.close();
233+
}
234+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# PR_26179_CHARLIE_022-sprites-tool-shell
2+
3+
Team: CHARLIE
4+
Date: 2026-06-28
5+
Branch: PR_26179_CHARLIE_022-sprites-tool-shell
6+
Scope: Sprite Creator shell only
7+
8+
## Summary
9+
10+
Updated the existing Sprites placeholder page into a Theme V2-compliant Sprite Creator shell. The shell is public Creator-facing and provides the requested left tools panel, center pixel work area placeholder, right sprite details / animation placeholder, and status area.
11+
12+
The current repository did not contain `docs_build/tool-planning/sprites/`, and no local/remote/GitHub branch named `PR_26179_CHARLIE_021` was available during startup. This PR did not merge, cherry-pick, or copy stale PR #219-#228 implementation code.
13+
14+
## Changed Files
15+
16+
- toolbox/sprites/index.html
17+
- src/shared/toolbox/tool-metadata-inventory.js
18+
- dev/tests/playwright/tools/SpritesToolShell.spec.mjs
19+
- docs_build/dev/reports/PR_26179_CHARLIE_022-sprites-tool-shell.md
20+
- docs_build/dev/reports/codex_changed_files.txt
21+
- docs_build/dev/reports/codex_review.diff
22+
23+
## Branch Validation
24+
25+
PASS
26+
27+
- Current branch: `PR_26179_CHARLIE_022-sprites-tool-shell`
28+
- Started from `main`
29+
- Scope remained shell-only
30+
- No `start_of_day` files changed
31+
- No runtime/API/database schema files changed
32+
- No browser-owned product data added
33+
34+
## Requirement Checklist
35+
36+
| Requirement | Status | Notes |
37+
| --- | --- | --- |
38+
| Read `docs_build/tool-planning/sprites/` from PR_26179_CHARLIE_021 | WARN | Folder/branch was unavailable in local, remote, and GitHub searches; proceeded without stale code. |
39+
| Do not merge/cherry-pick/copy stale PR #219-#228 code | PASS | Implemented shell directly from current main patterns. |
40+
| Update `toolbox/sprites/index.html` placeholder | PASS | Replaced placeholder-only copy with Sprite Creator shell. |
41+
| Use public Creator-facing language | PASS | Page title and copy use Sprite Creator / Sprites language. |
42+
| Theme V2 classes first | PASS | Uses existing container, page-title, tool-workspace, tool-column, accordion, card, table, status, and mini-stat classes. |
43+
| No inline styles/style blocks/script blocks/inline handlers | PASS | Static scan found no violations. |
44+
| External JS only if needed | PASS | Only existing Theme V2 external scripts are used. |
45+
| Add navigation only if required | PASS | No catalog route change required; metadata label updated for the existing Sprites route. |
46+
| No browser-owned product data | PASS | Shell contains static placeholders only; no persistence or authored data arrays. |
47+
| No Local DB schema/API endpoints/key generation | PASS | No database/API/runtime changes. |
48+
| Left panel tools placeholder | PASS | Visible Sprite Tools region with Drawing Tools, Canvas Setup, and Palette Source. |
49+
| Center pixel work area placeholder | PASS | Visible Pixel Work Area with static grid placeholder. |
50+
| Right details/animation placeholder | PASS | Visible Sprite Details and Animation regions. |
51+
| Footer/status area | PASS | Visible shell status card. |
52+
| Targeted shell test | PASS | Added `SpritesToolShell.spec.mjs`. |
53+
54+
## Validation Lane Report
55+
56+
PASS with one environment note.
57+
58+
Commands run:
59+
60+
```text
61+
node --check src/shared/toolbox/tool-metadata-inventory.js
62+
node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs
63+
git diff --check -- toolbox/sprites/index.html src/shared/toolbox/tool-metadata-inventory.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs
64+
rg --pcre2 -n -i "Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation|<style|style=|<script(?![^>]+src=)|on(click|change|submit|input|load|error)=" toolbox/sprites/index.html
65+
npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list
66+
```
67+
68+
Results:
69+
70+
- Node syntax checks: PASS
71+
- `git diff --check`: PASS
72+
- Placeholder/inline-style scan: PASS, no matches
73+
- Targeted Playwright: PASS, 1 test passed
74+
75+
Environment note: `node_modules` was absent initially. `npm ci` was run using the committed lockfile so the declared `@playwright/test` dependency could execute the targeted test. No package metadata was changed.
76+
77+
## Manual Validation Notes
78+
79+
1. Open `/toolbox/sprites/index.html`.
80+
2. Confirm the page title reads `Sprite Creator`.
81+
3. Confirm the left panel shows Sprite Tools, Drawing Tools, Canvas Setup, and Palette Source.
82+
4. Confirm the center panel shows Pixel Work Area and the static Pixel Grid placeholder.
83+
5. Confirm the right panel shows Sprite Details, Animation, and Readiness sections.
84+
6. Confirm the status card says the shell is ready and that save/load/data contracts remain later scoped work.
85+
7. Confirm no visible `Not implemented yet`, `future rebuild work`, `Static wireframe only`, or `Plan sprite creation` wording remains.
86+
87+
## ZIP Artifact
88+
89+
`tmp/PR_26179_CHARLIE_022-sprites-tool-shell_delta.zip`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
toolbox/sprites/index.html
2+
src/shared/toolbox/tool-metadata-inventory.js
3+
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
4+
docs_build/dev/reports/PR_26179_CHARLIE_022-sprites-tool-shell.md
5+
docs_build/dev/reports/codex_changed_files.txt
6+
docs_build/dev/reports/codex_review.diff

0 commit comments

Comments
 (0)