|
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) { |
0 commit comments