|
1 | 1 | diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs |
2 | | -index 695b0a609..dac65c676 100644 |
| 2 | +index dac65c676..4e617ed35 100644 |
3 | 3 | --- a/tests/playwright/tools/GameHubMockRepository.spec.mjs |
4 | 4 | +++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs |
5 | | -@@ -288,6 +288,22 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { |
6 | | - "StatusUnder Construction", |
7 | | - "OwnerUser 1", |
8 | | - ]); |
9 | | -+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(3); |
10 | | -+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='source-idea'] caption")).toHaveText("Source Idea"); |
11 | | -+ const readinessOutputTable = page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='readiness-output']"); |
12 | | -+ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output"); |
13 | | -+ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]); |
14 | | -+ await expect(readinessOutputTable.locator("tbody tr")).toHaveText([ |
15 | | -+ "Game StatusUnder Construction", |
16 | | -+ "Game ProgressDemo Game identity ready", |
17 | | -+ "Launch ProgressPublish blocked until configuration and required assets are ready", |
18 | | -+ "Current FocusComplete Game Configuration", |
19 | | -+ "Recommended Next ToolGame Configuration", |
20 | | -+ "Game identityComplete", |
21 | | -+ "Game configurationUnder Construction", |
22 | | -+ "Playable buildPlanned", |
23 | | -+ "Publishing reviewPlanned", |
24 | | -+ ]); |
25 | | - await demoGameRow.locator("[data-game-toggle='demo-game']").click(); |
26 | | - await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0); |
27 | | - |
28 | | -diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js |
29 | | -index 67f56ca41..4b21814d7 100644 |
30 | | ---- a/toolbox/game-hub/game-hub.js |
31 | | -+++ b/toolbox/game-hub/game-hub.js |
32 | | -@@ -319,7 +319,53 @@ function renderSourceIdeaChildTable(parent, game) { |
33 | | - parent.append(wrapper); |
34 | | - } |
| 5 | +@@ -335,6 +335,131 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { |
| 6 | + } |
| 7 | + }); |
35 | 8 |
|
36 | | --function renderExpandedGameRow(tbody, game) { |
37 | | -+function renderReadinessOutputChildTable(parent, game, progress, active) { |
38 | | -+ const readiness = active && isRecord(progress) |
39 | | -+ ? progress |
40 | | -+ : { |
41 | | -+ currentFocus: "Open this game to review readiness", |
42 | | -+ gameProgress: `${game.name} identity ready`, |
43 | | -+ gameStatus: game.status || "No status", |
44 | | -+ publishingProgress: "Open this game to review launch progress", |
45 | | -+ recommendedNextTool: "Game Hub", |
46 | | -+ progressChecklist: [], |
47 | | -+ }; |
48 | | -+ const wrapper = document.createElement("div"); |
49 | | -+ wrapper.className = "table-wrapper"; |
50 | | -+ const table = document.createElement("table"); |
51 | | -+ table.className = "data-table data-table--fixed"; |
52 | | -+ table.dataset.gameChildTable = "readiness-output"; |
53 | | -+ table.setAttribute("aria-label", `${game.name} readiness output`); |
54 | | -+ table.innerHTML = "<caption>Readiness Output</caption><thead><tr><th scope=\"col\">Output</th><th scope=\"col\">Status</th></tr></thead>"; |
55 | | -+ const body = document.createElement("tbody"); |
56 | | -+ [ |
57 | | -+ ["Game Status", readiness.gameStatus], |
58 | | -+ ["Game Progress", readiness.gameProgress], |
59 | | -+ ["Launch Progress", readiness.publishingProgress], |
60 | | -+ ["Current Focus", readiness.currentFocus], |
61 | | -+ ["Recommended Next Tool", readiness.recommendedNextTool], |
62 | | -+ ].forEach(([label, value]) => { |
63 | | -+ const row = document.createElement("tr"); |
64 | | -+ row.append(createCell(label, "th"), createCell(value || "Not available")); |
65 | | -+ row.firstElementChild.scope = "row"; |
66 | | -+ body.append(row); |
67 | | -+ }); |
| 9 | ++test("Game Hub validates Open Games parent and child tables", async ({ page }) => { |
| 10 | ++ const sourceLinkedGame = { |
| 11 | ++ id: "lantern-reef", |
| 12 | ++ ownerKey: MOCK_DB_KEYS.users.user1, |
| 13 | ++ name: "Lantern Reef", |
| 14 | ++ purpose: "Game", |
| 15 | ++ status: "Ready for Testing", |
| 16 | ++ ownerDisplayName: "User 1", |
| 17 | ++ members: [ |
| 18 | ++ { |
| 19 | ++ displayName: "User 1", |
| 20 | ++ gameId: "lantern-reef", |
| 21 | ++ permission: "Owner", |
| 22 | ++ role: "Owner", |
| 23 | ++ userKey: MOCK_DB_KEYS.users.user1, |
| 24 | ++ }, |
| 25 | ++ ], |
| 26 | ++ sourceIdea: { |
| 27 | ++ idea: "Lantern Reef", |
| 28 | ++ pitch: "Guide reef keepers through dusk storms.", |
| 29 | ++ notes: [ |
| 30 | ++ "Keep traversal gentle.", |
| 31 | ++ "Use warm lantern art.", |
| 32 | ++ ], |
| 33 | ++ }, |
| 34 | ++ }; |
| 35 | ++ const journeyBuckets = [ |
| 36 | ++ "Idea", |
| 37 | ++ "Design", |
| 38 | ++ "Graphics", |
| 39 | ++ "Audio", |
| 40 | ++ "Objects", |
| 41 | ++ "Worlds", |
| 42 | ++ "Interface", |
| 43 | ++ "Controls", |
| 44 | ++ "Rules", |
| 45 | ++ "Progression", |
| 46 | ++ "Play Test", |
| 47 | ++ "Publish", |
| 48 | ++ "Share", |
| 49 | ++ ]; |
68 | 50 | + |
69 | | -+ const checklist = Array.isArray(readiness.progressChecklist) ? readiness.progressChecklist : []; |
70 | | -+ checklist.forEach((item) => { |
71 | | -+ const row = document.createElement("tr"); |
72 | | -+ row.dataset.readinessChecklistRow = item.label || "Checklist"; |
73 | | -+ row.append(createCell(item.label || "Checklist", "th"), createCell(item.status || "Not available")); |
74 | | -+ row.firstElementChild.scope = "row"; |
75 | | -+ body.append(row); |
| 51 | ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { |
| 52 | ++ await route.fulfill({ |
| 53 | ++ body: JSON.stringify({ |
| 54 | ++ data: { result: sourceLinkedGame }, |
| 55 | ++ ok: true, |
| 56 | ++ rule: "Browser -> Server API -> Data Source", |
| 57 | ++ }), |
| 58 | ++ contentType: "application/json; charset=utf-8", |
| 59 | ++ status: 200, |
| 60 | ++ }); |
| 61 | ++ }); |
| 62 | ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => { |
| 63 | ++ await route.fulfill({ |
| 64 | ++ body: JSON.stringify({ |
| 65 | ++ data: { |
| 66 | ++ result: { |
| 67 | ++ currentFocus: "Review source idea context", |
| 68 | ++ gameProgress: "Lantern Reef identity ready", |
| 69 | ++ gameStatus: "Ready for Testing", |
| 70 | ++ publishingProgress: "Launch review pending", |
| 71 | ++ recommendedNextTool: "Game Journey", |
| 72 | ++ progressChecklist: journeyBuckets.map((label) => ({ label, status: "Planned" })), |
| 73 | ++ }, |
| 74 | ++ }, |
| 75 | ++ ok: true, |
| 76 | ++ rule: "Browser -> Server API -> Data Source", |
| 77 | ++ }), |
| 78 | ++ contentType: "application/json; charset=utf-8", |
| 79 | ++ status: 200, |
| 80 | ++ }); |
76 | 81 | + }); |
| 82 | ++ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { |
| 83 | ++ await route.fulfill({ |
| 84 | ++ body: JSON.stringify({ |
| 85 | ++ data: { result: [sourceLinkedGame] }, |
| 86 | ++ ok: true, |
| 87 | ++ rule: "Browser -> Server API -> Data Source", |
| 88 | ++ }), |
| 89 | ++ contentType: "application/json; charset=utf-8", |
| 90 | ++ status: 200, |
| 91 | ++ }); |
| 92 | ++ }); |
| 93 | ++ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); |
77 | 94 | + |
78 | | -+ table.append(body); |
79 | | -+ wrapper.append(table); |
80 | | -+ parent.append(wrapper); |
81 | | -+} |
| 95 | ++ try { |
| 96 | ++ await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games"); |
| 97 | ++ const parentRows = page.locator("[data-game-parent-table='open-games'] tbody > [data-game-row]"); |
| 98 | ++ await expect(parentRows).toHaveCount(1); |
| 99 | ++ const gameRow = page.locator("[data-game-row='lantern-reef']"); |
| 100 | ++ await expect(gameRow).toContainText("Lantern Reef"); |
| 101 | ++ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "false"); |
82 | 102 | + |
83 | | -+function renderExpandedGameRow(tbody, game, progress, active) { |
84 | | - const row = document.createElement("tr"); |
85 | | - row.dataset.gameExpandedRow = game.id; |
86 | | - row.id = `game-child-${game.id}`; |
87 | | -@@ -329,16 +375,18 @@ function renderExpandedGameRow(tbody, game) { |
88 | | - stack.className = "content-stack content-stack--compact"; |
89 | | - renderGameSummaryChildTable(stack, game); |
90 | | - renderSourceIdeaChildTable(stack, game); |
91 | | -+ renderReadinessOutputChildTable(stack, game, progress, active); |
92 | | - content.append(stack); |
93 | | - row.append(content); |
94 | | - tbody.append(row); |
95 | | - } |
96 | | - |
97 | | --function renderGameParentRow(tbody, game, activeGame) { |
98 | | -+function renderGameParentRow(tbody, game, activeGame, progress) { |
99 | | - const expanded = state.expandedGameId === game.id; |
100 | | -+ const active = activeGame?.id === game.id; |
101 | | - const row = document.createElement("tr"); |
102 | | - row.dataset.gameRow = game.id; |
103 | | -- if (activeGame?.id === game.id) { |
104 | | -+ if (active) { |
105 | | - row.dataset.gameActive = "true"; |
106 | | - } |
107 | | - |
108 | | -@@ -353,12 +401,12 @@ function renderGameParentRow(tbody, game, activeGame) { |
109 | | - ); |
110 | | - |
111 | | - const actions = document.createElement("td"); |
112 | | -- actions.append(createGameButton(game, activeGame?.id === game.id)); |
113 | | -+ actions.append(createGameButton(game, active)); |
114 | | - row.append(actions); |
115 | | - tbody.append(row); |
116 | | - |
117 | | - if (expanded) { |
118 | | -- renderExpandedGameRow(tbody, game); |
119 | | -+ renderExpandedGameRow(tbody, game, progress, active); |
120 | | - } |
121 | | - } |
122 | | - |
123 | | -@@ -389,7 +437,7 @@ function renderProjectInformation(activeGame, currentMember, progress) { |
124 | | - : "Project Information loaded. Sign in to save changes."); |
125 | | - } |
126 | | - |
127 | | --function renderGameList() { |
128 | | -+function renderGameList(progress) { |
129 | | - if (!elements.gameList) { |
130 | | - return; |
131 | | - } |
132 | | -@@ -421,7 +469,7 @@ function renderGameList() { |
133 | | - table.setAttribute("aria-label", "Open Games"); |
134 | | - 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>"; |
135 | | - const body = document.createElement("tbody"); |
136 | | -- listResult.forEach((game) => renderGameParentRow(body, game, activeGame)); |
137 | | -+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress)); |
138 | | - table.append(body); |
139 | | - wrapper.append(table); |
140 | | - elements.gameList.append(wrapper); |
141 | | -@@ -558,7 +606,7 @@ function renderWorkspace() { |
142 | | - } |
143 | | - } |
144 | | - |
145 | | -- renderGameList(); |
146 | | -+ renderGameList(progress); |
147 | | - renderMembersTable(activeGame); |
148 | | - renderTableCounts(); |
149 | | - renderChecklist(progress); |
| 103 | ++ await gameRow.locator("[data-game-toggle='lantern-reef']").click(); |
| 104 | ++ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "true"); |
| 105 | ++ const expandedRow = page.locator("[data-game-expanded-row='lantern-reef']"); |
| 106 | ++ await expect(expandedRow).toHaveCount(1); |
| 107 | ++ await expect(expandedRow.locator("[data-game-child-table]")).toHaveCount(3); |
| 108 | ++ |
| 109 | ++ const sourceIdeaTable = expandedRow.locator("[data-game-child-table='source-idea']"); |
| 110 | ++ await expect(sourceIdeaTable.locator("caption")).toHaveText("Source Idea"); |
| 111 | ++ await expect(sourceIdeaTable.locator("tbody tr")).toHaveText([ |
| 112 | ++ "IdeaLantern Reef", |
| 113 | ++ "PitchGuide reef keepers through dusk storms.", |
| 114 | ++ "Note 1Keep traversal gentle.", |
| 115 | ++ "Note 2Use warm lantern art.", |
| 116 | ++ ]); |
| 117 | ++ await expect(sourceIdeaTable.locator("button, input, textarea, select, [contenteditable='true'], [role='button']")).toHaveCount(0); |
| 118 | ++ await expect(sourceIdeaTable).not.toContainText(/Edit|Delete|Current Focus|Recommended Next Tool/); |
| 119 | ++ |
| 120 | ++ const readinessOutputTable = expandedRow.locator("[data-game-child-table='readiness-output']"); |
| 121 | ++ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output"); |
| 122 | ++ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]); |
| 123 | ++ await expect(readinessOutputTable).not.toContainText(/Guide reef keepers|Keep traversal gentle|Use warm lantern art/); |
| 124 | ++ await expect(readinessOutputTable.locator("[data-readiness-checklist-row] th")).toHaveText(journeyBuckets); |
| 125 | ++ |
| 126 | ++ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveCount(0); |
| 127 | ++ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveCount(0); |
| 128 | ++ await expectNoPageFailures(failures); |
| 129 | ++ } finally { |
| 130 | ++ await failures.server.close(); |
| 131 | ++ } |
| 132 | ++}); |
| 133 | ++ |
| 134 | + test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }) => { |
| 135 | + const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); |
0 commit comments