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-009-game-hub-parent-child-table-layout.
PASS - Stack base: pr/26174-ALFA-008-alpha-stack-final-validation.
PASS - Changes are scoped to Game Hub Open Games parent/child table layout, 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 Open Games renders as a parent table with expected headers.
PASS - Confirmed Demo Game expands to a child Game Summary table and collapses again.
PASS - Confirmed Open Game buttons still open games through the existing repository path.
PASS - Confirmed empty and unavailable Open Games states still render without game rows.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Requirement Checklist: PASS

PASS - Parent table is Open Games.
PASS - Each parent row represents one game.
PASS - Game rows can expand and collapse.
PASS - Used the existing parent-row plus expanded child-row table pattern.
PASS - Preserved existing Local API/service contract.
PASS - No browser-owned project data was added.
PASS - No page-local project arrays were added.
PASS - Empty and unavailable states remain explicit; no silent fallbacks were added.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Validation Lane: PASS

PASS - npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub creates, opens, and deletes mock games"
PASS - npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# PR_26174_ALFA_009-game-hub-parent-child-table-layout

## Purpose

Convert Game Hub Open Games into a reusable parent-table / child-table structure.

## Summary

- Rendered Open Games as a parent data table with one row per game.
- Added expand/collapse controls for each game row.
- Added a nested Game Summary child table in the expanded row using the existing parent-row plus expanded-child-row pattern.
- Preserved existing Local API/service repository calls and Open button behavior.

## Validation

PASS - `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub creates, opens, and deletes mock games"`
PASS - `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"`
13 changes: 7 additions & 6 deletions docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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_008-alpha-stack-final-validation.md
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-final-stack-report.md
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout.md
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-manual-validation-notes.txt
196 changes: 195 additions & 1 deletion docs_build/dev/reports/codex_review.diff
Original file line number Diff line number Diff line change
@@ -1 +1,195 @@
No executable code changes in PR_26174_ALFA_008; final validation and reporting only.
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
index e11631269..695b0a609 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
@@ -264,10 +264,32 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
await expect(page.locator("[data-game-list]")).toContainText("Collision Demo");
await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo");
+ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games");
+ await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
+ await expect(page.locator("[data-game-parent-table='open-games'] thead th")).toHaveText([
+ "Game",
+ "Purpose",
+ "Status",
+ "Owner",
+ "Actions",
+ ]);
const demoGameRow = page.locator("[data-game-row='demo-game']");
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/);
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true");
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
+ await expect(page.locator("[data-game-row='demo-game'] + [data-game-expanded-row='demo-game']")).toHaveCount(1);
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toContainText("Game Summary");
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary'] tbody tr")).toHaveText([
+ "ProjectDemo Game",
+ "PurposeGame",
+ "StatusUnder Construction",
+ "OwnerUser 1",
+ ]);
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
+ await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);

await page.getByLabel("Game Name").fill("Launch Test Game");
await page.getByRole("button", { name: "Create Game" }).click();
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
index 2d6f321e4..121e82562 100644
--- a/toolbox/game-hub/game-hub.js
+++ b/toolbox/game-hub/game-hub.js
@@ -39,6 +39,10 @@ const elements = {
tableCounts: document.querySelector("[data-game-table-counts]"),
};

