Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions dev/tests/playwright/tools/SpritesToolShell.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { expect, test } from "@playwright/test";
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs";
import { getToolRegistrySnapshot } from "../../../../toolbox/toolRegistry.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..", "..", "..", "..");

function contentTypeForPath(filePath) {
const extension = path.extname(filePath).toLowerCase();
if (extension === ".html") return "text/html; charset=utf-8";
if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8";
if (extension === ".json") return "application/json";
if (extension === ".css") return "text/css; charset=utf-8";
if (extension === ".svg") return "image/svg+xml";
if (extension === ".png") return "image/png";
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
if (extension === ".webp") return "image/webp";
if (extension === ".woff2") return "font/woff2";
if (extension === ".woff") return "font/woff";
return "application/octet-stream";
}

function isInsideRepoRoot(absolutePath) {
const relativePath = path.relative(repoRoot, absolutePath);
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
}

function jsonResponse(response, payload) {
response.statusCode = 200;
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(payload));
}

async function startSpriteShellTestServer() {
const server = http.createServer(async (request, response) => {
try {
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
const requestOrigin = `http://${request.headers.host}`;

if (requestUrl.pathname === "/api/public/config") {
jsonResponse(response, {
data: {
publicConfig: {
apiUrl: `${requestOrigin}/api`,
environmentLabel: "Playwright",
siteUrl: requestOrigin,
},
},
ok: true,
});
return;
}

if (requestUrl.pathname === "/api/toolbox/registry/snapshot") {
jsonResponse(response, {
data: getToolRegistrySnapshot(),
ok: true,
});
return;
}

if (requestUrl.pathname === "/api/session/current") {
jsonResponse(response, {
data: {
authenticated: false,
session: null,
user: null,
},
ok: true,
});
return;
}

if (requestUrl.pathname === "/api/platform-settings/banner") {
jsonResponse(response, {
data: {
banner: null,
enabled: false,
},
ok: true,
});
return;
}

if (requestUrl.pathname === "/api/toolbox/game-hub/repositories") {
jsonResponse(response, {
data: {
repositoryId: "sprites-shell-playwright",
},
ok: true,
});
return;
}

if (/^\/api\/toolbox\/game-hub\/repositories\/[^/]+\/methods\//.test(requestUrl.pathname)) {
jsonResponse(response, {
data: {
result: null,
},
ok: true,
});
return;
}

if (requestUrl.pathname === "/api/game-journey/completion-metrics") {
jsonResponse(response, {
data: {
metrics: [],
},
ok: true,
});
return;
}

const decodedPath = decodeURIComponent(requestUrl.pathname);
const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, "");
const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`);
if (!isInsideRepoRoot(absolutePath)) {
response.statusCode = 403;
response.end("Forbidden");
return;
}

let targetPath = absolutePath;
const stat = await fs.stat(targetPath).catch(() => null);
if (stat && stat.isDirectory()) {
targetPath = path.join(targetPath, "index.html");
}

const responseContents = await fs.readFile(targetPath);
response.statusCode = 200;
response.setHeader("Content-Type", contentTypeForPath(targetPath));
response.end(responseContents);
} catch {
response.statusCode = 404;
response.end("Not Found");
}
});

await new Promise((resolve, reject) => {
server.listen(0, "127.0.0.1", () => resolve());
server.on("error", reject);
});

const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to start Sprite Creator shell test server.");
}

return {
baseUrl: `http://127.0.0.1:${address.port}`,
close: async () => {
await new Promise((resolve, reject) => {
const forceClose = setTimeout(() => {
server.closeAllConnections?.();
}, 250);
server.close((error) => {
clearTimeout(forceClose);
if (error) reject(error);
else resolve();
});
server.closeIdleConnections?.();
});
},
};
}

function collectPageFailures(page) {
const failedRequests = [];
const pageErrors = [];
const consoleErrors = [];

page.on("response", (response) => {
if (response.status() >= 400) {
failedRequests.push(`${response.status()} ${response.url()}`);
}
});
page.on("requestfailed", (request) => {
failedRequests.push(`FAILED ${request.url()}`);
});
page.on("pageerror", (error) => {
const text = error.stack || error.message;
if (!isBrowserExtensionNoise(text)) {
pageErrors.push(error.message);
}
});
page.on("console", (message) => {
if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) {
consoleErrors.push(message.text());
}
});

return { consoleErrors, failedRequests, pageErrors };
}

