Skip to content

Commit a44b72f

Browse files
committed
PR_26174_ALFA_002-game-hub-project-intake-display
1 parent 07dab22 commit a44b72f

8 files changed

Lines changed: 110 additions & 211 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Branch Validation: PASS
2+
3+
- Stacked from `pr/26174-ALFA-001-idea-board-create-project-api-contract` as required.
4+
- Working branch: `pr/26174-ALFA-002-game-hub-project-intake-display`.
5+
- PR_001 draft branch is the base for this PR.
6+
- Worktree was clean before PR_002 edits.
7+
- Scope limited to Game Hub intake display validation and required reports.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Manual Validation Notes
2+
3+
PASS - Confirmed `Lantern Reef` appears in the Game Hub list after Idea Board conversion.
4+
PASS - Confirmed Game Hub source idea name, pitch, and notes render from the Local API-backed project.
5+
PASS - Confirmed source idea note context has no input, textarea, select, or button controls.
6+
PASS - Confirmed browser refresh keeps the created project active and source notes visible through Local API state.
7+
PASS - Confirmed no browser-owned project data store was introduced.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Requirement Checklist: PASS
2+
3+
PASS - Game Hub reads projects through API/service contract.
4+
PASS - Created project appears in Game Hub.
5+
PASS - Source idea notes display as read-only project context.
6+
PASS - No edit controls for source idea notes.
7+
PASS - Refresh preserves data through Local API.
8+
PASS - No browser-owned project data was added.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Validation Lane Report: PASS
2+
3+
Commands run:
4+
- `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
5+
6+
Result:
7+
- PASS: 3 tests passed.
8+
9+
Notes:
10+
- Targeted Playwright lane covers the Idea Board to Game Hub intake path.
11+
- Full samples smoke was not run by default per instruction.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# PR_26174_ALFA_002-game-hub-project-intake-display
2+
3+
## Purpose
4+
5+
Display Idea Board-created projects in Game Hub.
6+
7+
## Summary
8+
9+
- Added targeted stacked Playwright coverage proving an Idea Board-created project appears in Game Hub.
10+
- Verified Game Hub reads project state through Local API repository methods including `openGame` and `listGames`.
11+
- Verified source idea notes display as read-only project context with no edit controls.
12+
- Verified a page refresh preserves the created project and source notes through the Local API server state.
13+
14+
## Validation
15+
16+
PASS - `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
toolbox/idea-board/index.js
21
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
32
docs_build/dev/reports/codex_review.diff
43
docs_build/dev/reports/codex_changed_files.txt
5-
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract.md
6-
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-branch-validation.txt
7-
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-requirement-checklist.txt
8-
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-validation-lane.txt
9-
docs_build/dev/reports/PR_26174_ALFA_001-idea-board-create-project-api-contract-manual-validation-notes.txt
4+
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display.md
5+
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-branch-validation.txt
6+
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-requirement-checklist.txt
7+
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-validation-lane.txt
8+
docs_build/dev/reports/PR_26174_ALFA_002-game-hub-project-intake-display-manual-validation-notes.txt
Lines changed: 40 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,216 +1,51 @@
11
diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
2-
index 934faf46f..b2872b432 100644
2+
index b2872b432..1372df8be 100644
33
--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
44
+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
5-
@@ -1,6 +1,7 @@
6-
import { expect, test } from "@playwright/test";
7-
import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js";
8-
import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs";
9-
+import { createGameJourneyCompletionMetricsPostgresClientStub } from "../../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs";
10-
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
11-
12-
function restoreEnvValue(key, value) {
13-
@@ -112,7 +113,10 @@ async function expectNoNavigationFallbackUi(page) {
14-
}
15-
16-
test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
17-
- const server = await startRepoServer();
18-
+ const server = await startRepoServer({
19-
+ gameJourneyCompletionMetricsLegacyDbPath: null,
20-
+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(),
21-
+ });
22-
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
23-
const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
24-
const previousSupabaseEnv = {
25-
@@ -131,6 +135,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
26-
const pageErrors = [];
5+
@@ -136,6 +136,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
276
const consoleErrors = [];
287
const mutatingApiRequests = [];
29-
+ const createGamePayloads = [];
8+
const createGamePayloads = [];
9+
+ const gameHubRepositoryRequests = [];
3010

3111
page.on("response", (response) => {
3212
if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`);
33-
@@ -144,8 +149,12 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
34-
if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text());
35-
});
36-
page.on("request", (request) => {
37-
- if (request.url().includes("/api/") && request.method() !== "GET") {
38-
- mutatingApiRequests.push(`${request.method()} ${request.url()}`);
39-
+ const requestUrl = request.url();
40-
+ if (requestUrl.includes("/api/") && request.method() !== "GET") {
41-
+ mutatingApiRequests.push(`${request.method()} ${requestUrl}`);
42-
+ }
43-
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
44-
+ createGamePayloads.push(request.postDataJSON());
13+
@@ -156,6 +157,9 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
14+
if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
15+
createGamePayloads.push(request.postDataJSON());
4516
}
46-
});
47-
48-
@@ -228,6 +237,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
49-
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(2)).toHaveText("2026-06-20");
50-
await expect(page.locator("[data-idea-board-notes-count='top-thoughts']")).toHaveText("3 Notes");
51-
await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Delete"]);
52-
+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
53-
54-
await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] th")).toHaveText("Sky Orchard");
55-
await expectIdeaChevron(page, "sky-orchard", "gfs-chevron-down.svg");
56-
@@ -336,6 +346,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
57-
await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
58-
await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0);
59-
await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0);
60-
+ expect(createGamePayloads).toHaveLength(1);
61-
+ const [createGameInput] = createGamePayloads[0].args;
62-
+ expect(Object.keys(createGameInput).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]);
63-
+ expect(createGameInput).toMatchObject({
64-
+ name: "Lantern Reef",
65-
+ purpose: "Game",
66-
+ sourceIdea: {
67-
+ idea: "Lantern Reef",
68-
+ pitch: "Guide light through a reef that rearranges at dusk.",
69-
+ notes: ["Use dusk tide changes as the first Game Hub planning note."],
70-
+ },
71-
+ status: "Planning",
72-
+ });
73-
await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click();
74-
await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0);
75-
await page.locator("[data-idea-board-status-filter-option][value='Archived']").check();
76-
@@ -374,6 +397,78 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
77-
}
78-
});
79-
80-
+test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => {
81-
+ const server = await startRepoServer();
82-
+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
83-
+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
84-
+ const previousMetricsDbPath = process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH;
85-
+ const previousSupabaseEnv = {
86-
+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL,
87-
+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY,
88-
+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY,
89-
+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL,
90-
+ };
91-
+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`;
92-
+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl;
93-
+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board";
94-
+ process.env.GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH = `tmp/test-results/idea-board-${process.pid}-${Date.now()}.sqlite`;
95-
+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key";
96-
+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key";
97-
+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`;
98-
+ const createGameRequests = [];
99-
+
100-
+ page.on("request", (request) => {
101-
+ const requestUrl = request.url();
102-
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
103-
+ createGameRequests.push(requestUrl);
17+
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/")) {
18+
+ gameHubRepositoryRequests.push(`${request.method()} ${requestUrl}`);
10419
+ }
105-
+ });
106-
+
107-
+ try {
108-
+ await page.route("**/api/platform-settings/banner", async (route) => {
109-
+ await route.fulfill({
110-
+ contentType: "application/json",
111-
+ body: JSON.stringify({
112-
+ data: { banner: { active: false, message: "", tone: "info" } },
113-
+ ok: true,
114-
+ }),
115-
+ });
116-
+ });
117-
+ await page.route("**/api/toolbox/registry/snapshot", async (route) => {
118-
+ await route.fulfill({
119-
+ contentType: "application/json",
120-
+ body: JSON.stringify({
121-
+ data: {
122-
+ activeTools: [],
123-
+ readinessByStatus: {},
124-
+ tools: [],
125-
+ toolboxContract: {},
126-
+ },
127-
+ ok: true,
128-
+ }),
129-
+ });
130-
+ });
131-
+
132-
+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" });
133-
+ await page.locator("[data-idea-board-add-idea]").click();
134-
+ await page.locator("[data-idea-board-idea-input]").fill("Guest Reef");
135-
+ await page.locator("[data-idea-board-pitch-input]").fill("Guest cannot create authoritative project keys.");
136-
+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
137-
+ await page.locator("[data-idea-board-idea-action='save']").click();
138-
+ await expect(page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
139-
+
140-
+ await page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action='create-project']").click();
141-
+ await page.waitForURL(/\/account\/sign-in\.html$/);
142-
+ expect(createGameRequests).toEqual([]);
143-
+ } finally {
144-
+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl);
145-
+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl);
146-
+ restoreEnvValue("GAMEFOUNDRY_GAME_JOURNEY_METRICS_DB_PATH", previousMetricsDbPath);
147-
+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value));
148-
+ await server.close();
149-
+ }
150-
+});
151-
+
152-
test("Idea Board remains usable without visible navigation fallback when registry navigation is unavailable", async ({ page }) => {
153-
const server = await startRepoServer();
154-
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
155-
diff --git a/toolbox/idea-board/index.js b/toolbox/idea-board/index.js
156-
index d1e339336..0ae02bd7a 100644
157-
--- a/toolbox/idea-board/index.js
158-
+++ b/toolbox/idea-board/index.js
159-
@@ -1,9 +1,11 @@
160-
import { createServerRepositoryClient } from "../../src/api/server-api-client.js";
161-
+import { getSessionCurrent } from "../../src/api/session-api-client.js";
162-
163-
const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]);
164-
const defaultVisibleStatuses = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project"]);
165-
const userId = "user-1";
166-
const gameHubRoute = "toolbox/game-hub/index.html";
167-
+const signInRoute = "account/sign-in.html";
168-
let gameHubRepository = null;
169-
170-
const ideaTable = [
171-
@@ -561,6 +563,28 @@ function gameHubUrl(record) {
172-
return `${gameHubRoute}${suffix}`;
173-
}
20+
});
17421

175-
+function signInUrl() {
176-
+ return new URL(signInRoute, document.baseURI || window.location.href).href;
177-
+}
178-
+
179-
+function currentSessionState() {
180-
+ try {
181-
+ const session = getSessionCurrent();
182-
+ return {
183-
+ apiAvailable: true,
184-
+ authenticated: Boolean(session?.authenticated && session.userKey),
185-
+ session,
186-
+ };
187-
+ } catch (error) {
188-
+ console.warn("Idea Board could not verify the current session.", error instanceof Error ? error.message : String(error || ""));
189-
+ return {
190-
+ apiAvailable: false,
191-
+ authenticated: false,
192-
+ session: null,
193-
+ };
194-
+ }
195-
+}
196-
+
197-
function createProject(root, ideaId) {
198-
const record = ideaRecord(ideaId);
199-
if (!record) {
200-
@@ -571,6 +595,16 @@ function createProject(root, ideaId) {
201-
updateStatus(root, "Set this idea to Ready before creating a project.");
202-
return;
203-
}
204-
+ const sessionState = currentSessionState();
205-
+ if (!sessionState.apiAvailable) {
206-
+ updateStatus(root, "Sign-in status could not be verified. Try again shortly.");
207-
+ return;
208-
+ }
209-
+ if (!sessionState.authenticated) {
210-
+ updateStatus(root, "Sign in to create a Game Hub project.");
211-
+ window.location.href = signInUrl();
212-
+ return;
213-
+ }
214-
const repository = gameHubProjectRepository();
215-
const project = repository.createGame({
216-
name: record.idea,
22+
try {
23+
@@ -372,9 +376,19 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
24+
await page.waitForURL(/\/toolbox\/game-hub\/index\.html\?game=lantern-reef-\d+$/);
25+
await expect(page.getByRole("heading", { level: 1, name: "Game Hub" })).toBeVisible();
26+
await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef");
27+
+ await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef");
28+
+ await expect(page.locator("[data-source-idea-display]")).toHaveText("Lantern Reef");
29+
+ await expect(page.locator("[data-source-idea-pitch]")).toHaveText("Guide light through a reef that rearranges at dusk.");
30+
+ await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note.");
31+
+ await expect(page.locator("[data-source-idea-section] :is(input, textarea, select, button)")).toHaveCount(0);
32+
+ await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0);
33+
+ await page.reload({ waitUntil: "networkidle" });
34+
+ await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef");
35+
+ await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef");
36+
await expect(page.locator("[data-source-idea-display]")).toHaveText("Lantern Reef");
37+
await expect(page.locator("[data-source-idea-pitch]")).toHaveText("Guide light through a reef that rearranges at dusk.");
38+
await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note.");
39+
+ await expect(page.locator("[data-source-idea-section] :is(input, textarea, select, button)")).toHaveCount(0);
40+
await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0);
41+
await expect(page.locator("main")).not.toContainText(/\bproject records\b|\bAPI\b|\bDB\b|\bmock\b|\bseed\b|\bdebug\b|\binternal\b/i);
42+
await page.getByRole("link", { name: "Open Game Journey" }).click();
43+
@@ -386,6 +400,8 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
44+
45+
expect(mutatingApiRequests.some((request) => request.includes("/api/toolbox/game-hub/repositories"))).toBe(true);
46+
expect(mutatingApiRequests.some((request) => request.includes("/methods/createGame"))).toBe(true);
47+
+ expect(gameHubRepositoryRequests.some((request) => request.includes("/methods/openGame"))).toBe(true);
48+
+ expect(gameHubRepositoryRequests.some((request) => request.includes("/methods/listGames"))).toBe(true);
49+
expect(failedRequests).toEqual([]);
50+
expect(pageErrors).toEqual([]);
51+
expect(consoleErrors).toEqual([]);

0 commit comments

Comments
 (0)