|
1 | 1 | 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 |
3 | 3 | --- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs |
4 | 4 | +++ 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 }) => { |
27 | 6 | const consoleErrors = []; |
28 | 7 | const mutatingApiRequests = []; |
29 | | -+ const createGamePayloads = []; |
| 8 | + const createGamePayloads = []; |
| 9 | ++ const gameHubRepositoryRequests = []; |
30 | 10 |
|
31 | 11 | page.on("response", (response) => { |
32 | 12 | 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()); |
45 | 16 | } |
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}`); |
104 | 19 | + } |
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 | + }); |
174 | 21 |
|
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