Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Branch Validation: PASS

PASS - Current branch: pr/26174-ALFA-007-game-journey-count-ui-polish.
PASS - Stack base: pr/26174-ALFA-006-game-hub-empty-and-error-states.
PASS - Changes are scoped to Game Journey count UI polish, targeted tests, and required reports.
PASS - No merge to main performed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Manual Validation Notes: PASS

PASS - Confirmed target labels render Hero [1], Enemy [4], Boss [1], Background [3], Music [5].
PASS - Confirmed Hero count preview updates to [2] after edit and remains [2] after reload.
PASS - Confirmed updateRecommendedTarget API/service method is called with ["hero", 2].
PASS - Confirmed no checkbox inputs were introduced.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Requirement Checklist: PASS

PASS - Polished count-based Journey inputs with visible [count] labels.
PASS - Kept numeric counts and verified no checkbox inputs in the target model.
PASS - Preserved target/bucket order: Hero, Enemy, Boss, Background, Music.
PASS - Persisted edits through the existing API/service contract only.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Validation Lane: PASS

Targeted Playwright impacted lane:
PASS - npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs -g "Game Journey progress dashboard summarizes completion metrics"

Notes:
- Full workspace smoke was not run; targeted impacted Playwright validation was used per request.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# PR_26174_ALFA_007-game-journey-count-ui-polish

## Purpose

Polish count-based Game Journey inputs.

## Summary

- Added live [count] previews to Game Journey recommended target labels.
- Clarified the recommended target table header from Suggested to Count.
- Kept native numeric inputs and added numeric input hints.
- Extended targeted Playwright coverage for order, no-checkbox model, live preview updates, and API/service contract persistence.

## Validation

PASS - `npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs -g "Game Journey progress dashboard summarizes completion metrics"`
14 changes: 7 additions & 7 deletions docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
toolbox/game-hub/game-hub.js
tests/playwright/tools/GameHubMockRepository.spec.mjs
toolbox/game-journey/game-journey.js
tests/playwright/tools/GameJourneyTool.spec.mjs
docs_build/dev/reports/codex_review.diff
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish.md
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_007-game-journey-count-ui-polish-manual-validation-notes.txt
267 changes: 126 additions & 141 deletions docs_build/dev/reports/codex_review.diff
Original file line number Diff line number Diff line change
@@ -1,147 +1,132 @@
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
index 2d583e1d7..e11631269 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
@@ -322,6 +322,89 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }
diff --git a/tests/playwright/tools/GameJourneyTool.spec.mjs b/tests/playwright/tools/GameJourneyTool.spec.mjs
index e9005f08b..586e7521f 100644
--- a/tests/playwright/tools/GameJourneyTool.spec.mjs
+++ b/tests/playwright/tools/GameJourneyTool.spec.mjs
@@ -69,7 +69,6 @@ async function openRepoPage(page, pathName, options = {}) {
page.on("requestfailed", (request) => {
failedRequests.push(`FAILED ${request.url()}`);
});
-
if (collectCoverage) {
await workspaceV2CoverageReporter.start(page);
}
});
@@ -258,6 +257,7 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p
const failedRequests = [];
const pageErrors = [];
const consoleErrors = [];
+ const recommendedTargetRequests = [];

