diff --git a/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-branch-validation.txt b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-branch-validation.txt
new file mode 100644
index 000000000..d656315dd
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-branch-validation.txt
@@ -0,0 +1,6 @@
+Branch Validation: PASS
+
+PASS - Current branch is pr/26174-ALFA-012-game-hub-parent-child-final-validation.
+PASS - Expected branch is pr/26174-ALFA-012-game-hub-parent-child-final-validation.
+PASS - Continued stack from PR_26174_ALFA_011 branch lineage.
+PASS - Work stayed scoped to targeted Game Hub Playwright validation and required reports.
diff --git a/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-manual-validation-notes.txt b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-manual-validation-notes.txt
new file mode 100644
index 000000000..a50a8c4e8
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-manual-validation-notes.txt
@@ -0,0 +1,7 @@
+Manual Validation Notes: PASS
+
+- Reviewed the expanded Open Games row behavior through targeted Playwright assertions.
+- Confirmed Source Idea and Readiness Output use distinct child-table selectors and captions.
+- Confirmed Source Idea child context exposes no form controls, buttons, or editable regions.
+- Confirmed readiness checklist rows preserve the incoming service-contract order.
+- Confirmed creator-safe empty and unavailable states still pass targeted validation.
diff --git a/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-requirement-checklist.txt b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-requirement-checklist.txt
new file mode 100644
index 000000000..ddf3ecac6
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-requirement-checklist.txt
@@ -0,0 +1,10 @@
+Requirement Checklist: PASS
+
+PASS - Open Games parent rows are validated.
+PASS - Expanded game shows Source Idea child table.
+PASS - Expanded game shows Readiness Output child table.
+PASS - Source Idea is read-only with no edit/delete controls.
+PASS - Readiness Output is separate from Source Idea.
+PASS - Empty and unavailable states still pass targeted validation.
+PASS - Added targeted Playwright coverage only.
+PASS - No unrelated cleanup was introduced.
diff --git a/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-validation-lane.txt b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-validation-lane.txt
new file mode 100644
index 000000000..a645cc03d
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-validation-lane.txt
@@ -0,0 +1,10 @@
+Validation Lane: PASS
+
+Command:
+npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub validates Open Games parent and child tables|Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"
+
+Result:
+3 passed.
+
+Additional check:
+git diff --check
diff --git a/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation.md b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation.md
new file mode 100644
index 000000000..fa41c315b
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation.md
@@ -0,0 +1,16 @@
+# PR_26174_ALFA_012-game-hub-parent-child-final-validation
+
+## Purpose
+
+Final validation and report pass for the Game Hub parent/child table stack.
+
+## Summary
+
+- Added targeted Playwright coverage for Open Games parent rows and expanded child tables.
+- Validated Source Idea and Readiness Output render as separate child tables.
+- Validated Source Idea context is read-only and readiness output preserves incoming checklist order.
+- Re-ran targeted empty and unavailable state checks.
+
+## Validation
+
+PASS - `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub validates Open Games parent and child tables|Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"`
diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
index 137f2a6f7..3c4039b1f 100644
--- a/docs_build/dev/reports/codex_changed_files.txt
+++ b/docs_build/dev/reports/codex_changed_files.txt
@@ -1,9 +1,8 @@
-toolbox/game-hub/game-hub.js
tests/playwright/tools/GameHubMockRepository.spec.mjs
docs_build/dev/reports/codex_review.diff
docs_build/dev/reports/codex_changed_files.txt
-docs_build/dev/reports/PR_26174_ALFA_011-game-hub-readiness-output-child-table.md
-docs_build/dev/reports/PR_26174_ALFA_011-game-hub-readiness-output-child-table-branch-validation.txt
-docs_build/dev/reports/PR_26174_ALFA_011-game-hub-readiness-output-child-table-requirement-checklist.txt
-docs_build/dev/reports/PR_26174_ALFA_011-game-hub-readiness-output-child-table-validation-lane.txt
-docs_build/dev/reports/PR_26174_ALFA_011-game-hub-readiness-output-child-table-manual-validation-notes.txt
+docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation.md
+docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-branch-validation.txt
+docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-requirement-checklist.txt
+docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-validation-lane.txt
+docs_build/dev/reports/PR_26174_ALFA_012-game-hub-parent-child-final-validation-manual-validation-notes.txt
diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff
index 04ad8ebfe..41e24b645 100644
--- a/docs_build/dev/reports/codex_review.diff
+++ b/docs_build/dev/reports/codex_review.diff
@@ -1,149 +1,135 @@
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
-index 695b0a609..dac65c676 100644
+index dac65c676..4e617ed35 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
-@@ -288,6 +288,22 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
- "StatusUnder Construction",
- "OwnerUser 1",
- ]);
-+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(3);
-+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='source-idea'] caption")).toHaveText("Source Idea");
-+ const readinessOutputTable = page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='readiness-output']");
-+ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output");
-+ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]);
-+ await expect(readinessOutputTable.locator("tbody tr")).toHaveText([
-+ "Game StatusUnder Construction",
-+ "Game ProgressDemo Game identity ready",
-+ "Launch ProgressPublish blocked until configuration and required assets are ready",
-+ "Current FocusComplete Game Configuration",
-+ "Recommended Next ToolGame Configuration",
-+ "Game identityComplete",
-+ "Game configurationUnder Construction",
-+ "Playable buildPlanned",
-+ "Publishing reviewPlanned",
-+ ]);
- await demoGameRow.locator("[data-game-toggle='demo-game']").click();
- await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);
-
-diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
-index 67f56ca41..4b21814d7 100644
---- a/toolbox/game-hub/game-hub.js
-+++ b/toolbox/game-hub/game-hub.js
-@@ -319,7 +319,53 @@ function renderSourceIdeaChildTable(parent, game) {
- parent.append(wrapper);
- }
+@@ -335,6 +335,131 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
+ }
+ });
--function renderExpandedGameRow(tbody, game) {
-+function renderReadinessOutputChildTable(parent, game, progress, active) {
-+ const readiness = active && isRecord(progress)
-+ ? progress
-+ : {
-+ currentFocus: "Open this game to review readiness",
-+ gameProgress: `${game.name} identity ready`,
-+ gameStatus: game.status || "No status",
-+ publishingProgress: "Open this game to review launch progress",
-+ recommendedNextTool: "Game Hub",
-+ progressChecklist: [],
-+ };
-+ const wrapper = document.createElement("div");
-+ wrapper.className = "table-wrapper";
-+ const table = document.createElement("table");
-+ table.className = "data-table data-table--fixed";
-+ table.dataset.gameChildTable = "readiness-output";
-+ table.setAttribute("aria-label", `${game.name} readiness output`);
-+ table.innerHTML = "
Readiness Output| Output | Status |
";
-+ const body = document.createElement("tbody");
-+ [
-+ ["Game Status", readiness.gameStatus],
-+ ["Game Progress", readiness.gameProgress],
-+ ["Launch Progress", readiness.publishingProgress],
-+ ["Current Focus", readiness.currentFocus],
-+ ["Recommended Next Tool", readiness.recommendedNextTool],
-+ ].forEach(([label, value]) => {
-+ const row = document.createElement("tr");
-+ row.append(createCell(label, "th"), createCell(value || "Not available"));
-+ row.firstElementChild.scope = "row";
-+ body.append(row);
-+ });
++test("Game Hub validates Open Games parent and child tables", async ({ page }) => {
++ const sourceLinkedGame = {
++ id: "lantern-reef",
++ ownerKey: MOCK_DB_KEYS.users.user1,
++ name: "Lantern Reef",
++ purpose: "Game",
++ status: "Ready for Testing",
++ ownerDisplayName: "User 1",
++ members: [
++ {
++ displayName: "User 1",
++ gameId: "lantern-reef",
++ permission: "Owner",
++ role: "Owner",
++ userKey: MOCK_DB_KEYS.users.user1,
++ },
++ ],
++ sourceIdea: {
++ idea: "Lantern Reef",
++ pitch: "Guide reef keepers through dusk storms.",
++ notes: [
++ "Keep traversal gentle.",
++ "Use warm lantern art.",
++ ],
++ },
++ };
++ const journeyBuckets = [
++ "Idea",
++ "Design",
++ "Graphics",
++ "Audio",
++ "Objects",
++ "Worlds",
++ "Interface",
++ "Controls",
++ "Rules",
++ "Progression",
++ "Play Test",
++ "Publish",
++ "Share",
++ ];
+
-+ const checklist = Array.isArray(readiness.progressChecklist) ? readiness.progressChecklist : [];
-+ checklist.forEach((item) => {
-+ const row = document.createElement("tr");
-+ row.dataset.readinessChecklistRow = item.label || "Checklist";
-+ row.append(createCell(item.label || "Checklist", "th"), createCell(item.status || "Not available"));
-+ row.firstElementChild.scope = "row";
-+ body.append(row);
++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
++ await route.fulfill({
++ body: JSON.stringify({
++ data: { result: sourceLinkedGame },
++ 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/getGameProgress", async (route) => {
++ await route.fulfill({
++ body: JSON.stringify({
++ data: {
++ result: {
++ currentFocus: "Review source idea context",
++ gameProgress: "Lantern Reef identity ready",
++ gameStatus: "Ready for Testing",
++ publishingProgress: "Launch review pending",
++ recommendedNextTool: "Game Journey",
++ progressChecklist: journeyBuckets.map((label) => ({ label, status: "Planned" })),
++ },
++ },
++ 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: [sourceLinkedGame] },
++ 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() });
+
-+ table.append(body);
-+ wrapper.append(table);
-+ parent.append(wrapper);
-+}
++ try {
++ await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
++ const parentRows = page.locator("[data-game-parent-table='open-games'] tbody > [data-game-row]");
++ await expect(parentRows).toHaveCount(1);
++ const gameRow = page.locator("[data-game-row='lantern-reef']");
++ await expect(gameRow).toContainText("Lantern Reef");
++ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "false");
+
-+function renderExpandedGameRow(tbody, game, progress, active) {
- const row = document.createElement("tr");
- row.dataset.gameExpandedRow = game.id;
- row.id = `game-child-${game.id}`;
-@@ -329,16 +375,18 @@ function renderExpandedGameRow(tbody, game) {
- stack.className = "content-stack content-stack--compact";
- renderGameSummaryChildTable(stack, game);
- renderSourceIdeaChildTable(stack, game);
-+ renderReadinessOutputChildTable(stack, game, progress, active);
- content.append(stack);
- row.append(content);
- tbody.append(row);
- }
-
--function renderGameParentRow(tbody, game, activeGame) {
-+function renderGameParentRow(tbody, game, activeGame, progress) {
- const expanded = state.expandedGameId === game.id;
-+ const active = activeGame?.id === game.id;
- const row = document.createElement("tr");
- row.dataset.gameRow = game.id;
-- if (activeGame?.id === game.id) {
-+ if (active) {
- row.dataset.gameActive = "true";
- }
-
-@@ -353,12 +401,12 @@ function renderGameParentRow(tbody, game, activeGame) {
- );
-
- const actions = document.createElement("td");
-- actions.append(createGameButton(game, activeGame?.id === game.id));
-+ actions.append(createGameButton(game, active));
- row.append(actions);
- tbody.append(row);
-
- if (expanded) {
-- renderExpandedGameRow(tbody, game);
-+ renderExpandedGameRow(tbody, game, progress, active);
- }
- }
-
-@@ -389,7 +437,7 @@ function renderProjectInformation(activeGame, currentMember, progress) {
- : "Project Information loaded. Sign in to save changes.");
- }
-
--function renderGameList() {
-+function renderGameList(progress) {
- if (!elements.gameList) {
- return;
- }
-@@ -421,7 +469,7 @@ function renderGameList() {
- table.setAttribute("aria-label", "Open Games");
- table.innerHTML = "Open Games| Game | Purpose | Status | Owner | Actions |
";
- const body = document.createElement("tbody");
-- listResult.forEach((game) => renderGameParentRow(body, game, activeGame));
-+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress));
- table.append(body);
- wrapper.append(table);
- elements.gameList.append(wrapper);
-@@ -558,7 +606,7 @@ function renderWorkspace() {
- }
- }
-
-- renderGameList();
-+ renderGameList(progress);
- renderMembersTable(activeGame);
- renderTableCounts();
- renderChecklist(progress);
++ await gameRow.locator("[data-game-toggle='lantern-reef']").click();
++ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "true");
++ const expandedRow = page.locator("[data-game-expanded-row='lantern-reef']");
++ await expect(expandedRow).toHaveCount(1);
++ await expect(expandedRow.locator("[data-game-child-table]")).toHaveCount(3);
++
++ const sourceIdeaTable = expandedRow.locator("[data-game-child-table='source-idea']");
++ await expect(sourceIdeaTable.locator("caption")).toHaveText("Source Idea");
++ await expect(sourceIdeaTable.locator("tbody tr")).toHaveText([
++ "IdeaLantern Reef",
++ "PitchGuide reef keepers through dusk storms.",
++ "Note 1Keep traversal gentle.",
++ "Note 2Use warm lantern art.",
++ ]);
++ await expect(sourceIdeaTable.locator("button, input, textarea, select, [contenteditable='true'], [role='button']")).toHaveCount(0);
++ await expect(sourceIdeaTable).not.toContainText(/Edit|Delete|Current Focus|Recommended Next Tool/);
++
++ const readinessOutputTable = expandedRow.locator("[data-game-child-table='readiness-output']");
++ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output");
++ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]);
++ await expect(readinessOutputTable).not.toContainText(/Guide reef keepers|Keep traversal gentle|Use warm lantern art/);
++ await expect(readinessOutputTable.locator("[data-readiness-checklist-row] th")).toHaveText(journeyBuckets);
++
++ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveCount(0);
++ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveCount(0);
++ await expectNoPageFailures(failures);
++ } finally {
++ await failures.server.close();
++ }
++});
++
+ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }) => {
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html");
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
index dac65c676..4e617ed35 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
@@ -335,6 +335,131 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
}
});
+test("Game Hub validates Open Games parent and child tables", async ({ page }) => {
+ const sourceLinkedGame = {
+ id: "lantern-reef",
+ ownerKey: MOCK_DB_KEYS.users.user1,
+ name: "Lantern Reef",
+ purpose: "Game",
+ status: "Ready for Testing",
+ ownerDisplayName: "User 1",
+ members: [
+ {
+ displayName: "User 1",
+ gameId: "lantern-reef",
+ permission: "Owner",
+ role: "Owner",
+ userKey: MOCK_DB_KEYS.users.user1,
+ },
+ ],
+ sourceIdea: {
+ idea: "Lantern Reef",
+ pitch: "Guide reef keepers through dusk storms.",
+ notes: [
+ "Keep traversal gentle.",
+ "Use warm lantern art.",
+ ],
+ },
+ };
+ const journeyBuckets = [
+ "Idea",
+ "Design",
+ "Graphics",
+ "Audio",
+ "Objects",
+ "Worlds",
+ "Interface",
+ "Controls",
+ "Rules",
+ "Progression",
+ "Play Test",
+ "Publish",
+ "Share",
+ ];
+
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: { result: sourceLinkedGame },
+ 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/getGameProgress", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: {
+ result: {
+ currentFocus: "Review source idea context",
+ gameProgress: "Lantern Reef identity ready",
+ gameStatus: "Ready for Testing",
+ publishingProgress: "Launch review pending",
+ recommendedNextTool: "Game Journey",
+ progressChecklist: journeyBuckets.map((label) => ({ label, status: "Planned" })),
+ },
+ },
+ 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: [sourceLinkedGame] },
+ 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-game-parent-table='open-games'] caption")).toHaveText("Open Games");
+ const parentRows = page.locator("[data-game-parent-table='open-games'] tbody > [data-game-row]");
+ await expect(parentRows).toHaveCount(1);
+ const gameRow = page.locator("[data-game-row='lantern-reef']");
+ await expect(gameRow).toContainText("Lantern Reef");
+ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "false");
+
+ await gameRow.locator("[data-game-toggle='lantern-reef']").click();
+ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "true");
+ const expandedRow = page.locator("[data-game-expanded-row='lantern-reef']");
+ await expect(expandedRow).toHaveCount(1);
+ await expect(expandedRow.locator("[data-game-child-table]")).toHaveCount(3);
+
+ const sourceIdeaTable = expandedRow.locator("[data-game-child-table='source-idea']");
+ await expect(sourceIdeaTable.locator("caption")).toHaveText("Source Idea");
+ await expect(sourceIdeaTable.locator("tbody tr")).toHaveText([
+ "IdeaLantern Reef",
+ "PitchGuide reef keepers through dusk storms.",
+ "Note 1Keep traversal gentle.",
+ "Note 2Use warm lantern art.",
+ ]);
+ await expect(sourceIdeaTable.locator("button, input, textarea, select, [contenteditable='true'], [role='button']")).toHaveCount(0);
+ await expect(sourceIdeaTable).not.toContainText(/Edit|Delete|Current Focus|Recommended Next Tool/);
+
+ const readinessOutputTable = expandedRow.locator("[data-game-child-table='readiness-output']");
+ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output");
+ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]);
+ await expect(readinessOutputTable).not.toContainText(/Guide reef keepers|Keep traversal gentle|Use warm lantern art/);
+ await expect(readinessOutputTable.locator("[data-readiness-checklist-row] th")).toHaveText(journeyBuckets);
+
+ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveCount(0);
+ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveCount(0);
+ await expectNoPageFailures(failures);
+ } finally {
+ await failures.server.close();
+ }
+});
+
test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }) => {
const failures = await openRepoPage(page, "/toolbox/game-hub/index.html");