From c2260521975e90f04681f556c7f8c47ca5a8d30a Mon Sep 17 00:00:00 2001 From: DavidQ Date: Mon, 22 Jun 2026 16:56:43 -0400 Subject: [PATCH] PR_26174_ALFA_006-game-hub-empty-and-error-states --- ...pty-and-error-states-branch-validation.txt | 6 + ...d-error-states-manual-validation-notes.txt | 6 + ...and-error-states-requirement-checklist.txt | 6 + ...empty-and-error-states-validation-lane.txt | 7 + ...LFA_006-game-hub-empty-and-error-states.md | 16 ++ .../dev/reports/codex_changed_files.txt | 13 +- docs_build/dev/reports/codex_review.diff | 247 ++++++++++-------- .../tools/GameHubMockRepository.spec.mjs | 83 ++++++ toolbox/game-hub/game-hub.js | 29 +- 9 files changed, 283 insertions(+), 130 deletions(-) create mode 100644 docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt create mode 100644 docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md diff --git a/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt new file mode 100644 index 000000000..4b34b81be --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt @@ -0,0 +1,6 @@ +Branch Validation: PASS + +PASS - Current branch: pr/26174-ALFA-006-game-hub-empty-and-error-states. +PASS - Stack base: pr/26174-ALFA-005-idea-project-validation-polish. +PASS - Changes are scoped to Game Hub empty/error UI, targeted tests, and required reports. +PASS - No merge to main performed. diff --git a/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt new file mode 100644 index 000000000..185d2dc4c --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt @@ -0,0 +1,6 @@ +Manual Validation Notes: PASS + +PASS - Confirmed no-project list renders a creator-safe empty state and no game rows. +PASS - Confirmed listGames failure renders a creator-safe unavailable state and no game rows. +PASS - Confirmed intentionally sensitive upstream error text is not visible in main Game Hub UI. +PASS - Confirmed unavailable state is explicit, not a silent empty-list fallback. diff --git a/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt new file mode 100644 index 000000000..30146b914 --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt @@ -0,0 +1,6 @@ +Requirement Checklist: PASS + +PASS - Added creator-safe empty state for no Game Hub projects. +PASS - Added creator-safe API unavailable message. +PASS - No server/internal technical error details are shown to creators. +PASS - No silent fallbacks were added; unavailable project lists render an explicit unavailable state. diff --git a/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt new file mode 100644 index 000000000..474111a3f --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt @@ -0,0 +1,7 @@ +Validation Lane: PASS + +Targeted Playwright impacted lane: +PASS - npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state" + +Notes: +- Full workspace smoke was not run; targeted impacted Playwright validation was used per request. diff --git a/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md new file mode 100644 index 000000000..05a692487 --- /dev/null +++ b/docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md @@ -0,0 +1,16 @@ +# PR_26174_ALFA_006-game-hub-empty-and-error-states + +## Purpose + +Add creator-safe empty and API-unavailable states for Game Hub projects. + +## Summary + +- Added a distinct Game Hub project-list empty state for no available projects. +- Added a distinct project-list unavailable state when the Local API/service contract cannot return projects. +- Kept creator-facing messages free of server, database, repository, stack, or secret details. +- Added targeted Playwright coverage for empty and unavailable project-list states. + +## Validation + +PASS - `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 16e9c93bf..32a1eda1b 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,8 +1,9 @@ -tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +toolbox/game-hub/game-hub.js +tests/playwright/tools/GameHubMockRepository.spec.mjs docs_build/dev/reports/codex_review.diff docs_build/dev/reports/codex_changed_files.txt -docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish.md -docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-branch-validation.txt -docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-requirement-checklist.txt -docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-validation-lane.txt -docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-manual-validation-notes.txt +docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md +docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt +docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt +docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt +docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index f9206fec3..41724e43a 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,128 +1,147 @@ -diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -index 5d0ccfaff..722595ae9 100644 ---- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -@@ -429,6 +429,123 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { +diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs +index 2d583e1d7..e11631269 100644 +--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs ++++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs +@@ -322,6 +322,89 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page } } }); -+test("Idea Board gates Create Project to Ready ideas and locks converted projects", async ({ page }) => { -+ const server = await startRepoServer({ -+ gameJourneyCompletionMetricsLegacyDbPath: null, -+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(), -+ }); -+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; -+ const previousSupabaseEnv = { -+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL, -+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY, -+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY, -+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL, -+ }; -+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; -+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; -+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board"; -+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`; -+ const createGameRequests = []; -+ const failedRequests = []; -+ const pageErrors = []; -+ const consoleErrors = []; -+ -+ page.on("request", (request) => { -+ const requestUrl = request.url(); -+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { -+ createGameRequests.push(request.postDataJSON()); -+ } -+ }); -+ page.on("response", (response) => { -+ if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.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()); -+ }); -+ -+ try { -+ await page.route("**/api/platform-settings/banner", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { banner: { active: false, message: "", tone: "info" } }, -+ ok: true, -+ }), -+ }); ++test("Game Hub shows a creator-safe empty state when no projects exist", async ({ page }) => { ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { ++ await route.fulfill({ ++ body: JSON.stringify({ ++ data: { result: null }, ++ ok: true, ++ rule: "Browser -> Server API -> Data Source", ++ }), ++ contentType: "application/json; charset=utf-8", ++ status: 200, + }); -+ await page.route("**/api/toolbox/registry/snapshot", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { -+ activeTools: [], -+ readinessByStatus: {}, -+ tools: [], -+ toolboxContract: {}, ++ }); ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => { ++ await route.fulfill({ ++ body: JSON.stringify({ ++ data: { ++ result: { ++ gameStatus: "No Game", ++ gameProgress: "No active game", ++ publishingProgress: "Not started", ++ currentFocus: "Create a game", ++ recommendedNextTool: "Game Hub", ++ progressChecklist: [], + }, -+ ok: true, -+ }), -+ }); ++ }, ++ ok: true, ++ rule: "Browser -> Server API -> Data Source", ++ }), ++ contentType: "application/json; charset=utf-8", ++ status: 200, + }); -+ await page.request.post(`${server.baseUrl}/api/session/user`, { -+ data: { userKey: MOCK_DB_KEYS.users.user1 }, ++ }); ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { ++ await route.fulfill({ ++ body: JSON.stringify({ ++ data: { result: [] }, ++ ok: true, ++ rule: "Browser -> Server API -> Data Source", ++ }), ++ contentType: "application/json; charset=utf-8", ++ status: 200, + }); ++ }); ++ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); + -+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); -+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-idea-row='clockwork-courier'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ -+ await page.locator("[data-idea-board-add-idea]").click(); -+ await page.locator("[data-idea-board-idea-input]").fill("Validation Reef"); -+ await page.locator("[data-idea-board-pitch-input]").fill("Verify project gating and read-only conversion."); -+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Refining"); -+ await page.locator("[data-idea-board-idea-action='save']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Refining"); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ expect(createGameRequests).toEqual([]); -+ -+ await page.locator("[data-idea-board-idea-cell='validation-reef']").click(); -+ await page.locator("[data-idea-board-add-note='validation-reef']").click(); -+ await page.locator("[data-idea-board-note-input]").fill("This note should become read-only project context."); -+ await page.locator("[data-idea-board-note-action='save']").click(); -+ await expect(page.locator("[data-idea-board-notes-count='validation-reef']")).toHaveText("1 Note"); -+ -+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']").click(); -+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); -+ await page.locator("[data-idea-board-idea-action='save']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Ready"); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); ++ try { ++ await expect(page.locator("[data-active-game-name]")).toHaveText("No game open"); ++ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Create a game to start building."); ++ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0); ++ await expect(page.locator("[data-game-hub-log]")).not.toContainText(/server|API|repository|database|stack|error/i); ++ await expectNoPageFailures(failures); ++ } finally { ++ await failures.server.close(); ++ } ++}); + -+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Project"); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Open in Game Hub", "Archive"]); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-add-note='validation-reef']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-notes-table='validation-reef'] [data-idea-board-note-action]")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-note-input-row]")).toHaveCount(0); -+ expect(createGameRequests).toHaveLength(1); -+ expect(Object.keys(createGameRequests[0].args[0]).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]); ++test("Game Hub shows a creator-safe unavailable state when project list API fails", async ({ page }) => { ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { ++ await route.fulfill({ ++ body: JSON.stringify({ ++ error: "postgres://service-role-secret@internal.example:5432/gamefoundry failed with stack trace", ++ ok: false, ++ rule: "Browser -> Server API -> Data Source", ++ }), ++ contentType: "application/json; charset=utf-8", ++ status: 503, ++ }); ++ }); ++ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); + -+ expect(failedRequests).toEqual([]); -+ expect(pageErrors).toEqual([]); -+ expect(consoleErrors).toEqual([]); ++ try { ++ expect(failures.failedRequests.some((request) => request.includes("503") && request.includes("/methods/listGames"))).toBe(true); ++ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."); ++ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0); ++ await expect(page.locator("[data-game-hub-log]")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."); ++ await expect(page.locator("main")).not.toContainText(/postgres|service-role-secret|internal\.example|stack trace|repository|database/i); ++ expect(failures.pageErrors).toEqual([]); ++ expect(failures.consoleErrors.filter((message) => !message.includes("status of 503"))).toEqual([]); + } finally { -+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); -+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); -+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value)); -+ await server.close(); ++ await failures.server.close(); + } +}); + - test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => { - const server = await startRepoServer(); - const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + test("Game Hub shows active-game errors without throwing", async ({ page }) => { + await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { + await route.fulfill({ +diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js +index d2260fe04..2d6f321e4 100644 +--- a/toolbox/game-hub/game-hub.js ++++ b/toolbox/game-hub/game-hub.js +@@ -217,6 +217,14 @@ function createGameButton(game, isActive) { + return button; + } + ++function createGameListStatus(message, state) { ++ const emptyState = document.createElement("p"); ++ emptyState.className = "status"; ++ emptyState.dataset.gameListStatus = state; ++ emptyState.textContent = message; ++ return emptyState; ++} ++ + function renderProjectInformation(activeGame, currentMember, progress) { + if (!elements.projectRecordsTable) { + return; +@@ -252,22 +260,23 @@ function renderGameList() { + const activeGame = normalizeActiveGame(repository.getActiveGame()); + const gameUserKey = currentGameUserKey(activeGame); + const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {}); +- const games = Array.isArray(listResult) ? listResult : []; +- if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) { +- setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly."); +- } + + elements.gameList.replaceChildren(); + +- if (games.length === 0) { +- const emptyState = document.createElement("p"); +- emptyState.className = "status"; +- emptyState.textContent = "No games. Create a game to continue."; +- elements.gameList.append(emptyState); ++ if (!Array.isArray(listResult)) { ++ const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."; ++ reportRepositoryError(listResult, "Game Hub projects"); ++ setStatusLog(message); ++ elements.gameList.append(createGameListStatus(message, "unavailable")); ++ return; ++ } ++ ++ if (listResult.length === 0) { ++ elements.gameList.append(createGameListStatus("No Game Hub projects yet. Create a game to start building.", "empty")); + return; + } + +- games.forEach((game) => { ++ listResult.forEach((game) => { + const row = document.createElement("article"); + row.className = "callout"; + row.dataset.gameRow = game.id; diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs index 2d583e1d7..e11631269 100644 --- a/tests/playwright/tools/GameHubMockRepository.spec.mjs +++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs @@ -322,6 +322,89 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page } } }); +test("Game Hub shows a creator-safe empty state when no projects exist", async ({ page }) => { + await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { + await route.fulfill({ + body: JSON.stringify({ + data: { result: null }, + ok: true, + rule: "Browser -> Server API -> Data Source", + }), + contentType: "application/json; charset=utf-8", + status: 200, + }); + }); + await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => { + await route.fulfill({ + body: JSON.stringify({ + data: { + result: { + gameStatus: "No Game", + gameProgress: "No active game", + publishingProgress: "Not started", + currentFocus: "Create a game", + recommendedNextTool: "Game Hub", + progressChecklist: [], + }, + }, + ok: true, + rule: "Browser -> Server API -> Data Source", + }), + contentType: "application/json; charset=utf-8", + status: 200, + }); + }); + await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { + await route.fulfill({ + body: JSON.stringify({ + data: { result: [] }, + ok: true, + rule: "Browser -> Server API -> Data Source", + }), + contentType: "application/json; charset=utf-8", + status: 200, + }); + }); + const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); + + try { + await expect(page.locator("[data-active-game-name]")).toHaveText("No game open"); + await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Create a game to start building."); + await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0); + await expect(page.locator("[data-game-hub-log]")).not.toContainText(/server|API|repository|database|stack|error/i); + await expectNoPageFailures(failures); + } finally { + await failures.server.close(); + } +}); + +test("Game Hub shows a creator-safe unavailable state when project list API fails", async ({ page }) => { + await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { + await route.fulfill({ + body: JSON.stringify({ + error: "postgres://service-role-secret@internal.example:5432/gamefoundry failed with stack trace", + ok: false, + rule: "Browser -> Server API -> Data Source", + }), + contentType: "application/json; charset=utf-8", + status: 503, + }); + }); + const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); + + try { + expect(failures.failedRequests.some((request) => request.includes("503") && request.includes("/methods/listGames"))).toBe(true); + await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."); + await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0); + await expect(page.locator("[data-game-hub-log]")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."); + await expect(page.locator("main")).not.toContainText(/postgres|service-role-secret|internal\.example|stack trace|repository|database/i); + expect(failures.pageErrors).toEqual([]); + expect(failures.consoleErrors.filter((message) => !message.includes("status of 503"))).toEqual([]); + } finally { + await failures.server.close(); + } +}); + test("Game Hub shows active-game errors without throwing", async ({ page }) => { await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { await route.fulfill({ diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js index d2260fe04..2d6f321e4 100644 --- a/toolbox/game-hub/game-hub.js +++ b/toolbox/game-hub/game-hub.js @@ -217,6 +217,14 @@ function createGameButton(game, isActive) { return button; } +function createGameListStatus(message, state) { + const emptyState = document.createElement("p"); + emptyState.className = "status"; + emptyState.dataset.gameListStatus = state; + emptyState.textContent = message; + return emptyState; +} + function renderProjectInformation(activeGame, currentMember, progress) { if (!elements.projectRecordsTable) { return; @@ -252,22 +260,23 @@ function renderGameList() { const activeGame = normalizeActiveGame(repository.getActiveGame()); const gameUserKey = currentGameUserKey(activeGame); const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {}); - const games = Array.isArray(listResult) ? listResult : []; - if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) { - setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly."); - } elements.gameList.replaceChildren(); - if (games.length === 0) { - const emptyState = document.createElement("p"); - emptyState.className = "status"; - emptyState.textContent = "No games. Create a game to continue."; - elements.gameList.append(emptyState); + if (!Array.isArray(listResult)) { + const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."; + reportRepositoryError(listResult, "Game Hub projects"); + setStatusLog(message); + elements.gameList.append(createGameListStatus(message, "unavailable")); + return; + } + + if (listResult.length === 0) { + elements.gameList.append(createGameListStatus("No Game Hub projects yet. Create a game to start building.", "empty")); return; } - games.forEach((game) => { + listResult.forEach((game) => { const row = document.createElement("article"); row.className = "callout"; row.dataset.gameRow = game.id;