Skip to content

Commit 06a1b61

Browse files
committed
PR_26174_ALFA_009-game-hub-parent-child-table-layout
1 parent ae36ced commit 06a1b61

9 files changed

Lines changed: 374 additions & 26 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-009-game-hub-parent-child-table-layout.
4+
PASS - Stack base: pr/26174-ALFA-008-alpha-stack-final-validation.
5+
PASS - Changes are scoped to Game Hub Open Games parent/child table layout, 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 Open Games renders as a parent table with expected headers.
4+
PASS - Confirmed Demo Game expands to a child Game Summary table and collapses again.
5+
PASS - Confirmed Open Game buttons still open games through the existing repository path.
6+
PASS - Confirmed empty and unavailable Open Games states still render without game rows.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Requirement Checklist: PASS
2+
3+
PASS - Parent table is Open Games.
4+
PASS - Each parent row represents one game.
5+
PASS - Game rows can expand and collapse.
6+
PASS - Used the existing parent-row plus expanded child-row table pattern.
7+
PASS - Preserved existing Local API/service contract.
8+
PASS - No browser-owned project data was added.
9+
PASS - No page-local project arrays were added.
10+
PASS - Empty and unavailable states remain explicit; no silent fallbacks were added.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Validation Lane: PASS
2+
3+
PASS - npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub creates, opens, and deletes mock games"
4+
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"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PR_26174_ALFA_009-game-hub-parent-child-table-layout
2+
3+
## Purpose
4+
5+
Convert Game Hub Open Games into a reusable parent-table / child-table structure.
6+
7+
## Summary
8+
9+
- Rendered Open Games as a parent data table with one row per game.
10+
- Added expand/collapse controls for each game row.
11+
- Added a nested Game Summary child table in the expanded row using the existing parent-row plus expanded-child-row pattern.
12+
- Preserved existing Local API/service repository calls and Open button behavior.
13+
14+
## Validation
15+
16+
PASS - `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub creates, opens, and deletes mock games"`
17+
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"`
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
toolbox/game-hub/game-hub.js
2+
tests/playwright/tools/GameHubMockRepository.spec.mjs
13
docs_build/dev/reports/codex_review.diff
24
docs_build/dev/reports/codex_changed_files.txt
3-
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation.md
4-
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-final-stack-report.md
5-
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-branch-validation.txt
6-
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-requirement-checklist.txt
7-
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-validation-lane.txt
8-
docs_build/dev/reports/PR_26174_ALFA_008-alpha-stack-final-validation-manual-validation-notes.txt
5+
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout.md
6+
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-branch-validation.txt
7+
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-requirement-checklist.txt
8+
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-validation-lane.txt
9+
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-manual-validation-notes.txt
Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,195 @@
1-
No executable code changes in PR_26174_ALFA_008; final validation and reporting only.
1+
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
2+
index e11631269..695b0a609 100644
3+
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
4+
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
5+
@@ -264,10 +264,32 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
6+
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
7+
await expect(page.locator("[data-game-list]")).toContainText("Collision Demo");
8+
await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo");
9+
+ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games");
10+
+ await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
11+
+ await expect(page.locator("[data-game-parent-table='open-games'] thead th")).toHaveText([
12+
+ "Game",
13+
+ "Purpose",
14+
+ "Status",
15+
+ "Owner",
16+
+ "Actions",
17+
+ ]);
18+
const demoGameRow = page.locator("[data-game-row='demo-game']");
19+
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
20+
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
21+
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/);
22+
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true");
23+
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
24+
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
25+
+ await expect(page.locator("[data-game-row='demo-game'] + [data-game-expanded-row='demo-game']")).toHaveCount(1);
26+
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toContainText("Game Summary");
27+
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary'] tbody tr")).toHaveText([
28+
+ "ProjectDemo Game",
29+
+ "PurposeGame",
30+
+ "StatusUnder Construction",
31+
+ "OwnerUser 1",
32+
+ ]);
33+
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
34+
+ await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);
35+
36+
await page.getByLabel("Game Name").fill("Launch Test Game");
37+
await page.getByRole("button", { name: "Create Game" }).click();
38+
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
39+
index 2d6f321e4..121e82562 100644
40+
--- a/toolbox/game-hub/game-hub.js
41+
+++ b/toolbox/game-hub/game-hub.js
42+
@@ -39,6 +39,10 @@ const elements = {
43+
tableCounts: document.querySelector("[data-game-table-counts]"),
44+
};
45+
46+
+const state = {
47+
+ expandedGameId: "",
48+
+};
49+
+
50+
function setText(element, value) {
51+
if (element && typeof element.forEach === "function" && !element.nodeType) {
52+
element.forEach((item) => {
53+
@@ -225,6 +229,90 @@ function createGameListStatus(message, state) {
54+
return emptyState;
55+
}
56+
57+
+function createCell(value, tagName = "td") {
58+
+ const cell = document.createElement(tagName);
59+
+ cell.textContent = value;
60+
+ return cell;
61+
+}
62+
+
63+
+function createGameToggleButton(game, expanded) {
64+
+ const button = document.createElement("button");
65+
+ button.className = expanded ? "btn btn--compact primary" : "btn btn--compact";
66+
+ button.type = "button";
67+
+ button.dataset.gameToggle = game.id;
68+
+ button.setAttribute("aria-expanded", String(expanded));
69+
+ button.setAttribute("aria-controls", `game-child-${game.id}`);
70+
+ button.textContent = game.name;
71+
+ return button;
72+
+}
73+
+
74+
+function renderGameSummaryChildTable(parent, game) {
75+
+ const wrapper = document.createElement("div");
76+
+ wrapper.className = "table-wrapper";
77+
+ const table = document.createElement("table");
78+
+ table.className = "data-table data-table--fixed";
79+
+ table.dataset.gameChildTable = "summary";
80+
+ table.setAttribute("aria-label", `${game.name} game summary`);
81+
+ table.innerHTML = "<caption>Game Summary</caption><thead><tr><th scope=\"col\">Field</th><th scope=\"col\">Value</th></tr></thead>";
82+
+ const body = document.createElement("tbody");
83+
+ [
84+
+ ["Project", game.name],
85+
+ ["Purpose", game.purpose],
86+
+ ["Status", game.status],
87+
+ ["Owner", game.ownerDisplayName],
88+
+ ].forEach(([label, value]) => {
89+
+ const row = document.createElement("tr");
90+
+ row.append(createCell(label, "th"), createCell(value || "Not set"));
91+
+ row.firstElementChild.scope = "row";
92+
+ body.append(row);
93+
+ });
94+
+ table.append(body);
95+
+ wrapper.append(table);
96+
+ parent.append(wrapper);
97+
+}
98+
+
99+
+function renderExpandedGameRow(tbody, game) {
100+
+ const row = document.createElement("tr");
101+
+ row.dataset.gameExpandedRow = game.id;
102+
+ row.id = `game-child-${game.id}`;
103+
+ const content = document.createElement("td");
104+
+ content.colSpan = 5;
105+
+ const stack = document.createElement("div");
106+
+ stack.className = "content-stack content-stack--compact";
107+
+ renderGameSummaryChildTable(stack, game);
108+
+ content.append(stack);
109+
+ row.append(content);
110+
+ tbody.append(row);
111+
+}
112+
+
113+
+function renderGameParentRow(tbody, game, activeGame) {
114+
+ const expanded = state.expandedGameId === game.id;
115+
+ const row = document.createElement("tr");
116+
+ row.dataset.gameRow = game.id;
117+
+ if (activeGame?.id === game.id) {
118+
+ row.dataset.gameActive = "true";
119+
+ }
120+
+
121+
+ const nameCell = document.createElement("th");
122+
+ nameCell.scope = "row";
123+
+ nameCell.append(createGameToggleButton(game, expanded));
124+
+ row.append(
125+
+ nameCell,
126+
+ createCell(game.purpose || "Game"),
127+
+ createCell(game.status || "No status"),
128+
+ createCell(game.ownerDisplayName || "No owner"),
129+
+ );
130+
+
131+
+ const actions = document.createElement("td");
132+
+ actions.append(createGameButton(game, activeGame?.id === game.id));
133+
+ row.append(actions);
134+
+ tbody.append(row);
135+
+
136+
+ if (expanded) {
137+
+ renderExpandedGameRow(tbody, game);
138+
+ }
139+
+}
140+
+
141+
function renderProjectInformation(activeGame, currentMember, progress) {
142+
if (!elements.projectRecordsTable) {
143+
return;
144+
@@ -276,25 +364,18 @@ function renderGameList() {
145+
return;
146+
}
147+
148+
- listResult.forEach((game) => {
149+
- const row = document.createElement("article");
150+
- row.className = "callout";
151+
- row.dataset.gameRow = game.id;
152+
-
153+
- const title = document.createElement("h4");
154+
- title.textContent = game.name;
155+
-
156+
- const meta = document.createElement("p");
157+
- meta.className = "eyebrow";
158+
- meta.textContent = `${game.purpose} | ${game.status} | ${game.ownerDisplayName}`;
159+
-
160+
- const isActive = activeGame?.id === game.id;
161+
- const action = createGameButton(game, isActive);
162+
-
163+
- row.append(title, meta, action);
164+
-
165+
- elements.gameList.append(row);
166+
- });
167+
+ const wrapper = document.createElement("div");
168+
+ wrapper.className = "table-wrapper";
169+
+ const table = document.createElement("table");
170+
+ table.className = "data-table data-table--fixed";
171+
+ table.dataset.gameParentTable = "open-games";
172+
+ table.setAttribute("aria-label", "Open Games");
173+
+ 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>";
174+
+ const body = document.createElement("tbody");
175+
+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame));
176+
+ table.append(body);
177+
+ wrapper.append(table);
178+
+ elements.gameList.append(wrapper);
179+
}
180+
181+
function renderMembersTable(activeGame) {
182+
@@ -471,6 +552,13 @@ elements.form?.addEventListener("submit", (event) => {
183+
});
184+
185+
elements.gameList?.addEventListener("click", (event) => {
186+
+ const toggle = event.target.closest("[data-game-toggle]");
187+
+ if (toggle) {
188+
+ state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle;
189+
+ renderWorkspace();
190+
+ return;
191+
+ }
192+
+
193+
const button = event.target.closest("[data-game-open]");
194+
195+
if (!button) {

tests/playwright/tools/GameHubMockRepository.spec.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,32 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
264264
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
265265
await expect(page.locator("[data-game-list]")).toContainText("Collision Demo");
266266
await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo");
267+
await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games");
268+
await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
269+
await expect(page.locator("[data-game-parent-table='open-games'] thead th")).toHaveText([
270+
"Game",
271+
"Purpose",
272+
"Status",
273+
"Owner",
274+
"Actions",
275+
]);
267276
const demoGameRow = page.locator("[data-game-row='demo-game']");
268277
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
278+
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
269279
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/);
270280
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true");
281+
await demoGameRow.locator("[data-game-toggle='demo-game']").click();
282+
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
283+
await expect(page.locator("[data-game-row='demo-game'] + [data-game-expanded-row='demo-game']")).toHaveCount(1);
284+
await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toContainText("Game Summary");
285+
await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary'] tbody tr")).toHaveText([
286+
"ProjectDemo Game",
287+
"PurposeGame",
288+
"StatusUnder Construction",
289+
"OwnerUser 1",
290+
]);
291+
await demoGameRow.locator("[data-game-toggle='demo-game']").click();
292+
await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);
271293

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

0 commit comments

Comments
 (0)