Skip to content

Commit c226052

Browse files
committed
PR_26174_ALFA_006-game-hub-empty-and-error-states
1 parent 5cdd959 commit c226052

9 files changed

Lines changed: 283 additions & 130 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Branch Validation: PASS
2+
3+
PASS - Current branch: pr/26174-ALFA-006-game-hub-empty-and-error-states.
4+
PASS - Stack base: pr/26174-ALFA-005-idea-project-validation-polish.
5+
PASS - Changes are scoped to Game Hub empty/error UI, targeted tests, and required reports.
6+
PASS - No merge to main performed.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Manual Validation Notes: PASS
2+
3+
PASS - Confirmed no-project list renders a creator-safe empty state and no game rows.
4+
PASS - Confirmed listGames failure renders a creator-safe unavailable state and no game rows.
5+
PASS - Confirmed intentionally sensitive upstream error text is not visible in main Game Hub UI.
6+
PASS - Confirmed unavailable state is explicit, not a silent empty-list fallback.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Requirement Checklist: PASS
2+
3+
PASS - Added creator-safe empty state for no Game Hub projects.
4+
PASS - Added creator-safe API unavailable message.
5+
PASS - No server/internal technical error details are shown to creators.
6+
PASS - No silent fallbacks were added; unavailable project lists render an explicit unavailable state.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Validation Lane: PASS
2+
3+
Targeted Playwright impacted lane:
4+
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"
5+
6+
Notes:
7+
- Full workspace smoke was not run; targeted impacted Playwright validation was used per request.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# PR_26174_ALFA_006-game-hub-empty-and-error-states
2+
3+
## Purpose
4+
5+
Add creator-safe empty and API-unavailable states for Game Hub projects.
6+
7+
## Summary
8+
9+
- Added a distinct Game Hub project-list empty state for no available projects.
10+
- Added a distinct project-list unavailable state when the Local API/service contract cannot return projects.
11+
- Kept creator-facing messages free of server, database, repository, stack, or secret details.
12+
- Added targeted Playwright coverage for empty and unavailable project-list states.
13+
14+
## Validation
15+
16+
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"`
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
1+
toolbox/game-hub/game-hub.js
2+
tests/playwright/tools/GameHubMockRepository.spec.mjs
23
docs_build/dev/reports/codex_review.diff
34
docs_build/dev/reports/codex_changed_files.txt
4-
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish.md
5-
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-branch-validation.txt
6-
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-requirement-checklist.txt
7-
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-validation-lane.txt
8-
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-manual-validation-notes.txt
5+
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md
6+
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt
7+
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt
8+
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt
9+
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt
Lines changed: 133 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,147 @@
1-
diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
2-
index 5d0ccfaff..722595ae9 100644
3-
--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
4-
+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
5-
@@ -429,6 +429,123 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
1+
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
2+
index 2d583e1d7..e11631269 100644
3+
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
4+
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
5+
@@ -322,6 +322,89 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }
66
}
77
});
88

