|
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 } |
| 1 | +diff --git a/tests/playwright/tools/GameJourneyTool.spec.mjs b/tests/playwright/tools/GameJourneyTool.spec.mjs |
| 2 | +index e9005f08b..586e7521f 100644 |
| 3 | +--- a/tests/playwright/tools/GameJourneyTool.spec.mjs |
| 4 | ++++ b/tests/playwright/tools/GameJourneyTool.spec.mjs |
| 5 | +@@ -69,7 +69,6 @@ async function openRepoPage(page, pathName, options = {}) { |
| 6 | + page.on("requestfailed", (request) => { |
| 7 | + failedRequests.push(`FAILED ${request.url()}`); |
| 8 | + }); |
| 9 | +- |
| 10 | + if (collectCoverage) { |
| 11 | + await workspaceV2CoverageReporter.start(page); |
6 | 12 | } |
7 | | - }); |
| 13 | +@@ -258,6 +257,7 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p |
| 14 | + const failedRequests = []; |
| 15 | + const pageErrors = []; |
| 16 | + const consoleErrors = []; |
| 17 | ++ const recommendedTargetRequests = []; |
8 | 18 |
|
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, |
19 | | -+ }); |
| 19 | + page.on("pageerror", (error) => { |
| 20 | + pageErrors.push(error.message); |
| 21 | +@@ -275,6 +275,12 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p |
| 22 | + page.on("requestfailed", (request) => { |
| 23 | + failedRequests.push(`FAILED ${request.url()}`); |
| 24 | + }); |
| 25 | ++ page.on("request", (request) => { |
| 26 | ++ const requestUrl = request.url(); |
| 27 | ++ if (requestUrl.includes("/api/toolbox/game-journey/repositories/") && requestUrl.includes("/methods/updateRecommendedTarget")) { |
| 28 | ++ recommendedTargetRequests.push(request.postDataJSON()); |
| 29 | ++ } |
20 | 30 | + }); |
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: [], |
32 | | -+ }, |
33 | | -+ }, |
34 | | -+ ok: true, |
35 | | -+ rule: "Browser -> Server API -> Data Source", |
36 | | -+ }), |
37 | | -+ contentType: "application/json; charset=utf-8", |
38 | | -+ status: 200, |
39 | | -+ }); |
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, |
50 | | -+ }); |
51 | | -+ }); |
52 | | -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); |
53 | | -+ |
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 | | -+}); |
64 | | -+ |
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() }); |
78 | | -+ |
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([]); |
87 | | -+ } finally { |
88 | | -+ await failures.server.close(); |
89 | | -+ } |
90 | | -+}); |
91 | | -+ |
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 | 31 |
|
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; |
| 32 | + try { |
| 33 | + await workspaceV2CoverageReporter.start(page); |
| 34 | +@@ -332,12 +338,23 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p |
| 35 | + "Next action: mark one finished section item complete so overall progress can rise above 0%.", |
| 36 | + ]); |
| 37 | + await expect(page.locator("[data-journey-recommended-target]")).toHaveCount(5); |
| 38 | ++ const recommendedTargetOrder = await page.locator("[data-journey-recommended-target]").evaluateAll((rows) => ( |
| 39 | ++ rows.map((row) => row.dataset.journeyRecommendedTarget) |
| 40 | ++ )); |
| 41 | ++ expect(recommendedTargetOrder).toEqual([ |
| 42 | ++ "hero", |
| 43 | ++ "enemy", |
| 44 | ++ "boss", |
| 45 | ++ "background", |
| 46 | ++ "music", |
| 47 | ++ ]); |
| 48 | ++ await expect(page.locator("[data-journey-recommended-targets] th")).toHaveText(["Target", "Section", "Count"]); |
| 49 | + await expect(page.locator("[data-journey-recommended-target] td:first-child")).toHaveText([ |
| 50 | +- "Hero", |
| 51 | +- "Enemy", |
| 52 | +- "Boss", |
| 53 | +- "Background", |
| 54 | +- "Music", |
| 55 | ++ "Hero [1]", |
| 56 | ++ "Enemy [4]", |
| 57 | ++ "Boss [1]", |
| 58 | ++ "Background [3]", |
| 59 | ++ "Music [5]", |
| 60 | + ]); |
| 61 | + await expect(page.locator("[data-journey-recommended-target='hero'] td").nth(1)).toHaveText("Objects"); |
| 62 | + await expect(page.locator("[data-journey-recommended-target='enemy'] td").nth(1)).toHaveText("Objects"); |
| 63 | +@@ -349,11 +366,19 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p |
| 64 | + await expect(page.locator("[data-journey-target-input='boss']")).toHaveValue("1"); |
| 65 | + await expect(page.locator("[data-journey-target-input='background']")).toHaveValue("3"); |
| 66 | + await expect(page.locator("[data-journey-target-input='music']")).toHaveValue("5"); |
| 67 | ++ await expect(page.locator("[data-journey-recommended-target] input[type='checkbox']")).toHaveCount(0); |
| 68 | ++ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("type", "number"); |
| 69 | ++ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("inputmode", "numeric"); |
| 70 | ++ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("aria-label", "Hero count"); |
| 71 | + await page.locator("[data-journey-target-input='hero']").fill("2"); |
| 72 | + await expect(page.locator("[data-journey-target-status]")).toHaveText("Saved Hero target at 2."); |
| 73 | + await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("2"); |
| 74 | ++ await expect(page.locator("[data-journey-target-count-preview='hero']")).toHaveText(" [2]"); |
| 75 | ++ expect(recommendedTargetRequests).toHaveLength(1); |
| 76 | ++ expect(recommendedTargetRequests[0].args).toEqual(["hero", 2]); |
| 77 | + await page.reload({ waitUntil: "networkidle" }); |
| 78 | + await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("2"); |
| 79 | ++ await expect(page.locator("[data-journey-target-count-preview='hero']")).toHaveText(" [2]"); |
| 80 | + const repositoryData = await fetchApiData(server, "/api/toolbox/game-journey/repositories", { |
| 81 | + body: JSON.stringify({ options: {} }), |
| 82 | + method: "POST", |
| 83 | +diff --git a/toolbox/game-journey/game-journey.js b/toolbox/game-journey/game-journey.js |
| 84 | +index c2b2f876c..332274910 100644 |
| 85 | +--- a/toolbox/game-journey/game-journey.js |
| 86 | ++++ b/toolbox/game-journey/game-journey.js |
| 87 | +@@ -1161,7 +1161,7 @@ function renderRecommendedTargets() { |
| 88 | + table.setAttribute("aria-label", "Game Journey recommended planning targets"); |
| 89 | + const head = createElement("thead"); |
| 90 | + const headRow = createElement("tr"); |
| 91 | +- ["Target", "Section", "Suggested"].forEach((heading) => { |
| 92 | ++ ["Target", "Section", "Count"].forEach((heading) => { |
| 93 | + const cell = createElement("th", { text: heading }); |
| 94 | + cell.scope = "col"; |
| 95 | + headRow.append(cell); |
| 96 | +@@ -1169,16 +1169,22 @@ function renderRecommendedTargets() { |
| 97 | + head.append(headRow); |
| 98 | + const body = createElement("tbody"); |
| 99 | + targets.forEach((target) => { |
| 100 | ++ const targetCount = recommendedTargetValues.get(target.key) ?? target.suggestedCount; |
| 101 | + const row = createElement("tr"); |
| 102 | + row.dataset.journeyRecommendedTarget = target.key; |
| 103 | +- const labelCell = createElement("td", { text: target.label }); |
| 104 | ++ const labelCell = createElement("td"); |
| 105 | ++ const label = createElement("span", { text: target.label }); |
| 106 | ++ const countPreview = createElement("span", { text: ` [${targetCount}]` }); |
| 107 | ++ countPreview.dataset.journeyTargetCountPreview = target.key; |
| 108 | ++ labelCell.append(label, countPreview); |
| 109 | + const sectionCell = createElement("td", { text: target.sectionName }); |
| 110 | + const input = document.createElement("input"); |
| 111 | + input.type = "number"; |
| 112 | ++ input.inputMode = "numeric"; |
| 113 | + input.min = "0"; |
| 114 | + input.step = "1"; |
| 115 | +- input.value = String(recommendedTargetValues.get(target.key) ?? target.suggestedCount); |
| 116 | +- input.setAttribute("aria-label", `${target.label} suggested target`); |
| 117 | ++ input.value = String(targetCount); |
| 118 | ++ input.setAttribute("aria-label", `${target.label} count`); |
| 119 | + input.dataset.journeyTargetInput = target.key; |
| 120 | + const inputCell = createElement("td"); |
| 121 | + inputCell.append(input); |
| 122 | +@@ -1773,6 +1779,10 @@ recommendedTargets?.addEventListener("input", (event) => { |
| 123 | + const savedValue = normalizeTargetCount(updated.suggestedCount); |
| 124 | + recommendedTargetValues.set(target.key, savedValue); |
| 125 | + input.value = String(savedValue); |
| 126 | ++ const preview = recommendedTargets.querySelector(`[data-journey-target-count-preview='${target.key}']`); |
| 127 | ++ if (preview) { |
| 128 | ++ preview.textContent = ` [${savedValue}]`; |
136 | 129 | + } |
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; |
| 130 | + if (recommendedTargetStatus) { |
| 131 | + recommendedTargetStatus.textContent = `Saved ${target.label} target at ${savedValue}.`; |
141 | 132 | } |
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