+const state = {
+ expandedGameId: "",
+};
+
function setText(element, value) {
if (element && typeof element.forEach === "function" && !element.nodeType) {
element.forEach((item) => {
@@ -225,6 +229,90 @@ function createGameListStatus(message, state) {
return emptyState;
}

+function createCell(value, tagName = "td") {
+ const cell = document.createElement(tagName);
+ cell.textContent = value;
+ return cell;
+}
+
+function createGameToggleButton(game, expanded) {
+ const button = document.createElement("button");
+ button.className = expanded ? "btn btn--compact primary" : "btn btn--compact";
+ button.type = "button";
+ button.dataset.gameToggle = game.id;
+ button.setAttribute("aria-expanded", String(expanded));
+ button.setAttribute("aria-controls", `game-child-${game.id}`);
+ button.textContent = game.name;
+ return button;
+}
+
+function renderGameSummaryChildTable(parent, game) {
+ const wrapper = document.createElement("div");
+ wrapper.className = "table-wrapper";
+ const table = document.createElement("table");
+ table.className = "data-table data-table--fixed";
+ table.dataset.gameChildTable = "summary";
+ table.setAttribute("aria-label", `${game.name} game summary`);
+ table.innerHTML = "<caption>Game Summary</caption><thead><tr><th scope=\"col\">Field</th><th scope=\"col\">Value</th></tr></thead>";
+ const body = document.createElement("tbody");
+ [
+ ["Project", game.name],
+ ["Purpose", game.purpose],
+ ["Status", game.status],
+ ["Owner", game.ownerDisplayName],
+ ].forEach(([label, value]) => {
+ const row = document.createElement("tr");
+ row.append(createCell(label, "th"), createCell(value || "Not set"));
+ row.firstElementChild.scope = "row";
+ body.append(row);
+ });
+ table.append(body);
+ wrapper.append(table);
+ parent.append(wrapper);
+}
+
+function renderExpandedGameRow(tbody, game) {
+ const row = document.createElement("tr");
+ row.dataset.gameExpandedRow = game.id;
+ row.id = `game-child-${game.id}`;
+ const content = document.createElement("td");
+ content.colSpan = 5;
+ const stack = document.createElement("div");
+ stack.className = "content-stack content-stack--compact";
+ renderGameSummaryChildTable(stack, game);
+ content.append(stack);
+ row.append(content);
+ tbody.append(row);
+}
+
+function renderGameParentRow(tbody, game, activeGame) {
+ const expanded = state.expandedGameId === game.id;
+ const row = document.createElement("tr");
+ row.dataset.gameRow = game.id;
+ if (activeGame?.id === game.id) {
+ row.dataset.gameActive = "true";
+ }
+
+ const nameCell = document.createElement("th");
+ nameCell.scope = "row";
+ nameCell.append(createGameToggleButton(game, expanded));
+ row.append(
+ nameCell,
+ createCell(game.purpose || "Game"),
+ createCell(game.status || "No status"),
+ createCell(game.ownerDisplayName || "No owner"),
+ );
+
+ const actions = document.createElement("td");
+ actions.append(createGameButton(game, activeGame?.id === game.id));
+ row.append(actions);
+ tbody.append(row);
+
+ if (expanded) {
+ renderExpandedGameRow(tbody, game);
+ }
+}
+
function renderProjectInformation(activeGame, currentMember, progress) {
if (!elements.projectRecordsTable) {
return;
@@ -276,25 +364,18 @@ function renderGameList() {
return;
}

- listResult.forEach((game) => {
- const row = document.createElement("article");
- row.className = "callout";
- row.dataset.gameRow = game.id;
-
- const title = document.createElement("h4");
- title.textContent = game.name;
-
- const meta = document.createElement("p");
- meta.className = "eyebrow";
- meta.textContent = `${game.purpose} | ${game.status} | ${game.ownerDisplayName}`;
-
- const isActive = activeGame?.id === game.id;
- const action = createGameButton(game, isActive);
-
- row.append(title, meta, action);
-
- elements.gameList.append(row);
- });
+ const wrapper = document.createElement("div");
+ wrapper.className = "table-wrapper";
+ const table = document.createElement("table");
+ table.className = "data-table data-table--fixed";
+ table.dataset.gameParentTable = "open-games";
+ table.setAttribute("aria-label", "Open Games");
+ table.innerHTML = "<caption>Open Games</caption><thead><tr><th scope=\"col\">Game</th><th scope=\"col\">Purpose</th><th scope=\"col\">Status</th><th scope=\"col\">Owner</th><th scope=\"col\">Actions</th></tr></thead>";
+ const body = document.createElement("tbody");
+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame));
+ table.append(body);
+ wrapper.append(table);
+ elements.gameList.append(wrapper);
}

function renderMembersTable(activeGame) {
@@ -471,6 +552,13 @@ elements.form?.addEventListener("submit", (event) => {
});

elements.gameList?.addEventListener("click", (event) => {
+ const toggle = event.target.closest("[data-game-toggle]");
+ if (toggle) {
+ state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle;
+ renderWorkspace();
+ return;
+ }
+
const button = event.target.closest("[data-game-open]");

if (!button) {
22 changes: 22 additions & 0 deletions tests/playwright/tools/GameHubMockRepository.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,32 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
await expect(page.locator("[data-game-list]")).toContainText("Collision Demo");
await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo");
await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games");
await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
await expect(page.locator("[data-game-parent-table='open-games'] thead th")).toHaveText([
"Game",
"Purpose",
"Status",
"Owner",
"Actions",
]);
const demoGameRow = page.locator("[data-game-row='demo-game']");
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/);
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true");
await demoGameRow.locator("[data-game-toggle='demo-game']").click();
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
await expect(page.locator("[data-game-row='demo-game'] + [data-game-expanded-row='demo-game']")).toHaveCount(1);
await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toContainText("Game Summary");
await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary'] tbody tr")).toHaveText([
"ProjectDemo Game",
"PurposeGame",
"StatusUnder Construction",
"OwnerUser 1",
]);
await demoGameRow.locator("[data-game-toggle='demo-game']").click();
await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);

await page.getByLabel("Game Name").fill("Launch Test Game");
await page.getByRole("button", { name: "Create Game" }).click();
Expand Down
Loading
Loading