+test("Game Hub shows a creator-safe empty state when no projects exist", async ({ page }) => {
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: { result: null },
+ ok: true,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 200,
+ });
page.on("pageerror", (error) => {
pageErrors.push(error.message);
@@ -275,6 +275,12 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p
page.on("requestfailed", (request) => {
failedRequests.push(`FAILED ${request.url()}`);
});
+ page.on("request", (request) => {
+ const requestUrl = request.url();
+ if (requestUrl.includes("/api/toolbox/game-journey/repositories/") && requestUrl.includes("/methods/updateRecommendedTarget")) {
+ recommendedTargetRequests.push(request.postDataJSON());
+ }
+ });
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: {
+ result: {
+ gameStatus: "No Game",
+ gameProgress: "No active game",
+ publishingProgress: "Not started",
+ currentFocus: "Create a game",
+ recommendedNextTool: "Game Hub",
+ progressChecklist: [],
+ },
+ },
+ ok: true,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 200,
+ });
+ });
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: { result: [] },
+ ok: true,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 200,
+ });
+ });
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() });
+
+ try {
+ await expect(page.locator("[data-active-game-name]")).toHaveText("No game open");
+ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Create a game to start building.");
+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
+ await expect(page.locator("[data-game-hub-log]")).not.toContainText(/server|API|repository|database|stack|error/i);
+ await expectNoPageFailures(failures);
+ } finally {
+ await failures.server.close();
+ }
+});
+
+test("Game Hub shows a creator-safe unavailable state when project list API fails", async ({ page }) => {
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ error: "postgres://service-role-secret@internal.example:5432/gamefoundry failed with stack trace",
+ ok: false,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 503,
+ });
+ });
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() });
+
+ try {
+ expect(failures.failedRequests.some((request) => request.includes("503") && request.includes("/methods/listGames"))).toBe(true);
+ 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.");
+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.");
+ await expect(page.locator("main")).not.toContainText(/postgres|service-role-secret|internal\.example|stack trace|repository|database/i);
+ expect(failures.pageErrors).toEqual([]);
+ expect(failures.consoleErrors.filter((message) => !message.includes("status of 503"))).toEqual([]);
+ } finally {
+ await failures.server.close();
+ }
+});
+
test("Game Hub shows active-game errors without throwing", async ({ page }) => {
await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
await route.fulfill({
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
index d2260fe04..2d6f321e4 100644
--- a/toolbox/game-hub/game-hub.js
+++ b/toolbox/game-hub/game-hub.js
@@ -217,6 +217,14 @@ function createGameButton(game, isActive) {
return button;
}

+function createGameListStatus(message, state) {
+ const emptyState = document.createElement("p");
+ emptyState.className = "status";
+ emptyState.dataset.gameListStatus = state;
+ emptyState.textContent = message;
+ return emptyState;
+}
+
function renderProjectInformation(activeGame, currentMember, progress) {
if (!elements.projectRecordsTable) {
return;
@@ -252,22 +260,23 @@ function renderGameList() {
const activeGame = normalizeActiveGame(repository.getActiveGame());
const gameUserKey = currentGameUserKey(activeGame);
const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {});
- const games = Array.isArray(listResult) ? listResult : [];
- if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) {
- setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly.");
- }

elements.gameList.replaceChildren();

- if (games.length === 0) {
- const emptyState = document.createElement("p");
- emptyState.className = "status";
- emptyState.textContent = "No games. Create a game to continue.";
- elements.gameList.append(emptyState);
+ if (!Array.isArray(listResult)) {
+ const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.";
+ reportRepositoryError(listResult, "Game Hub projects");
+ setStatusLog(message);
+ elements.gameList.append(createGameListStatus(message, "unavailable"));
+ return;
try {
await workspaceV2CoverageReporter.start(page);
@@ -332,12 +338,23 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p
"Next action: mark one finished section item complete so overall progress can rise above 0%.",
]);
await expect(page.locator("[data-journey-recommended-target]")).toHaveCount(5);
+ const recommendedTargetOrder = await page.locator("[data-journey-recommended-target]").evaluateAll((rows) => (
+ rows.map((row) => row.dataset.journeyRecommendedTarget)
+ ));
+ expect(recommendedTargetOrder).toEqual([
+ "hero",
+ "enemy",
+ "boss",
+ "background",
+ "music",
+ ]);
+ await expect(page.locator("[data-journey-recommended-targets] th")).toHaveText(["Target", "Section", "Count"]);
await expect(page.locator("[data-journey-recommended-target] td:first-child")).toHaveText([
- "Hero",
- "Enemy",
- "Boss",
- "Background",
- "Music",
+ "Hero [1]",
+ "Enemy [4]",
+ "Boss [1]",
+ "Background [3]",
+ "Music [5]",
]);
await expect(page.locator("[data-journey-recommended-target='hero'] td").nth(1)).toHaveText("Objects");
await expect(page.locator("[data-journey-recommended-target='enemy'] td").nth(1)).toHaveText("Objects");
@@ -349,11 +366,19 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p
await expect(page.locator("[data-journey-target-input='boss']")).toHaveValue("1");
await expect(page.locator("[data-journey-target-input='background']")).toHaveValue("3");
await expect(page.locator("[data-journey-target-input='music']")).toHaveValue("5");
+ await expect(page.locator("[data-journey-recommended-target] input[type='checkbox']")).toHaveCount(0);
+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("type", "number");
+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("inputmode", "numeric");
+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("aria-label", "Hero count");
await page.locator("[data-journey-target-input='hero']").fill("2");
await expect(page.locator("[data-journey-target-status]")).toHaveText("Saved Hero target at 2.");
await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("2");
+ await expect(page.locator("[data-journey-target-count-preview='hero']")).toHaveText(" [2]");
+ expect(recommendedTargetRequests).toHaveLength(1);
+ expect(recommendedTargetRequests[0].args).toEqual(["hero", 2]);
await page.reload({ waitUntil: "networkidle" });
await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("2");
+ await expect(page.locator("[data-journey-target-count-preview='hero']")).toHaveText(" [2]");
const repositoryData = await fetchApiData(server, "/api/toolbox/game-journey/repositories", {
body: JSON.stringify({ options: {} }),
method: "POST",
diff --git a/toolbox/game-journey/game-journey.js b/toolbox/game-journey/game-journey.js
index c2b2f876c..332274910 100644
--- a/toolbox/game-journey/game-journey.js
+++ b/toolbox/game-journey/game-journey.js
@@ -1161,7 +1161,7 @@ function renderRecommendedTargets() {
table.setAttribute("aria-label", "Game Journey recommended planning targets");
const head = createElement("thead");
const headRow = createElement("tr");
- ["Target", "Section", "Suggested"].forEach((heading) => {
+ ["Target", "Section", "Count"].forEach((heading) => {
const cell = createElement("th", { text: heading });
cell.scope = "col";
headRow.append(cell);
@@ -1169,16 +1169,22 @@ function renderRecommendedTargets() {
head.append(headRow);
const body = createElement("tbody");
targets.forEach((target) => {
+ const targetCount = recommendedTargetValues.get(target.key) ?? target.suggestedCount;
const row = createElement("tr");
row.dataset.journeyRecommendedTarget = target.key;
- const labelCell = createElement("td", { text: target.label });
+ const labelCell = createElement("td");
+ const label = createElement("span", { text: target.label });
+ const countPreview = createElement("span", { text: ` [${targetCount}]` });
+ countPreview.dataset.journeyTargetCountPreview = target.key;
+ labelCell.append(label, countPreview);
const sectionCell = createElement("td", { text: target.sectionName });
const input = document.createElement("input");
input.type = "number";
+ input.inputMode = "numeric";
input.min = "0";
input.step = "1";
- input.value = String(recommendedTargetValues.get(target.key) ?? target.suggestedCount);
- input.setAttribute("aria-label", `${target.label} suggested target`);
+ input.value = String(targetCount);
+ input.setAttribute("aria-label", `${target.label} count`);
input.dataset.journeyTargetInput = target.key;
const inputCell = createElement("td");
inputCell.append(input);
@@ -1773,6 +1779,10 @@ recommendedTargets?.addEventListener("input", (event) => {
const savedValue = normalizeTargetCount(updated.suggestedCount);
recommendedTargetValues.set(target.key, savedValue);
input.value = String(savedValue);
+ const preview = recommendedTargets.querySelector(`[data-journey-target-count-preview='${target.key}']`);
+ if (preview) {
+ preview.textContent = ` [${savedValue}]`;
+ }
+
+ if (listResult.length === 0) {
+ elements.gameList.append(createGameListStatus("No Game Hub projects yet. Create a game to start building.", "empty"));
return;
if (recommendedTargetStatus) {
recommendedTargetStatus.textContent = `Saved ${target.label} target at ${savedValue}.`;
}

- games.forEach((game) => {
+ listResult.forEach((game) => {
const row = document.createElement("article");
row.className = "callout";
row.dataset.gameRow = game.id;
Loading
Loading