Skip to content

Commit 7ebadc4

Browse files
committed
PR_26174_ALFA_007-game-journey-count-ui-polish
1 parent c226052 commit 7ebadc4

9 files changed

Lines changed: 219 additions & 158 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-007-game-journey-count-ui-polish.
4+
PASS - Stack base: pr/26174-ALFA-006-game-hub-empty-and-error-states.
5+
PASS - Changes are scoped to Game Journey count UI polish, 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 target labels render Hero [1], Enemy [4], Boss [1], Background [3], Music [5].
4+
PASS - Confirmed Hero count preview updates to [2] after edit and remains [2] after reload.
5+
PASS - Confirmed updateRecommendedTarget API/service method is called with ["hero", 2].
6+
PASS - Confirmed no checkbox inputs were introduced.
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 - Polished count-based Journey inputs with visible [count] labels.
4+
PASS - Kept numeric counts and verified no checkbox inputs in the target model.
5+
PASS - Preserved target/bucket order: Hero, Enemy, Boss, Background, Music.
6+
PASS - Persisted edits through the existing API/service contract only.
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/GameJourneyTool.spec.mjs -g "Game Journey progress dashboard summarizes completion metrics"
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_007-game-journey-count-ui-polish
2+
3+
## Purpose
4+
5+
Polish count-based Game Journey inputs.
6+
7+
## Summary
8+
9+
- Added live [count] previews to Game Journey recommended target labels.
10+
- Clarified the recommended target table header from Suggested to Count.
11+
- Kept native numeric inputs and added numeric input hints.
12+
- Extended targeted Playwright coverage for order, no-checkbox model, live preview updates, and API/service contract persistence.
13+
14+
## Validation
15+
16+
PASS - `npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs -g "Game Journey progress dashboard summarizes completion metrics"`
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
toolbox/game-hub/game-hub.js
2-
tests/playwright/tools/GameHubMockRepository.spec.mjs
1+
toolbox/game-journey/game-journey.js
2+
tests/playwright/tools/GameJourneyTool.spec.mjs
33
docs_build/dev/reports/codex_review.diff
44
docs_build/dev/reports/codex_changed_files.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
5+
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish.md
6+
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-branch-validation.txt
7+
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-requirement-checklist.txt
8+
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-validation-lane.txt
9+
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-manual-validation-notes.txt
Lines changed: 126 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,132 @@
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);
612
}
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 = [];
818

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+
+ }
2030
+ });
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-
- }
12231

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}]`;
136129
+ }
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}.`;
141132
}
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)