From 57548d3b0872d94921b47e9adfaf91da8a7bfd56 Mon Sep 17 00:00:00 2001 From: Delta Team Date: Wed, 24 Jun 2026 12:45:44 -0400 Subject: [PATCH] PR_26175_ALFA_016: consolidate Alfa parent child table runtime changes --- ...6-alfa-parent-child-table-consolidation.md | 89 ++ .../dev/reports/codex_changed_files.txt | 13 +- docs_build/dev/reports/codex_review.diff | 869 +++++------------- .../tools/GameHubMockRepository.spec.mjs | 57 +- .../tools/IdeaBoardTableNotes.spec.mjs | 16 +- toolbox/game-hub/game-hub.js | 39 +- 6 files changed, 410 insertions(+), 673 deletions(-) create mode 100644 docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md diff --git a/docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md b/docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md new file mode 100644 index 000000000..189879abc --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md @@ -0,0 +1,89 @@ +# PR_26175_ALFA_016 - Alfa Parent Child Table Consolidation + +Branch: `PR_26175_ALFA_016-alfa-parent-child-table-consolidation` +Team: Alfa +Source PRs represented: #103, #104, #105 +Playwright impacted: YES + +## Executive Summary + +This PR consolidates the remaining current-main-safe Game Hub parent/child table runtime work from PRs #103, #104, and #105. + +Current `main` already contained the Source Idea and Readiness Output child table behavior from the earlier stack. This branch preserves those current-main implementations and adds the remaining parent/child layout pieces: + +- marks the Games table as the Open Games parent table, +- adds an Open Games table caption, +- adds a Game Summary child table under expanded game rows, +- keeps Source Idea as a separate child table when source idea details exist, +- keeps Readiness Output as a separate child table. + +No status bar files were modified. + +## Runtime Files Changed + +| File | Change | +| --- | --- | +| `toolbox/game-hub/game-hub.js` | Adds Open Games parent-table identity/caption and a Game Summary child table. Preserves Source Idea and Readiness Output as separate child tables. | + +No `toolbox/game-hub/index.html` or `assets/theme-v2/css/tables.css` change was required. + +## Tests Updated + +| File | Change | +| --- | --- | +| `tests/playwright/tools/GameHubMockRepository.spec.mjs` | Updates Game Hub table assertions for Open Games parent-table identity, Game Summary child table, Source Idea child table, and Readiness Output child table. | +| `tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` | Updates the Idea Board to Game Hub flow assertions for the expanded Game Hub child table sequence. | + +## Reports Generated + +| File | Purpose | +| --- | --- | +| `docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md` | This implementation and validation report. | +| `docs_build/dev/reports/codex_changed_files.txt` | Changed file inventory. | +| `docs_build/dev/reports/codex_review.diff` | Review diff. | +| `tmp/PR_26175_ALFA_016-alfa-parent-child-table-consolidation_delta.zip` | Repo-structured delta ZIP. | + +## Branch Validation + +| Check | Result | Notes | +| --- | --- | --- | +| Started from `main` | PASS | `main` confirmed before branch creation. | +| Pulled latest `origin/main` | PASS | `git pull --ff-only` fast-forwarded before branch creation. | +| Worktree clean before branch | PASS | No local changes before branch creation. | +| Local/origin sync before branch | PASS | Sync confirmed as `0 0`. | +| Scope limited to Alfa Game Hub parent/child tables | PASS | Runtime change is limited to Game Hub table rendering. | +| Status bar untouched | PASS | No diff in status bar JS/CSS/tests. | + +## Requirement Checklist + +| Requirement | Result | Notes | +| --- | --- | --- | +| Read all Project Instructions | PASS | Active Project Instructions and history snapshots were read. | +| Implement remaining current-main-safe runtime changes from #103, #104, #105 | PASS | Added the remaining parent table identity and Game Summary child table while preserving current Source Idea and Readiness Output behavior. | +| Focus on Game Hub parent/child table layout | PASS | Open Games is now explicitly marked/captioned as the parent table. | +| Focus on Source Idea child table | PASS | Source Idea remains a dedicated child table for source-linked games. | +| Focus on Readiness Output child table | PASS | Readiness Output remains a dedicated child table. | +| Do not create report-only PR | PASS | Runtime and test files changed. | +| Do not change status bar work | PASS | Status bar files have no diff. | +| Do not install Chromium | PASS | Chromium install was not attempted. | +| If Playwright browser is missing, document blocked and continue | PASS | Chromium is missing and Playwright validation is documented as blocked. | +| Required reports and ZIP | PASS | Required reports are included and ZIP was created under `tmp/`. | + +## Validation Lane Report + +| Validation | Result | Command / Notes | +| --- | --- | --- | +| Runtime syntax check | PASS | `node --check toolbox/game-hub/game-hub.js` | +| Game Hub spec syntax check | PASS | `node --check tests/playwright/tools/GameHubMockRepository.spec.mjs` | +| Idea Board spec syntax check | PASS | `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` | +| Diff whitespace check | PASS | `git diff --check`; Git reported line-ending warnings only for touched specs. | +| Inline style guard | PASS | `rg -n "<[s]tyle|[s]tyle=" toolbox/game-hub/game-hub.js toolbox/game-hub/index.html tests/playwright/tools/GameHubMockRepository.spec.mjs tests/playwright/tools/IdeaBoardTableNotes.spec.mjs assets/theme-v2/css/tables.css` returned no matches. | +| Status bar scope check | PASS | `git diff -- assets/theme-v2/css/status.css assets/theme-v2/js/toolbox-status-bar.js tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` returned no diff. | +| Playwright targeted browser lane | BLOCKED | Local Chromium is missing at `C:\Users\davidq\AppData\Local\ms-playwright\chromium-1217\chrome-win64\chrome.exe`. Per instruction, Chromium was not installed. | + +## Manual Validation Notes + +- Manual browser validation was not performed because Playwright Chromium is missing locally. +- Current-main Source Idea behavior was preserved: source-linked games render Source Idea as a child table; games without source details do not show an empty Source Idea child table. +- Readiness Output remains separate from Source Idea. +- The Game Summary child table intentionally includes Project, Purpose, and Status only to preserve the current simplified Game Hub table model and avoid reintroducing owner/role columns. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 1c6777b35..bda7aa1b8 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,9 +1,6 @@ -docs_build/dev/BUILD_PR.md -assets/theme-v2/js/toolbox-status-bar.js -assets/theme-v2/css/status.css -tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md -docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md -docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md -docs_build/dev/reports/codex_review.diff +toolbox/game-hub/game-hub.js +tests/playwright/tools/GameHubMockRepository.spec.mjs +tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md docs_build/dev/reports/codex_changed_files.txt +docs_build/dev/reports/codex_review.diff diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 548c6e30e..7b99a3b40 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,643 +1,244 @@ -diff --git a/assets/theme-v2/css/status.css b/assets/theme-v2/css/status.css -index 6acb4555b..4ca9cb266 100644 ---- a/assets/theme-v2/css/status.css -+++ b/assets/theme-v2/css/status.css -@@ -38,6 +38,7 @@ - .toolbox-status-bar { - --toolbox-status-bar-height: var(--space-52); - --toolbox-status-game-max: 220px; -+ --toolbox-status-progress-max: 34vw; - width: 100%; - min-block-size: var(--toolbox-status-bar-height); - border-block: var(--border-standard); -@@ -50,7 +51,7 @@ - margin: var(--space-0) auto; - padding: var(--space-10) var(--space-0); - display: grid; -- grid-template-columns: minmax(var(--space-0), max-content) minmax(var(--space-0), 1fr); -+ grid-template-columns: minmax(var(--space-0), max-content) minmax(var(--space-0), 1fr) minmax(var(--space-0), max-content); - gap: var(--space-16); - align-items: center - } -@@ -90,6 +91,19 @@ - overflow-wrap: anywhere - } - -+.toolbox-status-bar__progress { -+ min-width: var(--space-0); -+ max-width: var(--toolbox-status-progress-max); -+ margin: var(--space-0); -+ color: var(--muted); -+ font-size: var(--font-size-sm); -+ line-height: var(--line-height-tight); -+ overflow: hidden; -+ text-align: right; -+ text-overflow: ellipsis; -+ white-space: nowrap -+} -+ - .toolbox-status-bar[data-selected-game-state="active"] { - border-color: color-mix(in srgb, var(--green) 52%, var(--line)) - } -@@ -249,16 +263,21 @@ body.tool-focus-mode .tool-center-panel { - @media (max-width: 720px) { - .toolbox-status-bar__inner { - width: var(--container-width); -- grid-template-columns: minmax(var(--space-0), max-content) minmax(var(--space-0), 1fr); -+ grid-template-columns: minmax(var(--space-0), max-content) minmax(var(--space-0), 1fr) minmax(var(--space-0), max-content); - gap: var(--space-10); - text-align: center - } - - .toolbox-status-bar__game-name { -- max-width: 34vw -+ max-width: 24vw - } - -- .toolbox-status-bar__message { -+ .toolbox-status-bar__message, -+ .toolbox-status-bar__progress { - font-size: var(--font-size-sm) - } -+ -+ .toolbox-status-bar__progress { -+ max-width: 30vw -+ } - } -diff --git a/assets/theme-v2/js/toolbox-status-bar.js b/assets/theme-v2/js/toolbox-status-bar.js -index 24c935311..6adc17290 100644 ---- a/assets/theme-v2/js/toolbox-status-bar.js -+++ b/assets/theme-v2/js/toolbox-status-bar.js -@@ -1,7 +1,39 @@ -+import { readGameJourneyCompletionMetrics } from "/src/api/game-journey-completion-api-client.js"; - import { createServerRepositoryClient } from "/src/api/server-api-client.js"; -+import { getToolBySlug } from "/src/shared/toolbox/tool-metadata-inventory.js"; - - const EXCLUDED_SELECTED_GAME_TOOLS = new Set(["idea-board"]); - const STATUS_BAR_SELECTOR = "[data-toolbox-status-bar]"; -+const TOOL_PROGRESS_BUCKET_BY_SLUG = Object.freeze({ -+ "achievements": "Progression", -+ "assets": "Graphics", -+ "audio": "Audio", -+ "audio-effects": "Audio", -+ "characters": "Objects", -+ "colors": "Graphics", -+ "community": "Share", -+ "controls": "Controls", -+ "events": "Rules", -+ "game-configuration": "Create", -+ "game-design": "Design", -+ "game-hub": "Create", -+ "game-testing": "Play Test", -+ "hitboxes": "Objects", -+ "idea-board": "Idea", -+ "input-mapping-v2": "Controls", -+ "marketplace": "Share", -+ "messages": "Interface", -+ "music": "Audio", -+ "objects": "Objects", -+ "publish": "Publish", -+ "ratings": "Share", -+ "sprites": "Graphics", -+ "tags": "Progression", -+ "text-to-speech": "Audio", -+ "videos": "Graphics", -+ "voices": "Audio", -+ "worlds": "Worlds", -+}); - - let repository = null; - let messageObserver = null; -@@ -101,7 +133,10 @@ function createStatusBar() { - message.setAttribute("role", "status"); - center.append(message); - -- inner.append(game, center); -+ const progress = createText("p", "toolbox-status-bar__progress", "toolboxStatusProgress"); -+ progress.setAttribute("aria-label", "Tool and journey progress"); -+ -+ inner.append(game, center, progress); - bar.append(inner); - return bar; - } -@@ -233,6 +268,107 @@ function classifyToolContext(messageText, state, required) { - return { kind: "action" }; - } - -+function normalizeTextKey(value) { -+ return String(value || "") -+ .toLowerCase() -+ .replace(/[^a-z0-9]+/g, ""); -+} -+ -+function formatToolSlug(slug) { -+ return String(slug || "Tool") -+ .split(/[-_\s]+/) -+ .filter(Boolean) -+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) -+ .join(" ") || "Tool"; -+} -+ -+function currentToolContext() { -+ const slug = toolSlugFromPath(mountOptions.pagePath); -+ const tool = getToolBySlug(slug); -+ return { -+ label: tool?.shortLabel || tool?.displayName || tool?.name || formatToolSlug(slug), -+ slug, -+ }; -+} -+ -+function normalizeProgressRecord(record) { -+ const total = Math.max(0, Number(record?.plannedCount) || 0); -+ const complete = Math.max(0, Math.min(Number(record?.completedCount) || 0, total)); -+ const percent = Number.isFinite(Number(record?.percentComplete)) -+ ? Math.max(0, Math.min(Number(record.percentComplete), 100)) -+ : total > 0 -+ ? Math.round((complete / total) * 100) -+ : 0; -+ return { -+ complete, -+ percent, -+ total, -+ }; -+} -+ -+function formatProgressRecord(label, record) { -+ const progress = normalizeProgressRecord(record); -+ return `${label} ${progress.complete}/${progress.total} (${progress.percent}%)`; -+} -+ -+function findMetricForCurrentTool(snapshot, toolContext) { -+ if (toolContext.slug === "game-journey") { -+ return snapshot; -+ } -+ -+ const records = Array.isArray(snapshot?.records) ? snapshot.records : []; -+ const explicitBucketName = TOOL_PROGRESS_BUCKET_BY_SLUG[toolContext.slug]; -+ const explicitBucket = normalizeTextKey(explicitBucketName); -+ const toolLabel = normalizeTextKey(toolContext.label); -+ const toolSlug = normalizeTextKey(toolContext.slug); -+ -+ return records.find((metric) => { -+ const bucketName = normalizeTextKey(metric?.bucketName); -+ return bucketName && ( -+ bucketName === explicitBucket || -+ bucketName === toolLabel || -+ bucketName === toolSlug -+ ); -+ }) || null; -+} -+ -+function resolveProgressContext() { -+ const toolContext = currentToolContext(); -+ const snapshot = readGameJourneyCompletionMetrics(); -+ const currentMetric = findMetricForCurrentTool(snapshot, toolContext); -+ const journeyText = formatProgressRecord("Journey", snapshot); -+ -+ if (!currentMetric) { -+ return { -+ state: "unmapped", -+ text: `${toolContext.label} progress unavailable | ${journeyText}`, -+ }; -+ } -+ -+ return { -+ state: "active", -+ text: `${formatProgressRecord(toolContext.label, currentMetric)} | ${journeyText}`, -+ }; -+} -+ -+function renderProgressContext(bar) { -+ const progress = bar.querySelector("[data-toolbox-status-progress]"); -+ if (!progress) { -+ return; -+ } -+ -+ try { -+ const context = resolveProgressContext(); -+ bar.dataset.toolboxProgressState = context.state; -+ progress.textContent = context.text; -+ progress.removeAttribute("title"); -+ } catch (error) { -+ bar.dataset.toolboxProgressState = "error"; -+ progress.textContent = "Progress unavailable"; -+ progress.removeAttribute("title"); -+ } -+} -+ - function renderSelectedGame(bar, selectedGame, state, messageText) { - const required = pageRequiresSelectedGame(); - const name = bar.querySelector("[data-toolbox-selected-game-name]"); -@@ -247,6 +383,7 @@ function renderSelectedGame(bar, selectedGame, state, messageText) { - bar.dataset.selectedGameState = state; - bar.dataset.selectedGameRequired = String(required); - bar.dataset.toolboxStatusContextKind = context.kind; -+ renderProgressContext(bar); - - if (selectedGame) { - name.textContent = selectedGame.name; -diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -index 0778a1f1e..f4bc1e8e4 100644 ---- a/docs_build/dev/BUILD_PR.md -+++ b/docs_build/dev/BUILD_PR.md -@@ -1,50 +1,54 @@ --# PR_26175_ALFA_009-status-bar-single-row-rebuild -+# PR_26175_ALFA_011-status-bar-journey-progress-context - - ## Purpose --Rebuild the shared toolbox status bar on current `main` so it is a single-row creator context bar. -+Add right-anchored progress context to the shared toolbox status bar using the existing Game Journey completion metrics/API pipeline. - - ## Source Of Truth --This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_009-status-bar-single-row-rebuild`. -+This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_011-status-bar-journey-progress-context`. - - ## Exact Scope --- Display only the selected Game Hub game name on the left side of the toolbox status bar. --- Display only the current status message in the center of the toolbox status bar. --- Remove visible status bar labels: `Selected Game Name`, `Selected Game Purpose`, `Save State`, `Tool Action`, `Warning`, and `Error`. --- Remove selected-game purpose from the visible status bar. --- Preserve normal status bar placement above the footer. --- Remove extra status bar/footer spacing so the shared footer top padding resolves to `0px`. --- Preserve fullscreen/tool display mode bottom anchoring. --- Ensure fullscreen center scrollable content stops above the fixed status bar. --- Ensure fullscreen tool content is not hidden behind the status bar. --- Preserve Idea Board selected-game filtering exclusion. --- Preserve Game Hub as selected-game owner through the existing repository contract. -+- Preserve the ALFA_009 single-row toolbox status bar behavior: -+ - left side displays only the selected Game Hub game name. -+ - center displays only the current status message. -+- Add right-anchored progress text in this format: -+ - `{CurrentTool} {complete}/{total} ({percent}%) | Journey {complete}/{total} ({percent}%)` -+- Use existing Game Journey completion metrics/API pipeline for Journey totals. -+- Derive current-tool progress from the existing completion metrics record that matches the current toolbox tool/section. -+- Do not add new storage. -+- Do not use browser-owned authoritative progress data. -+- Preserve fullscreen bottom anchoring and existing fullscreen content bottom reserve. -+- Preserve normal placement above the footer. - - Use shared Theme V2 CSS/classes only. --- Update targeted Playwright coverage for the single-row status bar, footer spacing, and fullscreen bottom reserve. -+- Update targeted Playwright coverage for the right-anchored progress text and existing left/center behavior. - - ## Exact Targets - - `docs_build/dev/BUILD_PR.md` - - `assets/theme-v2/js/toolbox-status-bar.js` - - `assets/theme-v2/css/status.css` --- `assets/theme-v2/css/layout.css` - - `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` --- `docs_build/dev/reports/PR_26175_ALFA_009-status-bar-single-row-rebuild_report.md` --- `docs_build/dev/reports/PR_26175_ALFA_009-status-bar-single-row-rebuild_validation-lane.md` --- `docs_build/dev/reports/PR_26175_ALFA_009-status-bar-single-row-rebuild_requirements-checklist.md` -+- `docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md` -+- `docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md` -+- `docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md` - - `docs_build/dev/reports/codex_review.diff` - - `docs_build/dev/reports/codex_changed_files.txt` - -+## Evidence Sources -+- `assets/js/shared/game-journey-api-client.js` -+- `src/api/game-journey-completion-api-client.js` -+- `src/dev-runtime/server/local-api-router.mjs` -+- `src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs` -+ - ## Out Of Scope --- No merge of PR #120. --- No reuse of stale ALFA_003 branch. -+- No Game Journey API/service/repository contract changes. -+- No new persistence/storage. -+- No browser-owned product data as source of truth. -+- No silent fallback data. - - No environment/server details in the status bar. - - No selected game purpose in the visible status bar. - - No visible status category labels in the status bar. --- No status action links in the visible status bar. - - No large banners. - - No modal messages or modal-style status messages. - - No row highlights. --- No API/service/repository contract changes. --- No browser-owned product data as source of truth. - - No inline styles, style blocks, or page-local CSS. - - No engine core changes. - - No `start_of_day` folder changes. -@@ -59,12 +63,12 @@ npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs - Also verify changed source does not introduce inline styles or style blocks: - - ```powershell --rg -n "<[s]tyle|[s]tyle=" assets/theme-v2/js/toolbox-status-bar.js assets/theme-v2/css/status.css assets/theme-v2/css/layout.css tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -+rg -n "<[s]tyle|[s]tyle=" assets/theme-v2/js/toolbox-status-bar.js assets/theme-v2/css/status.css tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs - ``` - - ## Artifact - Create repo-structured delta ZIP: - - ```text --tmp/PR_26175_ALFA_009-status-bar-single-row-rebuild_delta.zip -+tmp/PR_26175_ALFA_011-status-bar-journey-progress-context_delta.zip - ``` -diff --git a/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md b/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md -new file mode 100644 -index 000000000..f737efdd4 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md -@@ -0,0 +1,38 @@ -+# PR_26175_ALFA_011-status-bar-journey-progress-context Report -+ -+## Status -+PASS -+ -+Added right-anchored progress context to the shared toolbox status bar while preserving the ALFA_009 left and center behavior. -+ -+## Changes -+- `assets/theme-v2/js/toolbox-status-bar.js` -+ - Imports the existing Game Journey completion metrics API client at line 1. -+ - Adds current-tool-to-Journey-bucket matching at lines 7-35. -+ - Adds the right progress node at line 136. -+ - Formats progress as `{CurrentTool} {complete}/{total} ({percent}%) | Journey {complete}/{total} ({percent}%)` at lines 309-350. -+ - Renders visible unavailable progress state without raw environment/server details at lines 354-370. -+- `assets/theme-v2/css/status.css` -+ - Adds a three-column shared status bar grid at line 54. -+ - Adds right-aligned single-line progress styling at lines 94-106. -+ - Preserves existing fullscreen anchoring and bottom reserve rules at lines 119-150. -+- `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` -+ - Routes `/api/game-journey/completion-metrics` through the existing server API response shape at lines 76-83. -+ - Verifies Game Design progress at lines 239 and 253. -+ - Verifies the requested Objects example at lines 275 and 279. -+ - Verifies right anchoring at lines 260 and 280. -+ - Verifies Game Hub, missing-game, Idea Board, and fullscreen behavior at lines 304, 328, 353, 371, and 398. -+ -+## Contract Notes -+- No storage was added. -+- No API/service/repository contract was changed. -+- Progress data is read from the existing Game Journey completion metrics API pipeline. -+- Browser state is not used as authoritative progress data. -+- Existing selected-game ownership and left/center status bar behavior are preserved. -+ -+## Validation -+- PASS: `npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1` -+- PASS: `rg -n "<[s]tyle|[s]tyle=" assets/theme-v2/js/toolbox-status-bar.js assets/theme-v2/css/status.css tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` -+ -+## Artifact -+- `tmp/PR_26175_ALFA_011-status-bar-journey-progress-context_delta.zip` -diff --git a/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md b/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md -new file mode 100644 -index 000000000..94c18da59 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md -@@ -0,0 +1,17 @@ -+# PR_26175_ALFA_011 Requirements Checklist -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Implement right-anchored progress in the shared toolbox status bar. | PASS | `assets/theme-v2/js/toolbox-status-bar.js:136`, `assets/theme-v2/css/status.css:94`. | -+| Use format `{CurrentTool} {complete}/{total} ({percent}%) | Journey {complete}/{total} ({percent}%)`. | PASS | `assets/theme-v2/js/toolbox-status-bar.js:309` and `:350`; tests at `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs:239`, `:275`. | -+| Use existing Game Journey completion metrics/API pipeline. | PASS | `assets/theme-v2/js/toolbox-status-bar.js:1`, `:337`; tests route `/api/game-journey/completion-metrics` at `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs:76`. | -+| Do not add new storage. | PASS | No persistence files changed. | -+| Do not use browser-owned authoritative progress data. | PASS | Progress is read from `readGameJourneyCompletionMetrics()` and not local/session storage. | -+| Keep existing left status bar behavior from ALFA_009. | PASS | Selected game assertions remain at `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs:237`, `:302`, `:391`. | -+| Keep existing center status bar behavior from ALFA_009. | PASS | Message assertions remain at `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs:238`, `:303`, `:370`, `:397`. | -+| Preserve fullscreen bottom anchoring. | PASS | Existing CSS rules remain at `assets/theme-v2/css/status.css:119`; test coverage at `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs:315`. | -+| Preserve normal placement above footer. | PASS | Existing status bar placement test remains at `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs:224`. | -+| No inline styles, style blocks, or page-local CSS. | PASS | Style scan returned no matches. | -+| Use Theme V2 shared CSS/classes. | PASS | Styling changes are limited to `assets/theme-v2/css/status.css`. | -+| Update targeted Playwright coverage. | PASS | `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` now validates right progress text and anchoring. | -+| Produce required reports and repo-structured ZIP. | PASS | PR reports added and ZIP target is `tmp/PR_26175_ALFA_011-status-bar-journey-progress-context_delta.zip`. | -diff --git a/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md b/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md -new file mode 100644 -index 000000000..daa775695 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md -@@ -0,0 +1,38 @@ -+# PR_26175_ALFA_011 Validation Lane -+ -+## Playwright -+Command: -+ -+```powershell -+npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1 -+``` -+ -+Result: -+ -+```text -+Running 7 tests using 1 worker -+7 passed -+``` -+ -+Covered: -+- Existing selected Game Hub game appears on the left. -+- Existing center status message behavior is preserved. -+- Right progress text renders in the required format. -+- Objects example renders as `Objects 12/25 (48%) | Journey 12/125 (10%)`. -+- Game Hub save state keeps center status behavior and right progress. -+- Fullscreen/tool display mode keeps the fixed bottom status bar and content reserve. -+- Game Hub owner selection updates global status context. -+- Missing-game prompt remains creator-safe. -+- Idea Board remains excluded from selected-game filtering. -+ -+## Style And Scope -+Command: -+ -+```powershell -+rg -n "<[s]tyle|[s]tyle=" assets/theme-v2/js/toolbox-status-bar.js assets/theme-v2/css/status.css tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -+``` -+ -+Result: PASS. The command returned no matches. -+ -+## Generated Files -+Playwright updated generated coverage reports during validation. They were restored to `HEAD` because they are outside this PR's exact target list. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index 4aa64ab71..1c6777b35 100644 +index 1c6777b35..bda7aa1b8 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,17 +1,9 @@ --docs_build/dev/ProjectInstructions/PROJECT_INSTRUCTIONS.md --docs_build/dev/ProjectInstructions/TEAM_START_COMMANDS.md --docs_build/dev/ProjectInstructions/addendums/multi_team.md --docs_build/dev/ProjectInstructions/addendums/pr_workflow.md --docs_build/dev/ProjectInstructions/addendums/team_release_readiness.md --docs_build/dev/ProjectInstructions/addendums/team_start_and_release.md --docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md --docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md --docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md --docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md --docs_build/dev/reports/PR_26175_OWNER_046-pr-targeted-review-packets.md --docs_build/dev/reports/PR_REVIEW_003.md --docs_build/dev/reports/PR_REVIEW_050.md --docs_build/dev/reports/PR_REVIEW_051.md --docs_build/dev/reports/PR_REVIEW_118.md --docs_build/dev/reports/codex_changed_files.txt -+docs_build/dev/BUILD_PR.md -+assets/theme-v2/js/toolbox-status-bar.js -+assets/theme-v2/css/status.css -+tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -+docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md -+docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md -+docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md - docs_build/dev/reports/codex_review.diff -+docs_build/dev/reports/codex_changed_files.txt -diff --git a/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs b/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -index 1a40d61c5..e4a940bad 100644 ---- a/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -+++ b/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -@@ -73,6 +73,15 @@ async function openRepoPage(page, pathName, options = {}) { - }); - }); +@@ -1,9 +1,6 @@ +-docs_build/dev/BUILD_PR.md +-assets/theme-v2/js/toolbox-status-bar.js +-assets/theme-v2/css/status.css +-tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs +-docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md +-docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md +-docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md +-docs_build/dev/reports/codex_review.diff ++toolbox/game-hub/game-hub.js ++tests/playwright/tools/GameHubMockRepository.spec.mjs ++tests/playwright/tools/IdeaBoardTableNotes.spec.mjs ++docs_build/dev/reports/PR_26175_ALFA_016-alfa-parent-child-table-consolidation.md + docs_build/dev/reports/codex_changed_files.txt ++docs_build/dev/reports/codex_review.diff +diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs +index 29e11c37a..2baf13cd0 100644 +--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs ++++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs +@@ -292,9 +292,9 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + 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("summary").filter({ hasText: /^Open Games$/ })).toHaveCount(0); +- await expect(page.locator("[data-game-parent-table='open-games']")).toHaveCount(0); +- await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Games"); +- await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveCount(0); ++ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games"); ++ await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Open Games"); ++ await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveText("Open Games"); + await expect(page.locator("[data-game-rows-table='true'] thead th")).toHaveText([ + "Game", + "Purpose", +@@ -354,12 +354,20 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + await demoGameRow.locator("[data-game-toggle='demo-game']").click(); + await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true"); + const demoChildRows = page.locator("[data-game-expanded-row='demo-game']"); +- await expect(demoChildRows).toHaveCount(1); +- await expect(demoChildRows.nth(0)).toHaveAttribute("data-game-child-row", "readiness-output"); +- await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toHaveCount(0); +- await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(1); ++ await expect(demoChildRows).toHaveCount(2); ++ await expect(demoChildRows.nth(0)).toHaveAttribute("data-game-child-row", "summary"); ++ await expect(demoChildRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); ++ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(2); ++ const summaryTable = demoChildRows.nth(0).locator("[data-game-child-table='summary']"); ++ await expect(summaryTable.locator("caption")).toHaveText("Game Summary"); ++ await expect(summaryTable.locator("thead th")).toHaveText(["Field", "Value"]); ++ await expect(summaryTable.locator("tbody tr")).toHaveText([ ++ "ProjectDemo Game", ++ "PurposeGame", ++ "StatusUnder Construction", ++ ]); + await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='source-idea']")).toHaveCount(0); +- const readinessOutputTable = demoChildRows.nth(0).locator("[data-game-child-table='readiness-output']"); ++ const readinessOutputTable = demoChildRows.nth(1).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.locator("tbody tr")).toHaveText([ +@@ -441,8 +449,9 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).not.toHaveAttribute("aria-current", "true"); + await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1); + await expect(page.locator("[data-game-toggle][data-game-active='true']")).toHaveCount(1); +- await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveCount(1); +- await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveAttribute("data-game-child-row", "readiness-output"); ++ await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveCount(2); ++ await expect(page.locator("[data-game-expanded-row='launch-test-game-1']").nth(0)).toHaveAttribute("data-game-child-row", "summary"); ++ await expect(page.locator("[data-game-expanded-row='launch-test-game-1']").nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); + await expect(page.locator("[data-game-expanded-row='archive-game-2']")).toHaveCount(0); + await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveClass("btn btn--compact primary"); + await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Edit Launch Test Game" })).not.toHaveClass(/primary/); +@@ -547,9 +556,9 @@ test("Game Hub validates game parent rows and child tables", async ({ page }) => + + try { + await expect(page.locator("summary").filter({ hasText: /^Open Games$/ })).toHaveCount(0); +- await expect(page.locator("[data-game-parent-table='open-games']")).toHaveCount(0); +- await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Games"); +- await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveCount(0); ++ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games"); ++ await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Open Games"); ++ await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveText("Open Games"); + const parentRows = page.locator("[data-game-rows-table='true'] tbody > [data-game-row]"); + await expect(parentRows).toHaveCount(1); + const gameRow = page.locator("[data-game-row='lantern-reef']"); +@@ -559,13 +568,20 @@ test("Game Hub validates game parent rows and child tables", async ({ page }) => + await gameRow.locator("[data-game-toggle='lantern-reef']").click(); + await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "true"); + const expandedRows = page.locator("[data-game-expanded-row='lantern-reef']"); +- await expect(expandedRows).toHaveCount(2); +- await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "source-idea"); +- await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); +- await expect(expandedRows.locator("[data-game-child-table]")).toHaveCount(2); +- await expect(expandedRows.locator("[data-game-child-table='summary']")).toHaveCount(0); ++ await expect(expandedRows).toHaveCount(3); ++ await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "summary"); ++ await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "source-idea"); ++ await expect(expandedRows.nth(2)).toHaveAttribute("data-game-child-row", "readiness-output"); ++ await expect(expandedRows.locator("[data-game-child-table]")).toHaveCount(3); ++ const summaryTable = expandedRows.nth(0).locator("[data-game-child-table='summary']"); ++ await expect(summaryTable.locator("caption")).toHaveText("Game Summary"); ++ await expect(summaryTable.locator("tbody tr")).toHaveText([ ++ "ProjectLantern Reef", ++ "PurposeGame", ++ "StatusPlanning", ++ ]); + +- const sourceIdeaTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); ++ const sourceIdeaTable = expandedRows.nth(1).locator("[data-game-child-table='source-idea']"); + await expect(sourceIdeaTable.locator("caption")).toHaveText("Source Idea"); + await expect(sourceIdeaTable.locator("tbody tr")).toHaveText([ + "IdeaLantern Reef", +@@ -576,7 +592,7 @@ test("Game Hub validates game parent rows and child tables", async ({ page }) => + 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 = expandedRows.nth(1).locator("[data-game-child-table='readiness-output']"); ++ const readinessOutputTable = expandedRows.nth(2).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/); +@@ -678,6 +694,7 @@ test("Game Hub shows a creator-safe empty state when no projects exist", async ( + try { + await expect(page.locator("[data-active-game-name]")).toHaveCount(0); + await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Add a game to start building."); ++ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games"); + await expect(page.locator("[data-game-rows-table='true'] thead th")).toHaveText([ + "Game", + "Purpose", +diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +index 61d8fdc68..01a730513 100644 +--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs ++++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +@@ -400,10 +400,12 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(activeGameToggle).toHaveText("Lantern Reef"); + await activeGameToggle.click(); + let expandedRows = page.locator("[data-game-expanded-row]"); +- await expect(expandedRows).toHaveCount(2); +- await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "source-idea"); +- await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); +- let sourceIdeaChildTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); ++ await expect(expandedRows).toHaveCount(3); ++ await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "summary"); ++ await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "source-idea"); ++ await expect(expandedRows.nth(2)).toHaveAttribute("data-game-child-row", "readiness-output"); ++ await expect(expandedRows.nth(0).locator("[data-game-child-table='summary'] caption")).toHaveText("Game Summary"); ++ let sourceIdeaChildTable = expandedRows.nth(1).locator("[data-game-child-table='source-idea']"); + await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea"); + await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]); + await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([ +@@ -412,7 +414,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + "Note 1Use dusk tide changes as the first Game Hub planning note.", + ]); + await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0); +- await expect(expandedRows.nth(1).locator("[data-game-child-table='readiness-output'] caption")).toHaveText("Readiness Output"); ++ await expect(expandedRows.nth(2).locator("[data-game-child-table='readiness-output'] caption")).toHaveText("Readiness Output"); + await page.reload({ waitUntil: "networkidle" }); + await expect(page.locator("[data-active-game-name]")).toHaveCount(0); + await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef"); +@@ -421,8 +423,8 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(page.locator("[data-game-hub-foundation]")).toHaveCount(0); + await activeGameToggle.click(); + expandedRows = page.locator("[data-game-expanded-row]"); +- await expect(expandedRows).toHaveCount(2); +- sourceIdeaChildTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); ++ await expect(expandedRows).toHaveCount(3); ++ sourceIdeaChildTable = expandedRows.nth(1).locator("[data-game-child-table='source-idea']"); + await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([ + "IdeaLantern Reef", + "PitchGuide light through a reef that rearranges at dusk.", +diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js +index 50bbe46f1..2b9f8c031 100644 +--- a/toolbox/game-hub/game-hub.js ++++ b/toolbox/game-hub/game-hub.js +@@ -287,7 +287,7 @@ function createGameToggleButton(game, expanded, active) { + button.setAttribute("aria-current", "true"); } -+ await page.route("**/api/game-journey/completion-metrics", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: options.completionMetrics || completionMetricsFixture(), -+ ok: true, -+ }), -+ }); -+ }); - if (options.session) { - const userKey = options.session.userKey || MOCK_DB_KEYS.users.user1; - await page.route("**/api/session/current", async (route) => { -@@ -113,6 +122,44 @@ function creatorSession() { - }; + button.setAttribute("aria-expanded", String(expanded)); +- const controlledRows = []; ++ const controlledRows = [`game-child-summary-${game.id}`]; + if (hasSourceIdeaDetails(game)) { + controlledRows.push(`game-child-source-idea-${game.id}`); + } +@@ -316,6 +316,30 @@ function hasSourceIdeaDetails(game) { + return Boolean(sourceIdea.name || sourceIdea.pitch || sourceIdea.notes.length); } -+function completionMetric(bucketKey, bucketName, completedCount, plannedCount) { -+ return { -+ active: true, -+ bucketKey, -+ bucketName, -+ completedCount, -+ percentComplete: plannedCount > 0 ? Math.round((completedCount / plannedCount) * 100) : 0, -+ plannedCount, -+ }; -+} -+ -+function completionMetricsFixture() { -+ return { -+ api: "Local API", -+ completedCount: 12, -+ databaseEngine: "Postgres", -+ percentComplete: 10, -+ plannedCount: 125, -+ records: [ -+ completionMetric("001-idea", "Idea", 1, 4), -+ completionMetric("002-create", "Create", 3, 5), -+ completionMetric("003-design", "Design", 2, 5), -+ completionMetric("004-graphics", "Graphics", 5, 10), -+ completionMetric("005-audio", "Audio", 0, 4), -+ completionMetric("006-objects", "Objects", 12, 25), -+ completionMetric("007-worlds", "Worlds", 0, 5), -+ completionMetric("008-interface", "Interface", 1, 5), -+ completionMetric("009-controls", "Controls", 1, 4), -+ completionMetric("010-rules", "Rules", 4, 5), -+ completionMetric("011-progression", "Progression", 0, 4), -+ completionMetric("012-play-test", "Play Test", 0, 5), -+ completionMetric("013-publish", "Publish", 0, 5), -+ completionMetric("014-share", "Share", 0, 5), -+ ], -+ serviceContract: "Web UI -> Local API/Service Contract -> Postgres", -+ }; ++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 = "Game SummaryFieldValue"; ++ const body = document.createElement("tbody"); ++ [ ++ ["Project", game.name], ++ ["Purpose", game.purpose], ++ ["Status", game.status], ++ ].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); +} + - const REMOVED_STATUS_BAR_LABELS = [ - "Selected Game Name", - "Selected Game Purpose", -@@ -148,6 +195,7 @@ async function statusBarSnapshot(page) { - const centerPanel = document.querySelector(".tool-center-panel"); - const gameName = bar.querySelector("[data-toolbox-selected-game-name]"); - const message = bar.querySelector("[data-toolbox-status-message]"); -+ const progress = bar.querySelector("[data-toolbox-status-progress]"); - const position = getComputedStyle(bar).position; - const barBox = bar.getBoundingClientRect(); - const footerBox = footer?.getBoundingClientRect(); -@@ -165,6 +213,9 @@ async function statusBarSnapshot(page) { - messageBox: boxSnapshot(message), - messageText: message?.textContent.replace(/\s+/g, " ").trim() || "", - position, -+ progressBox: boxSnapshot(progress), -+ progressState: bar.dataset.toolboxProgressState || "", -+ progressText: progress?.textContent.replace(/\s+/g, " ").trim() || "", - topBeforeFooter: footerBox ? barBox.bottom <= footerBox.top + 1 : false, - }; - }); -@@ -185,6 +236,7 @@ test("shared toolbox status bar shows selected Game Hub game above the footer", - await expect(statusBar.locator("[data-toolbox-status-action]")).toHaveCount(0); - await expect(statusBar.locator("[data-toolbox-selected-game]")).not.toContainText("Under Construction"); - await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Game Design mock repository ready."); -+ await expect(statusBar.locator("[data-toolbox-status-progress]")).toHaveText("Game Design 2/5 (40%) | Journey 12/125 (10%)"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-id", "demo-game"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "active"); - -@@ -197,9 +249,35 @@ test("shared toolbox status bar shows selected Game Hub game above the footer", - expect(snapshot.dataset.selectedGameRequired).toBe("true"); - expect(snapshot.gameText).toBe("Demo Game"); - expect(snapshot.messageText).toContain("Game Design mock repository ready."); -+ expect(snapshot.progressState).toBe("active"); -+ expect(snapshot.progressText).toBe("Game Design 2/5 (40%) | Journey 12/125 (10%)"); - expect(Math.max(snapshot.gameBox.top, snapshot.messageBox.top)).toBeLessThanOrEqual( - Math.min(snapshot.gameBox.bottom, snapshot.messageBox.bottom), - ); -+ expect(Math.max(snapshot.gameBox.top, snapshot.messageBox.top, snapshot.progressBox.top)).toBeLessThanOrEqual( -+ Math.min(snapshot.gameBox.bottom, snapshot.messageBox.bottom, snapshot.progressBox.bottom), -+ ); -+ expect(snapshot.progressBox.right).toBeGreaterThan(snapshot.messageBox.right); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("shared toolbox status bar shows right-anchored current tool and journey progress", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/objects/index.html"); -+ -+ try { -+ const statusBar = page.locator("[data-toolbox-status-bar]"); -+ await expect(statusBar).toBeVisible(); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("Demo Game"); -+ await expect(statusBar.locator("[data-toolbox-status-progress]")).toHaveText("Objects 12/25 (48%) | Journey 12/125 (10%)"); -+ -+ const snapshot = await statusBarSnapshot(page); -+ expect(snapshot.progressState).toBe("active"); -+ expect(snapshot.progressText).toBe("Objects 12/25 (48%) | Journey 12/125 (10%)"); -+ expect(snapshot.progressBox.right).toBeGreaterThan(snapshot.messageBox.right); - - expectNoPageFailures(failures); - } finally { -@@ -223,6 +301,7 @@ test("shared toolbox status bar center reports save state after Game Hub saves", - await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveCount(0); - await expect(statusBar.locator("[data-toolbox-status-message]")).toHaveText("Created and opened Status Bar Save."); - await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("Status Bar Save"); -+ await expect(statusBar.locator("[data-toolbox-status-progress]")).toHaveText("Game Hub 3/5 (60%) | Journey 12/125 (10%)"); - await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveCount(0); - await expect(statusBar).not.toContainText("Learning Game"); - await expect(statusBar).not.toContainText("Environment"); -@@ -246,6 +325,7 @@ test("shared toolbox status bar anchors to the bottom in tool display mode", asy - expect(Math.abs(snapshot.bottomGap)).toBeLessThanOrEqual(2); - expect(snapshot.gameText).toBe("Demo Game"); - expect(snapshot.messageText).toContain("Game Design mock repository ready."); -+ expect(snapshot.progressText).toBe("Game Design 2/5 (40%) | Journey 12/125 (10%)"); - expect(snapshot.centerPanelBox.bottom).toBeLessThanOrEqual(snapshot.barBox.top + 1); - - expectNoPageFailures(failures); -@@ -270,6 +350,7 @@ test("Game Hub owner selection updates the global toolbox status bar", async ({ - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "active"); - await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveCount(0); - await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Sign in to create or update Game Hub projects."); -+ await expect(statusBar.locator("[data-toolbox-status-progress]")).toHaveText("Game Hub 3/5 (60%) | Journey 12/125 (10%)"); + function renderSourceIdeaChildTable(parent, game) { + const sourceIdea = gameSourceIdeaDetails(game); + const wrapper = document.createElement("div"); +@@ -397,7 +421,13 @@ function renderReadinessOutputChildTable(parent, game, progress, active) { + } - expectNoPageFailures(failures); - } finally { -@@ -287,6 +368,7 @@ test("non-Idea Board toolbox pages show a creator-safe prompt when no Game Hub g - await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveCount(0); - await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveCount(0); - await expect(statusBar.locator("[data-toolbox-status-message]")).toHaveText("Select or create a game in Game Hub before using this toolbox page."); -+ await expect(statusBar.locator("[data-toolbox-status-progress]")).toHaveText("Game Design 2/5 (40%) | Journey 12/125 (10%)"); - await expect(statusBar.locator("[data-toolbox-status-action]")).toHaveCount(0); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "missing"); - await expect(page.locator("body")).not.toHaveAttribute("data-toolbox-selected-game-id", /.+/); -@@ -313,6 +395,7 @@ test("Idea Board is excluded from selected-game filtering and does not show the - await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveCount(0); - await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Ready to shape ideas and notes."); - await expect(statusBar.locator("[data-toolbox-status-message]")).not.toContainText("Select or create a game"); -+ await expect(statusBar.locator("[data-toolbox-status-progress]")).toHaveText("Idea Board 1/4 (25%) | Journey 12/125 (10%)"); - await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "optional"); - await expect(page.locator("body")).not.toHaveClass(/toolbox-selected-game-missing/); + function renderExpandedGameRow(tbody, game, progress, active) { +- const childRows = []; ++ const childRows = [ ++ { ++ id: `game-child-summary-${game.id}`, ++ render: (parent) => renderGameSummaryChildTable(parent, game), ++ type: "summary", ++ }, ++ ]; + if (hasSourceIdeaDetails(game)) { + childRows.push({ + id: `game-child-source-idea-${game.id}`, +@@ -562,9 +592,10 @@ function renderGameList(progress) { + wrapper.className = "table-wrapper"; + const table = document.createElement("table"); + table.className = "data-table data-table--fixed"; ++ table.dataset.gameParentTable = "open-games"; + table.dataset.gameRowsTable = "true"; +- table.setAttribute("aria-label", "Games"); +- table.innerHTML = "GamePurposeStatusActions"; ++ table.setAttribute("aria-label", "Open Games"); ++ table.innerHTML = "Open GamesGamePurposeStatusActions"; + const body = document.createElement("tbody"); + listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress)); + renderAddGameRow(body); diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs index 29e11c37a..2baf13cd0 100644 --- a/tests/playwright/tools/GameHubMockRepository.spec.mjs +++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs @@ -292,9 +292,9 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { 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("summary").filter({ hasText: /^Open Games$/ })).toHaveCount(0); - await expect(page.locator("[data-game-parent-table='open-games']")).toHaveCount(0); - await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Games"); - await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveCount(0); + await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games"); + await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Open Games"); + await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveText("Open Games"); await expect(page.locator("[data-game-rows-table='true'] thead th")).toHaveText([ "Game", "Purpose", @@ -354,12 +354,20 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { await demoGameRow.locator("[data-game-toggle='demo-game']").click(); await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true"); const demoChildRows = page.locator("[data-game-expanded-row='demo-game']"); - await expect(demoChildRows).toHaveCount(1); - await expect(demoChildRows.nth(0)).toHaveAttribute("data-game-child-row", "readiness-output"); - await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toHaveCount(0); - await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(1); + await expect(demoChildRows).toHaveCount(2); + await expect(demoChildRows.nth(0)).toHaveAttribute("data-game-child-row", "summary"); + await expect(demoChildRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); + await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(2); + const summaryTable = demoChildRows.nth(0).locator("[data-game-child-table='summary']"); + await expect(summaryTable.locator("caption")).toHaveText("Game Summary"); + await expect(summaryTable.locator("thead th")).toHaveText(["Field", "Value"]); + await expect(summaryTable.locator("tbody tr")).toHaveText([ + "ProjectDemo Game", + "PurposeGame", + "StatusUnder Construction", + ]); await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='source-idea']")).toHaveCount(0); - const readinessOutputTable = demoChildRows.nth(0).locator("[data-game-child-table='readiness-output']"); + const readinessOutputTable = demoChildRows.nth(1).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.locator("tbody tr")).toHaveText([ @@ -441,8 +449,9 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).not.toHaveAttribute("aria-current", "true"); await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1); await expect(page.locator("[data-game-toggle][data-game-active='true']")).toHaveCount(1); - await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveCount(1); - await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveAttribute("data-game-child-row", "readiness-output"); + await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveCount(2); + await expect(page.locator("[data-game-expanded-row='launch-test-game-1']").nth(0)).toHaveAttribute("data-game-child-row", "summary"); + await expect(page.locator("[data-game-expanded-row='launch-test-game-1']").nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); await expect(page.locator("[data-game-expanded-row='archive-game-2']")).toHaveCount(0); await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveClass("btn btn--compact primary"); await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Edit Launch Test Game" })).not.toHaveClass(/primary/); @@ -547,9 +556,9 @@ test("Game Hub validates game parent rows and child tables", async ({ page }) => try { await expect(page.locator("summary").filter({ hasText: /^Open Games$/ })).toHaveCount(0); - await expect(page.locator("[data-game-parent-table='open-games']")).toHaveCount(0); - await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Games"); - await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveCount(0); + await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games"); + await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Open Games"); + await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveText("Open Games"); const parentRows = page.locator("[data-game-rows-table='true'] tbody > [data-game-row]"); await expect(parentRows).toHaveCount(1); const gameRow = page.locator("[data-game-row='lantern-reef']"); @@ -559,13 +568,20 @@ test("Game Hub validates game parent rows and child tables", async ({ page }) => await gameRow.locator("[data-game-toggle='lantern-reef']").click(); await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "true"); const expandedRows = page.locator("[data-game-expanded-row='lantern-reef']"); - await expect(expandedRows).toHaveCount(2); - await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "source-idea"); - await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); - await expect(expandedRows.locator("[data-game-child-table]")).toHaveCount(2); - await expect(expandedRows.locator("[data-game-child-table='summary']")).toHaveCount(0); + await expect(expandedRows).toHaveCount(3); + await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "summary"); + await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "source-idea"); + await expect(expandedRows.nth(2)).toHaveAttribute("data-game-child-row", "readiness-output"); + await expect(expandedRows.locator("[data-game-child-table]")).toHaveCount(3); + const summaryTable = expandedRows.nth(0).locator("[data-game-child-table='summary']"); + await expect(summaryTable.locator("caption")).toHaveText("Game Summary"); + await expect(summaryTable.locator("tbody tr")).toHaveText([ + "ProjectLantern Reef", + "PurposeGame", + "StatusPlanning", + ]); - const sourceIdeaTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); + const sourceIdeaTable = expandedRows.nth(1).locator("[data-game-child-table='source-idea']"); await expect(sourceIdeaTable.locator("caption")).toHaveText("Source Idea"); await expect(sourceIdeaTable.locator("tbody tr")).toHaveText([ "IdeaLantern Reef", @@ -576,7 +592,7 @@ test("Game Hub validates game parent rows and child tables", async ({ page }) => 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 = expandedRows.nth(1).locator("[data-game-child-table='readiness-output']"); + const readinessOutputTable = expandedRows.nth(2).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/); @@ -678,6 +694,7 @@ test("Game Hub shows a creator-safe empty state when no projects exist", async ( try { await expect(page.locator("[data-active-game-name]")).toHaveCount(0); await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Add a game to start building."); + await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games"); await expect(page.locator("[data-game-rows-table='true'] thead th")).toHaveText([ "Game", "Purpose", diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs index 61d8fdc68..01a730513 100644 --- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs @@ -400,10 +400,12 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { await expect(activeGameToggle).toHaveText("Lantern Reef"); await activeGameToggle.click(); let expandedRows = page.locator("[data-game-expanded-row]"); - await expect(expandedRows).toHaveCount(2); - await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "source-idea"); - await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); - let sourceIdeaChildTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); + await expect(expandedRows).toHaveCount(3); + await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "summary"); + await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "source-idea"); + await expect(expandedRows.nth(2)).toHaveAttribute("data-game-child-row", "readiness-output"); + await expect(expandedRows.nth(0).locator("[data-game-child-table='summary'] caption")).toHaveText("Game Summary"); + let sourceIdeaChildTable = expandedRows.nth(1).locator("[data-game-child-table='source-idea']"); await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea"); await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]); await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([ @@ -412,7 +414,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { "Note 1Use dusk tide changes as the first Game Hub planning note.", ]); await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0); - await expect(expandedRows.nth(1).locator("[data-game-child-table='readiness-output'] caption")).toHaveText("Readiness Output"); + await expect(expandedRows.nth(2).locator("[data-game-child-table='readiness-output'] caption")).toHaveText("Readiness Output"); await page.reload({ waitUntil: "networkidle" }); await expect(page.locator("[data-active-game-name]")).toHaveCount(0); await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef"); @@ -421,8 +423,8 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { await expect(page.locator("[data-game-hub-foundation]")).toHaveCount(0); await activeGameToggle.click(); expandedRows = page.locator("[data-game-expanded-row]"); - await expect(expandedRows).toHaveCount(2); - sourceIdeaChildTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); + await expect(expandedRows).toHaveCount(3); + sourceIdeaChildTable = expandedRows.nth(1).locator("[data-game-child-table='source-idea']"); await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([ "IdeaLantern Reef", "PitchGuide light through a reef that rearranges at dusk.", diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js index 50bbe46f1..2b9f8c031 100644 --- a/toolbox/game-hub/game-hub.js +++ b/toolbox/game-hub/game-hub.js @@ -287,7 +287,7 @@ function createGameToggleButton(game, expanded, active) { button.setAttribute("aria-current", "true"); } button.setAttribute("aria-expanded", String(expanded)); - const controlledRows = []; + const controlledRows = [`game-child-summary-${game.id}`]; if (hasSourceIdeaDetails(game)) { controlledRows.push(`game-child-source-idea-${game.id}`); } @@ -316,6 +316,30 @@ function hasSourceIdeaDetails(game) { return Boolean(sourceIdea.name || sourceIdea.pitch || sourceIdea.notes.length); } +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 = "Game SummaryFieldValue"; + const body = document.createElement("tbody"); + [ + ["Project", game.name], + ["Purpose", game.purpose], + ["Status", game.status], + ].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 renderSourceIdeaChildTable(parent, game) { const sourceIdea = gameSourceIdeaDetails(game); const wrapper = document.createElement("div"); @@ -397,7 +421,13 @@ function renderReadinessOutputChildTable(parent, game, progress, active) { } function renderExpandedGameRow(tbody, game, progress, active) { - const childRows = []; + const childRows = [ + { + id: `game-child-summary-${game.id}`, + render: (parent) => renderGameSummaryChildTable(parent, game), + type: "summary", + }, + ]; if (hasSourceIdeaDetails(game)) { childRows.push({ id: `game-child-source-idea-${game.id}`, @@ -562,9 +592,10 @@ function renderGameList(progress) { wrapper.className = "table-wrapper"; const table = document.createElement("table"); table.className = "data-table data-table--fixed"; + table.dataset.gameParentTable = "open-games"; table.dataset.gameRowsTable = "true"; - table.setAttribute("aria-label", "Games"); - table.innerHTML = "GamePurposeStatusActions"; + table.setAttribute("aria-label", "Open Games"); + table.innerHTML = "Open GamesGamePurposeStatusActions"; const body = document.createElement("tbody"); listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress)); renderAddGameRow(body);