9-
+test("Idea Board gates Create Project to Ready ideas and locks converted projects", async ({ page }) => {
10-
+ const server = await startRepoServer({
11-
+ gameJourneyCompletionMetricsLegacyDbPath: null,
12-
+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(),
13-
+ });
14-
+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
15-
+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
16-
+ const previousSupabaseEnv = {
17-
+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL,
18-
+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY,
19-
+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY,
20-
+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL,
21-
+ };
22-
+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`;
23-
+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl;
24-
+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board";
25-
+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key";
26-
+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key";
27-
+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`;
28-
+ const createGameRequests = [];
29-
+ const failedRequests = [];
30-
+ const pageErrors = [];
31-
+ const consoleErrors = [];
32-
+
33-
+ page.on("request", (request) => {
34-
+ const requestUrl = request.url();
35-
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
36-
+ createGameRequests.push(request.postDataJSON());
37-
+ }
38-
+ });
39-
+ page.on("response", (response) => {
40-
+ if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`);
41-
+ });
42-
+ page.on("pageerror", (error) => {
43-
+ const text = error.stack || error.message;
44-
+ if (!isBrowserExtensionNoise(text)) pageErrors.push(error.message);
45-
+ });
46-
+ page.on("console", (message) => {
47-
+ if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text());
48-
+ });
49-
+
50-
+ try {
51-
+ await page.route("**/api/platform-settings/banner", async (route) => {
52-
+ await route.fulfill({
53-
+ contentType: "application/json",
54-
+ body: JSON.stringify({
55-
+ data: { banner: { active: false, message: "", tone: "info" } },
56-
+ ok: true,
57-
+ }),
58-
+ });
9+
+test("Game Hub shows a creator-safe empty state when no projects exist", async ({ page }) => {
10+
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
11+
+ await route.fulfill({
12+
+ body: JSON.stringify({
13+
+ data: { result: null },
14+
+ ok: true,
15+
+ rule: "Browser -> Server API -> Data Source",
16+
+ }),
17+
+ contentType: "application/json; charset=utf-8",
18+
+ status: 200,
5919
+ });
60-
+ await page.route("**/api/toolbox/registry/snapshot", async (route) => {
61-
+ await route.fulfill({
62-
+ contentType: "application/json",
63-
+ body: JSON.stringify({
64-
+ data: {
65-
+ activeTools: [],
66-
+ readinessByStatus: {},
67-
+ tools: [],
68-
+ toolboxContract: {},
20+
+ });
21+
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => {
22+
+ await route.fulfill({
23+
+ body: JSON.stringify({
24+
+ data: {
25+
+ result: {
26+
+ gameStatus: "No Game",
27+
+ gameProgress: "No active game",
28+
+ publishingProgress: "Not started",
29+
+ currentFocus: "Create a game",
30+
+ recommendedNextTool: "Game Hub",
31+
+ progressChecklist: [],
6932
+ },
70-
+ ok: true,
71-
+ }),
72-
+ });
33+
+ },
34+
+ ok: true,
35+
+ rule: "Browser -> Server API -> Data Source",
36+
+ }),
37+
+ contentType: "application/json; charset=utf-8",
38+
+ status: 200,
7339
+ });
74-
+ await page.request.post(`${server.baseUrl}/api/session/user`, {
75-
+ data: { userKey: MOCK_DB_KEYS.users.user1 },
40+
+ });
41+
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => {
42+
+ await route.fulfill({
43+
+ body: JSON.stringify({
44+
+ data: { result: [] },
45+
+ ok: true,
46+
+ rule: "Browser -> Server API -> Data Source",
47+
+ }),
48+
+ contentType: "application/json; charset=utf-8",
49+
+ status: 200,
7650
+ });
51+
+ });
52+
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() });
7753
+
78-
+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" });
79-
+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
80-
+ await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
81-
+ await expect(page.locator("[data-idea-board-idea-row='clockwork-courier'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
82-
+
83-
+ await page.locator("[data-idea-board-add-idea]").click();
84-
+ await page.locator("[data-idea-board-idea-input]").fill("Validation Reef");
85-
+ await page.locator("[data-idea-board-pitch-input]").fill("Verify project gating and read-only conversion.");
86-
+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Refining");
87-
+ await page.locator("[data-idea-board-idea-action='save']").click();
88-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Refining");
89-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
90-
+ expect(createGameRequests).toEqual([]);
91-
+
92-
+ await page.locator("[data-idea-board-idea-cell='validation-reef']").click();
93-
+ await page.locator("[data-idea-board-add-note='validation-reef']").click();
94-
+ await page.locator("[data-idea-board-note-input]").fill("This note should become read-only project context.");
95-
+ await page.locator("[data-idea-board-note-action='save']").click();
96-
+ await expect(page.locator("[data-idea-board-notes-count='validation-reef']")).toHaveText("1 Note");
97-
+
98-
+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']").click();
99-
+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
100-
+ await page.locator("[data-idea-board-idea-action='save']").click();
101-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Ready");
102-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
54+
+ try {
55+
+ await expect(page.locator("[data-active-game-name]")).toHaveText("No game open");
56+
+ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Create a game to start building.");
57+
+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
58+
+ await expect(page.locator("[data-game-hub-log]")).not.toContainText(/server|API|repository|database|stack|error/i);
59+
+ await expectNoPageFailures(failures);
60+
+ } finally {
61+
+ await failures.server.close();
62+
+ }
63+
+});
10364
+
104-
+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']").click();
105-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Project");
106-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Open in Game Hub", "Archive"]);
107-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']")).toHaveCount(0);
108-
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
109-
+ await expect(page.locator("[data-idea-board-add-note='validation-reef']")).toHaveCount(0);
110-
+ await expect(page.locator("[data-idea-board-notes-table='validation-reef'] [data-idea-board-note-action]")).toHaveCount(0);
111-
+ await expect(page.locator("[data-idea-board-note-input-row]")).toHaveCount(0);
112-
+ expect(createGameRequests).toHaveLength(1);
113-
+ expect(Object.keys(createGameRequests[0].args[0]).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]);
65+
+test("Game Hub shows a creator-safe unavailable state when project list API fails", async ({ page }) => {
66+
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => {
67+
+ await route.fulfill({
68+
+ body: JSON.stringify({
69+
+ error: "postgres://service-role-secret@internal.example:5432/gamefoundry failed with stack trace",
70+
+ ok: false,
71+
+ rule: "Browser -> Server API -> Data Source",
72+
+ }),
73+
+ contentType: "application/json; charset=utf-8",
74+
+ status: 503,
75+
+ });
76+
+ });
77+
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() });
11478
+
115-
+ expect(failedRequests).toEqual([]);
116-
+ expect(pageErrors).toEqual([]);
117-
+ expect(consoleErrors).toEqual([]);
79+
+ try {
80+
+ expect(failures.failedRequests.some((request) => request.includes("503") && request.includes("/methods/listGames"))).toBe(true);
81+
+ 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.");
82+
+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
83+
+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.");
84+
+ await expect(page.locator("main")).not.toContainText(/postgres|service-role-secret|internal\.example|stack trace|repository|database/i);
85+
+ expect(failures.pageErrors).toEqual([]);
86+
+ expect(failures.consoleErrors.filter((message) => !message.includes("status of 503"))).toEqual([]);
11887
+ } finally {
119-
+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl);
120-
+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl);
121-
+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value));
122-
+ await server.close();
88+
+ await failures.server.close();
12389
+ }
12490
+});
12591
+
126-
test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => {
127-
const server = await startRepoServer();
128-
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
92+
test("Game Hub shows active-game errors without throwing", async ({ page }) => {
93+
await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
94+
await route.fulfill({
95+
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
96+
index d2260fe04..2d6f321e4 100644
97+
--- a/toolbox/game-hub/game-hub.js
98+
+++ b/toolbox/game-hub/game-hub.js
99+
@@ -217,6 +217,14 @@ function createGameButton(game, isActive) {
100+
return button;
101+
}
102+
103+
+function createGameListStatus(message, state) {
104+
+ const emptyState = document.createElement("p");
105+
+ emptyState.className = "status";
106+
+ emptyState.dataset.gameListStatus = state;
107+
+ emptyState.textContent = message;
108+
+ return emptyState;
109+
+}
110+
+
111+
function renderProjectInformation(activeGame, currentMember, progress) {
112+
if (!elements.projectRecordsTable) {
113+
return;
114+
@@ -252,22 +260,23 @@ function renderGameList() {
115+
const activeGame = normalizeActiveGame(repository.getActiveGame());
116+
const gameUserKey = currentGameUserKey(activeGame);
117+
const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {});
118+
- const games = Array.isArray(listResult) ? listResult : [];
119+
- if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) {
120+
- setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly.");
121+
- }
122+
123+
elements.gameList.replaceChildren();
124+
125+
- if (games.length === 0) {
126+
- const emptyState = document.createElement("p");
127+
- emptyState.className = "status";
128+
- emptyState.textContent = "No games. Create a game to continue.";
129+
- elements.gameList.append(emptyState);
130+
+ if (!Array.isArray(listResult)) {
131+
+ const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.";
132+
+ reportRepositoryError(listResult, "Game Hub projects");
133+
+ setStatusLog(message);
134+
+ elements.gameList.append(createGameListStatus(message, "unavailable"));
135+
+ return;
136+
+ }
137+
+
138+
+ if (listResult.length === 0) {
139+
+ elements.gameList.append(createGameListStatus("No Game Hub projects yet. Create a game to start building.", "empty"));
140+
return;
141+
}
142+
143+
- games.forEach((game) => {
144+
+ listResult.forEach((game) => {
145+
const row = document.createElement("article");
146+
row.className = "callout";
147+
row.dataset.gameRow = game.id;

0 commit comments

Comments
 (0)