test("Sprite Creator shell loads with visible tool, canvas, details, and status regions", async ({ page }) => {
const server = await startSpriteShellTestServer();
const failures = collectPageFailures(page);

try {
await page.goto(`${server.baseUrl}/toolbox/sprites/index.html`, { waitUntil: "networkidle" });

await expect(page).toHaveTitle(/Sprite Creator - GameFoundryStudio/);
await expect(page.getByRole("heading", { level: 1, name: "Sprite Creator" })).toBeVisible();
await expect(page.locator("[data-sprites-tools-panel]")).toBeVisible();
await expect(page.locator("[data-sprites-work-area]")).toBeVisible();
await expect(page.locator("[data-sprites-details-panel]")).toBeVisible();
await expect(page.locator("[data-sprites-footer-status]")).toBeVisible();
await expect(page.locator("[data-sprites-tools-panel]")).toContainText("Sprite Tools");
await expect(page.getByText("Drawing Tools")).toBeVisible();
await expect(page.getByText("Canvas Setup")).toBeVisible();
await expect(page.getByRole("heading", { level: 2, name: "Pixel Work Area" })).toBeVisible();
await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible();
await expect(page.getByRole("button", { name: "Pencil" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Eraser" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Fill" })).toBeDisabled();
await expect(page.locator("[data-sprites-pixel-grid]")).toBeVisible();
await expect(page.locator("[data-sprites-shell-status]")).toContainText("Shell ready");
await expect(page.locator("main")).toContainText("Palette/Colors keys only");
await expect(page.locator("main")).not.toContainText(/Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation/i);
await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);

expect(failures.failedRequests).toEqual([]);
expect(failures.pageErrors).toEqual([]);
expect(failures.consoleErrors).toEqual([]);
} finally {
await server.close();
}
});
89 changes: 89 additions & 0 deletions docs_build/dev/reports/PR_26179_CHARLIE_022-sprites-tool-shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# PR_26179_CHARLIE_022-sprites-tool-shell

Team: CHARLIE
Date: 2026-06-28
Branch: PR_26179_CHARLIE_022-sprites-tool-shell
Scope: Sprite Creator shell only

## Summary

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.

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.

## Changed Files

- toolbox/sprites/index.html
- src/shared/toolbox/tool-metadata-inventory.js
- dev/tests/playwright/tools/SpritesToolShell.spec.mjs
- docs_build/dev/reports/PR_26179_CHARLIE_022-sprites-tool-shell.md
- docs_build/dev/reports/codex_changed_files.txt
- docs_build/dev/reports/codex_review.diff

## Branch Validation

PASS

- Current branch: `PR_26179_CHARLIE_022-sprites-tool-shell`
- Started from `main`
- Scope remained shell-only
- No `start_of_day` files changed
- No runtime/API/database schema files changed
- No browser-owned product data added

## Requirement Checklist

| Requirement | Status | Notes |
| --- | --- | --- |
| 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. |
| Do not merge/cherry-pick/copy stale PR #219-#228 code | PASS | Implemented shell directly from current main patterns. |
| Update `toolbox/sprites/index.html` placeholder | PASS | Replaced placeholder-only copy with Sprite Creator shell. |
| Use public Creator-facing language | PASS | Page title and copy use Sprite Creator / Sprites language. |
| Theme V2 classes first | PASS | Uses existing container, page-title, tool-workspace, tool-column, accordion, card, table, status, and mini-stat classes. |
| No inline styles/style blocks/script blocks/inline handlers | PASS | Static scan found no violations. |
| External JS only if needed | PASS | Only existing Theme V2 external scripts are used. |
| Add navigation only if required | PASS | No catalog route change required; metadata label updated for the existing Sprites route. |
| No browser-owned product data | PASS | Shell contains static placeholders only; no persistence or authored data arrays. |
| No Local DB schema/API endpoints/key generation | PASS | No database/API/runtime changes. |
| Left panel tools placeholder | PASS | Visible Sprite Tools region with Drawing Tools, Canvas Setup, and Palette Source. |
| Center pixel work area placeholder | PASS | Visible Pixel Work Area with static grid placeholder. |
| Right details/animation placeholder | PASS | Visible Sprite Details and Animation regions. |
| Footer/status area | PASS | Visible shell status card. |
| Targeted shell test | PASS | Added `SpritesToolShell.spec.mjs`. |

## Validation Lane Report

PASS with one environment note.

Commands run:

```text
node --check src/shared/toolbox/tool-metadata-inventory.js
node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs
git diff --check -- toolbox/sprites/index.html src/shared/toolbox/tool-metadata-inventory.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs
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
npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list
```

Results:

- Node syntax checks: PASS
- `git diff --check`: PASS
- Placeholder/inline-style scan: PASS, no matches
- Targeted Playwright: PASS, 1 test passed

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.

## Manual Validation Notes

1. Open `/toolbox/sprites/index.html`.
2. Confirm the page title reads `Sprite Creator`.
3. Confirm the left panel shows Sprite Tools, Drawing Tools, Canvas Setup, and Palette Source.
4. Confirm the center panel shows Pixel Work Area and the static Pixel Grid placeholder.
5. Confirm the right panel shows Sprite Details, Animation, and Readiness sections.
6. Confirm the status card says the shell is ready and that save/load/data contracts remain later scoped work.
7. Confirm no visible `Not implemented yet`, `future rebuild work`, `Static wireframe only`, or `Plan sprite creation` wording remains.

## ZIP Artifact

`tmp/PR_26179_CHARLIE_022-sprites-tool-shell_delta.zip`
6 changes: 6 additions & 0 deletions docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
toolbox/sprites/index.html
src/shared/toolbox/tool-metadata-inventory.js
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
docs_build/dev/reports/PR_26179_CHARLIE_022-sprites-tool-shell.md
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/codex_review.diff
Loading
Loading