From e5d71a88c62f645ba67c38db879b16daa0927d46 Mon Sep 17 00:00:00 2001 From: DavidQ Date: Tue, 23 Jun 2026 18:39:43 -0400 Subject: [PATCH] ALFA 006 game hub create project validation --- docs_build/dev/BUILD_PR.md | 56 +- ...me-hub-create-project-validation_report.md | 24 + ...oject-validation_requirements-checklist.md | 18 + ...eate-project-validation_validation-lane.md | 18 + .../dev/reports/codex_changed_files.txt | 11 +- docs_build/dev/reports/codex_review.diff | 731 ++++++------------ .../tools/GameHubMockRepository.spec.mjs | 16 +- toolbox/game-hub/game-hub.js | 27 +- 8 files changed, 350 insertions(+), 551 deletions(-) create mode 100644 docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_report.md create mode 100644 docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_requirements-checklist.md create mode 100644 docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_validation-lane.md diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md index 65c4d9709..cf7378880 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md @@ -1,59 +1,59 @@ -# PR_26175_ALFA_002-toolbox-status-bar-context-polish +# PR_26175_ALFA_006-game-hub-create-project-validation ## Purpose -Polish the shared toolbox status bar context display so it shows only selected-game name/purpose on the left and categorized tool context in the center. +Add creator-facing validation for the Game Hub create-project row so blank game names do not silently create fallback projects. ## Source Of Truth -This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_002-toolbox-status-bar-context-polish`. +This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_006-game-hub-create-project-validation`. ## Exact Scope -- Do not include environment text in the status bar because environment already appears in the platform banner. -- On the left side, display the selected Game Hub game name and selected Game Hub game purpose. -- On the center side, display tool context messages for tool actions, save state, validation messages, warnings, or errors. -- Preserve normal placement above the footer. -- Preserve fullscreen/tool display mode bottom anchoring. -- Preserve Idea Board selected-game filtering exclusion. -- Preserve Game Hub as selected-game owner through the existing repository contract. -- Keep the shared Theme V2 toolbox component model. -- Update targeted Playwright coverage for the polished left and center context. +- Validate the Game Hub add-game row before calling the repository create method. +- Block signed-in creator saves when the game name is blank or whitespace-only. +- Keep the add-game row open after validation failure. +- Show a creator-safe validation message in the existing Game Hub status log. +- Mark the game name input invalid for accessibility. +- Preserve valid create/open/delete behavior. +- Preserve guest save redirect behavior. +- Preserve API/service/repository contracts. +- Add targeted Playwright coverage for the create validation path. ## Exact Targets - `docs_build/dev/BUILD_PR.md` -- `assets/theme-v2/js/toolbox-status-bar.js` -- `assets/theme-v2/css/status.css` -- `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` -- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_report.md` -- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_validation-lane.md` -- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_requirements-checklist.md` +- `toolbox/game-hub/game-hub.js` +- `tests/playwright/tools/GameHubMockRepository.spec.mjs` +- `docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_report.md` +- `docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_validation-lane.md` +- `docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_requirements-checklist.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope -- No environment status in the toolbox status bar. -- No row highlights. -- No large banners. -- No modal-style status messages. +- No repository/API/service contract changes. +- No Game Journey completion-metrics changes. +- No shared toolbox status bar changes. +- No unrelated Game Hub workflow changes. +- No browser-owned product data as source of truth. +- No silent create-name fallback in the Game Hub page flow. - No inline styles, style blocks, or page-local CSS. -- No API/service contract changes. - No engine core changes. - No `start_of_day` folder changes. ## Validation -Run: +Run targeted create-project validation: ```powershell -npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1 +npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs --workers=1 --grep "Game Hub creates, opens, and deletes mock games" ``` -Also verify the changed source does not introduce inline styles or style blocks: +Also verify changed source does not introduce inline styles or style blocks: ```powershell -rg -n " String(value || "").trim()) -- .filter(Boolean) -- .join(" - "); -+function classifyToolContext(messageText, state, required) { -+ const text = String(messageText || "").trim(); -+ if (state === "error") { -+ return { kind: "error", label: "Error" }; -+ } -+ if (required && state === "missing") { -+ return { kind: "action", label: "Tool Action" }; -+ } -+ if (/\b(error|failed|malformed|unavailable|could not)\b/i.test(text)) { -+ return { kind: "error", label: "Error" }; -+ } -+ if (/\b(sign in|refresh|try again|temporarily|blocked)\b/i.test(text)) { -+ return { kind: "warning", label: "Warning" }; -+ } -+ if (/\b(validation|requirement|requirements|missing|required|open or seed)\b/i.test(text)) { -+ return { kind: "validation", label: "Validation" }; -+ } -+ if (/\b(saved|created|deleted|updated|loaded|save changes)\b/i.test(text)) { -+ return { kind: "save", label: "Save State" }; -+ } -+ return { kind: "action", label: "Tool Action" }; - } - - function renderSelectedGame(bar, selectedGame, state, messageText) { - const required = pageRequiresSelectedGame(); - const name = bar.querySelector("[data-toolbox-selected-game-name]"); -- const meta = bar.querySelector("[data-toolbox-selected-game-meta]"); -+ const purpose = bar.querySelector("[data-toolbox-selected-game-purpose]"); -+ const contextType = bar.querySelector("[data-toolbox-status-context-type]"); - const message = bar.querySelector("[data-toolbox-status-message]"); - const action = bar.querySelector("[data-toolbox-status-action]"); -+ const nextMessage = messageText || latestToolMessage || (selectedGame -+ ? `Tool context is filtered to ${selectedGame.name}.` -+ : required -+ ? "Select or create a game in Game Hub before using this toolbox page." -+ : "Idea Board can capture ideas before a Game Hub game exists."); -+ const context = classifyToolContext(nextMessage, state, required); - - bar.dataset.selectedGameState = state; - bar.dataset.selectedGameRequired = String(required); -+ bar.dataset.toolboxStatusContextKind = context.kind; -+ contextType.textContent = context.label; - action.hidden = false; - action.href = mountOptions.gameHubHref; - - if (selectedGame) { - name.textContent = selectedGame.name; -- meta.textContent = selectedGameMeta(selectedGame) || "Game Hub selected game"; -- message.textContent = messageText || latestToolMessage || `Data filtered to ${selectedGame.name}.`; -+ purpose.textContent = selectedGame.purpose || "Game"; -+ message.textContent = nextMessage; - action.textContent = "Open Game Hub"; - return; - } - - if (!required) { -- name.textContent = "Optional"; -- meta.textContent = "Idea Board can start before Game Hub creation"; -- message.textContent = latestToolMessage || "Idea Board can capture ideas before a Game Hub game exists."; -+ name.textContent = "No game selected"; -+ purpose.textContent = "Idea Board optional"; -+ message.textContent = nextMessage; - action.textContent = "Open Game Hub"; - return; - } - - if (state === "error") { - name.textContent = "Unavailable"; -- meta.textContent = "Game Hub selected game could not be read"; -- message.textContent = messageText || "Game Hub selected game is unavailable. Refresh or restore the Local API."; -+ purpose.textContent = "Game Hub selected game could not be read"; -+ message.textContent = nextMessage; - action.textContent = "Open Game Hub"; - return; - } - - name.textContent = "No game selected"; -- meta.textContent = "Game Hub owns game selection"; -+ purpose.textContent = "Game Hub owns game selection"; - message.textContent = "Select or create a game in Game Hub before using this toolbox page."; - action.textContent = "Select or Create in Game Hub"; - } -@@ -284,6 +329,9 @@ function installEventListeners() { - return; - } - listenersInstalled = true; -+ document.addEventListener("click", scheduleToolMessageRefresh, true); -+ document.addEventListener("submit", scheduleToolMessageRefresh, true); -+ document.addEventListener("change", scheduleToolMessageRefresh, true); - window.addEventListener("gamefoundry:toolbox-selected-game-changed", refreshToolboxStatusBar); - window.addEventListener("gamefoundry:data-changed", refreshToolboxStatusBar); - } diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -index 51d6abdeb..65c4d9709 100644 +index 65c4d9709..cf7378880 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md -@@ -1,41 +1,42 @@ --# PR_26175_ALFA_001-toolbox-selected-game-status-bar -+# PR_26175_ALFA_002-toolbox-status-bar-context-polish +@@ -1,59 +1,59 @@ +-# PR_26175_ALFA_002-toolbox-status-bar-context-polish ++# PR_26175_ALFA_006-game-hub-create-project-validation ## Purpose --Add one shared Theme V2 toolbox status bar that surfaces the Game Hub selected game and current tool messages across toolbox pages. -+Polish the shared toolbox status bar context display so it shows only selected-game name/purpose on the left and categorized tool context in the center. +-Polish the shared toolbox status bar context display so it shows only selected-game name/purpose on the left and categorized tool context in the center. ++Add creator-facing validation for the Game Hub create-project row so blank game names do not silently create fallback projects. ## Source Of Truth --This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_001-toolbox-selected-game-status-bar`. -+This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_002-toolbox-status-bar-context-polish`. +-This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_002-toolbox-status-bar-context-polish`. ++This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_006-game-hub-create-project-validation`. ## Exact Scope --- Add a shared toolbox status bar renderer loaded through Theme V2 shared partial bootstrapping. --- Render the status bar above the footer in normal page mode. --- Anchor the status bar to the viewport bottom while `body.tool-focus-mode` is active. --- Read the selected game only from the Game Hub repository through the existing Local API/service contract. --- Display the selected Game Hub game on the left side of the status bar. --- Display tool actions, warnings, errors, save state, validation messages, or the missing-game prompt in the center of the status bar. --- Expose the selected Game Hub game as derived page context for toolbox pages without persisting browser-owned product data. --- Require selected-game context on toolbox pages except Idea Board, which remains excluded because ideas can exist before game creation. --- Show a creator-safe prompt to select or create a game in Game Hub when no selected game exists. --- Notify the shared status bar when Game Hub changes the selected game. --- Add targeted Playwright coverage for placement, fullscreen anchoring, selected-game display/update, missing-game prompt, and Idea Board exclusion. -+- Do not include environment text in the status bar because environment already appears in the platform banner. -+- On the left side, display the selected Game Hub game name and selected Game Hub game purpose. -+- On the center side, display tool context messages for tool actions, save state, validation messages, warnings, or errors. -+- Preserve normal placement above the footer. -+- Preserve fullscreen/tool display mode bottom anchoring. -+- Preserve Idea Board selected-game filtering exclusion. -+- Preserve Game Hub as selected-game owner through the existing repository contract. -+- Keep the shared Theme V2 toolbox component model. -+- Update targeted Playwright coverage for the polished left and center context. +-- Do not include environment text in the status bar because environment already appears in the platform banner. +-- On the left side, display the selected Game Hub game name and selected Game Hub game purpose. +-- On the center side, display tool context messages for tool actions, save state, validation messages, warnings, or errors. +-- Preserve normal placement above the footer. +-- Preserve fullscreen/tool display mode bottom anchoring. +-- Preserve Idea Board selected-game filtering exclusion. +-- Preserve Game Hub as selected-game owner through the existing repository contract. +-- Keep the shared Theme V2 toolbox component model. +-- Update targeted Playwright coverage for the polished left and center context. ++- Validate the Game Hub add-game row before calling the repository create method. ++- Block signed-in creator saves when the game name is blank or whitespace-only. ++- Keep the add-game row open after validation failure. ++- Show a creator-safe validation message in the existing Game Hub status log. ++- Mark the game name input invalid for accessibility. ++- Preserve valid create/open/delete behavior. ++- Preserve guest save redirect behavior. ++- Preserve API/service/repository contracts. ++- Add targeted Playwright coverage for the create validation path. ## Exact Targets --- `assets/theme-v2/js/gamefoundry-partials.js` -+- `docs_build/dev/BUILD_PR.md` - - `assets/theme-v2/js/toolbox-status-bar.js` - - `assets/theme-v2/css/status.css` --- `toolbox/game-hub/game-hub.js` - - `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` --- `docs_build/dev/reports/PR_26175_ALFA_001-toolbox-selected-game-status-bar_report.md` --- `docs_build/dev/reports/PR_26175_ALFA_001-toolbox-selected-game-status-bar_validation-lane.md` --- `docs_build/dev/reports/PR_26175_ALFA_001-toolbox-selected-game-status-bar_requirements-checklist.md` -+- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_report.md` -+- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_validation-lane.md` -+- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_requirements-checklist.md` -+- `docs_build/dev/reports/codex_review.diff` -+- `docs_build/dev/reports/codex_changed_files.txt` + - `docs_build/dev/BUILD_PR.md` +-- `assets/theme-v2/js/toolbox-status-bar.js` +-- `assets/theme-v2/css/status.css` +-- `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` +-- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_report.md` +-- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_validation-lane.md` +-- `docs_build/dev/reports/PR_26175_ALFA_002-toolbox-status-bar-context-polish_requirements-checklist.md` ++- `toolbox/game-hub/game-hub.js` ++- `tests/playwright/tools/GameHubMockRepository.spec.mjs` ++- `docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_report.md` ++- `docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_validation-lane.md` ++- `docs_build/dev/reports/PR_26175_ALFA_006-game-hub-create-project-validation_requirements-checklist.md` + - `docs_build/dev/reports/codex_review.diff` + - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope -+- No environment status in the toolbox status bar. -+- No row highlights. -+- No large banners. -+- No modal-style status messages. -+- No inline styles, style blocks, or page-local CSS. -+- No API/service contract changes. +-- No environment status in the toolbox status bar. +-- No row highlights. +-- No large banners. +-- No modal-style status messages. ++- No repository/API/service contract changes. ++- No Game Journey completion-metrics changes. ++- No shared toolbox status bar changes. ++- No unrelated Game Hub workflow changes. ++- No browser-owned product data as source of truth. ++- No silent create-name fallback in the Game Hub page flow. + - No inline styles, style blocks, or page-local CSS. +-- No API/service contract changes. - No engine core changes. - No `start_of_day` folder changes. --- No API/service contract changes. --- No page-local CSS, inline styles, or style blocks. --- No browser storage or browser-owned product data as selected-game source of truth. --- No Idea Board selected-game filtering. ## Validation - Run: -@@ -47,12 +48,12 @@ npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs - Also verify the changed source does not introduce inline styles or style blocks: +-Run: ++Run targeted create-project validation: ```powershell --rg -n " { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { -+ authenticated: true, -+ displayName: options.session.displayName || "User 1", -+ roleSlugs: options.session.roleSlugs || ["creator"], -+ userKey, -+ }, -+ ok: true, -+ }), -+ }); -+ }); -+ await page.request.post(`${server.baseUrl}/api/session/user`, { -+ data: { userKey }, -+ }); + docs_build/dev/reports/codex_review.diff +-tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs +diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs +index 6f6d046d2..5bae39718 100644 +--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs ++++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs +@@ -254,7 +254,7 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled(); + await expect(page.getByLabel("Game Name")).toHaveCount(0); + await expect(page.getByLabel("Game Purpose")).toHaveCount(0); +- await expect(page.getByLabel("Game Status")).toHaveCount(0); ++ await expect(page.locator("input[aria-label='Game Status'], textarea[aria-label='Game Status'], select[aria-label='Game Status']")).toHaveCount(0); + await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveClass("btn"); + await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeEnabled(); + await expect(page.locator("summary").filter({ hasText: /^Game Setup$/ })).toHaveCount(0); +@@ -370,7 +370,19 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + await expect(addGameRow.getByRole("button", { name: "Save" })).toHaveClass("btn btn--compact primary"); + await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass("btn btn--compact primary"); + await expect(addGameRow.locator("td")).toHaveCount(3); +- await addGameRow.getByLabel("Game").fill("Launch Test Game"); ++ const addGameNameInput = addGameRow.getByLabel("Game"); ++ await expect(addGameNameInput).toHaveAttribute("required", ""); ++ await addGameRow.getByRole("button", { name: "Save" }).click(); ++ await expect(addGameNameInput).toHaveAttribute("aria-invalid", "true"); ++ await expect(addGameNameInput).toBeFocused(); ++ await expect(page.locator("[data-game-hub-log]")).toHaveText("Enter a game name before saving."); ++ await expect(page.locator("[data-game-list]")).not.toContainText("Untitled Game"); ++ await addGameNameInput.fill(" "); ++ await addGameRow.getByRole("button", { name: "Save" }).click(); ++ await expect(addGameNameInput).toHaveAttribute("aria-invalid", "true"); ++ await expect(page.locator("[data-game-hub-log]")).toHaveText("Enter a game name before saving."); ++ await expect(page.locator("[data-game-list]")).not.toContainText("Untitled Game"); ++ await addGameNameInput.fill("Launch Test Game"); + await addGameRow.getByLabel("Purpose").selectOption("Learning Game"); + await addGameRow.getByLabel("Status").selectOption("Ready for Testing"); + await addGameRow.getByRole("button", { name: "Save" }).click(); +diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js +index b59da1c43..50bbe46f1 100644 +--- a/toolbox/game-hub/game-hub.js ++++ b/toolbox/game-hub/game-hub.js +@@ -268,6 +268,9 @@ function createInput(value, datasetName, ariaLabel, options = {}) { + input.value = value || ""; + input.placeholder = options.placeholder || ""; + input.setAttribute("aria-label", ariaLabel); ++ if (options.required) { ++ input.required = true; + } + if (options.readOnly) { + input.readOnly = true; + } +@@ -439,6 +442,7 @@ function renderAddGameRow(tbody) { + nameCell.scope = "row"; + nameCell.append(createInput("", "gameNameInput", "Game", { + placeholder: "Untitled game", ++ required: true, + })); - await workspaceV2CoverageReporter.start(page); - await page.goto(`${server.baseUrl}${pathName}`, { waitUntil: "networkidle" }); -@@ -84,6 +105,14 @@ function expectNoPageFailures(failures) { - expect(failures.consoleErrors).toEqual([]); + const purposeCell = document.createElement("td"); +@@ -674,11 +678,32 @@ function readGameRowFields(row) { + }; } -+function creatorSession() { -+ return { -+ displayName: "User 1", -+ roleSlugs: ["creator"], -+ userKey: MOCK_DB_KEYS.users.user1, -+ }; ++function validateAddedGameFields(row) { ++ const input = readGameRowFields(row); ++ const nameInput = row?.querySelector("[data-game-name-input]"); ++ input.name = String(input.name || "").trim(); ++ if (!input.name) { ++ if (nameInput) { ++ nameInput.setAttribute("aria-invalid", "true"); ++ nameInput.focus(); ++ } ++ setStatusLog("Enter a game name before saving."); ++ return null; ++ } ++ if (nameInput) { ++ nameInput.removeAttribute("aria-invalid"); ++ } ++ return input; +} + - async function statusBarSnapshot(page) { - return page.locator("[data-toolbox-status-bar]").evaluate((bar) => { - const footer = document.querySelector("footer.footer"); -@@ -111,8 +140,13 @@ test("shared toolbox status bar shows selected Game Hub game above the footer", - const statusBar = page.locator("[data-toolbox-status-bar]"); - await expect(statusBar).toBeVisible(); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); -+ await expect(statusBar).not.toContainText("Environment"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name-label]")).toHaveText("Selected Game Name"); - await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("Demo Game"); -- await expect(statusBar.locator("[data-toolbox-selected-game-meta]")).toHaveText("Game - Under Construction"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose-label]")).toHaveText("Selected Game Purpose"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Game"); -+ await expect(statusBar.locator("[data-toolbox-selected-game]")).not.toContainText("Under Construction"); -+ await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveText("Tool Action"); - await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Game Design mock repository ready."); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-id", "demo-game"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "active"); -@@ -130,6 +164,29 @@ test("shared toolbox status bar shows selected Game Hub game above the footer", + function saveAddedGame(row) { + if (!ensureProjectRecordsSaveAllowedForSave()) { + return; } - }); - -+test("shared toolbox status bar center reports save state after Game Hub saves", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); -+ -+ try { -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ const addGameRow = page.locator("[data-game-add-row='input']"); -+ await addGameRow.getByLabel("Game").fill("Status Bar Save"); -+ await addGameRow.getByLabel("Purpose").selectOption("Learning Game"); -+ await addGameRow.getByLabel("Status").selectOption("Ready for Testing"); -+ await addGameRow.getByRole("button", { name: "Save" }).click(); -+ -+ await expect(page.locator("[data-toolbox-status-context-type]")).toHaveText("Save State"); -+ await expect(page.locator("[data-toolbox-status-message]")).toHaveText("Created and opened Status Bar Save."); -+ await expect(page.locator("[data-toolbox-selected-game-name]")).toHaveText("Status Bar Save"); -+ await expect(page.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Learning Game"); -+ await expect(page.locator("[data-toolbox-status-bar]")).not.toContainText("Environment"); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); +- const input = readGameRowFields(row); ++ const input = validateAddedGameFields(row); ++ if (!input) { ++ return; + } -+}); -+ - test("shared toolbox status bar anchors to the bottom in tool display mode", async ({ page }) => { - const failures = await openRepoPage(page, "/toolbox/game-design/index.html"); - -@@ -156,9 +213,11 @@ test("Game Hub owner selection updates the global toolbox status bar", async ({ - await expect(page.locator("[data-toolbox-selected-game-name]")).toHaveText("Demo Game"); - await page.locator("[data-game-toggle='gravity-demo']").click(); - await expect(page.locator("[data-toolbox-selected-game-name]")).toHaveText("Gravity Demo"); -- await expect(page.locator("[data-toolbox-selected-game-meta]")).toHaveText("Capability Demo - Wireframe"); -+ await expect(page.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Capability Demo"); -+ await expect(page.locator("[data-toolbox-selected-game]")).not.toContainText("Wireframe"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-id", "gravity-demo"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "active"); -+ await expect(page.locator("[data-toolbox-status-context-type]")).toHaveText("Warning"); - await expect(page.locator("[data-toolbox-status-message]")).toContainText("Sign in to create or update Game Hub projects."); - - expectNoPageFailures(failures); -@@ -173,6 +232,8 @@ test("non-Idea Board toolbox pages show a creator-safe prompt when no Game Hub g - try { - const statusBar = page.locator("[data-toolbox-status-bar]"); - await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("No game selected"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Game Hub owns game selection"); -+ await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveText("Tool Action"); - await expect(statusBar.locator("[data-toolbox-status-message]")).toHaveText("Select or create a game in Game Hub before using this toolbox page."); - await expect(statusBar.locator("[data-toolbox-status-action]")).toHaveText("Select or Create in Game Hub"); - await expect(statusBar.locator("[data-toolbox-status-action]")).toHaveAttribute("href", /toolbox\/game-hub\/index\.html$/); -@@ -195,8 +256,9 @@ test("Idea Board is excluded from selected-game filtering and does not show the - try { - const statusBar = page.locator("[data-toolbox-status-bar]"); - await expect(statusBar).toBeVisible(); -- await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("Optional"); -- await expect(statusBar.locator("[data-toolbox-selected-game-meta]")).toHaveText("Idea Board can start before Game Hub creation"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("No game selected"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Idea Board optional"); -+ await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveText("Tool Action"); - await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Ready to shape ideas and notes."); - await expect(statusBar.locator("[data-toolbox-status-message]")).not.toContainText("Select or create a game"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "optional"); + const game = repository.createGame({ + name: input.name, + purpose: input.purpose, diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs index 6f6d046d2..5bae39718 100644 --- a/tests/playwright/tools/GameHubMockRepository.spec.mjs +++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs @@ -254,7 +254,7 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled(); await expect(page.getByLabel("Game Name")).toHaveCount(0); await expect(page.getByLabel("Game Purpose")).toHaveCount(0); - await expect(page.getByLabel("Game Status")).toHaveCount(0); + await expect(page.locator("input[aria-label='Game Status'], textarea[aria-label='Game Status'], select[aria-label='Game Status']")).toHaveCount(0); await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveClass("btn"); await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeEnabled(); await expect(page.locator("summary").filter({ hasText: /^Game Setup$/ })).toHaveCount(0); @@ -370,7 +370,19 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { await expect(addGameRow.getByRole("button", { name: "Save" })).toHaveClass("btn btn--compact primary"); await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass("btn btn--compact primary"); await expect(addGameRow.locator("td")).toHaveCount(3); - await addGameRow.getByLabel("Game").fill("Launch Test Game"); + const addGameNameInput = addGameRow.getByLabel("Game"); + await expect(addGameNameInput).toHaveAttribute("required", ""); + await addGameRow.getByRole("button", { name: "Save" }).click(); + await expect(addGameNameInput).toHaveAttribute("aria-invalid", "true"); + await expect(addGameNameInput).toBeFocused(); + await expect(page.locator("[data-game-hub-log]")).toHaveText("Enter a game name before saving."); + await expect(page.locator("[data-game-list]")).not.toContainText("Untitled Game"); + await addGameNameInput.fill(" "); + await addGameRow.getByRole("button", { name: "Save" }).click(); + await expect(addGameNameInput).toHaveAttribute("aria-invalid", "true"); + await expect(page.locator("[data-game-hub-log]")).toHaveText("Enter a game name before saving."); + await expect(page.locator("[data-game-list]")).not.toContainText("Untitled Game"); + await addGameNameInput.fill("Launch Test Game"); await addGameRow.getByLabel("Purpose").selectOption("Learning Game"); await addGameRow.getByLabel("Status").selectOption("Ready for Testing"); await addGameRow.getByRole("button", { name: "Save" }).click(); diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js index b59da1c43..50bbe46f1 100644 --- a/toolbox/game-hub/game-hub.js +++ b/toolbox/game-hub/game-hub.js @@ -268,6 +268,9 @@ function createInput(value, datasetName, ariaLabel, options = {}) { input.value = value || ""; input.placeholder = options.placeholder || ""; input.setAttribute("aria-label", ariaLabel); + if (options.required) { + input.required = true; + } if (options.readOnly) { input.readOnly = true; } @@ -439,6 +442,7 @@ function renderAddGameRow(tbody) { nameCell.scope = "row"; nameCell.append(createInput("", "gameNameInput", "Game", { placeholder: "Untitled game", + required: true, })); const purposeCell = document.createElement("td"); @@ -674,11 +678,32 @@ function readGameRowFields(row) { }; } +function validateAddedGameFields(row) { + const input = readGameRowFields(row); + const nameInput = row?.querySelector("[data-game-name-input]"); + input.name = String(input.name || "").trim(); + if (!input.name) { + if (nameInput) { + nameInput.setAttribute("aria-invalid", "true"); + nameInput.focus(); + } + setStatusLog("Enter a game name before saving."); + return null; + } + if (nameInput) { + nameInput.removeAttribute("aria-invalid"); + } + return input; +} + function saveAddedGame(row) { if (!ensureProjectRecordsSaveAllowedForSave()) { return; } - const input = readGameRowFields(row); + const input = validateAddedGameFields(row); + if (!input) { + return; + } const game = repository.createGame({ name: input.name, purpose: input.purpose,