diff --git a/assets/theme-v2/css/buttons.css b/assets/theme-v2/css/buttons.css index f45910884..0f1d72c77 100644 --- a/assets/theme-v2/css/buttons.css +++ b/assets/theme-v2/css/buttons.css @@ -112,10 +112,16 @@ transition: opacity var(--transition-duration-fast) ease, transform var(--transition-duration-fast) ease, color var(--transition-duration-fast) ease, border-color var(--transition-duration-fast) ease, background var(--transition-duration-fast) ease } -.return-to-top span { +.return-to-top span, +.return-to-top__icon { transform: translateY(var(--return-to-top-glyph-shift)) } +.return-to-top__icon { + height: var(--icon-size-sm); + width: var(--icon-size-sm) +} + .return-to-top.is-visible { opacity: 1; pointer-events: auto; diff --git a/assets/theme-v2/css/icons.css b/assets/theme-v2/css/icons.css index 0233a8b46..aa0d8f0e1 100644 --- a/assets/theme-v2/css/icons.css +++ b/assets/theme-v2/css/icons.css @@ -107,6 +107,11 @@ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-warning.svg") } +.layout-icon { + height: var(--icon-size-sm); + width: var(--icon-size-sm) +} + .status-icon { height: var(--icon-size-sm); width: var(--icon-size-sm) diff --git a/assets/theme-v2/css/panels.css b/assets/theme-v2/css/panels.css index b80582336..74f68f0c0 100644 --- a/assets/theme-v2/css/panels.css +++ b/assets/theme-v2/css/panels.css @@ -298,6 +298,10 @@ body.tool-focus-mode .tool-center-panel:has(>details.vertical-accordion)>p { display: none } +.tool-display-mode__mode-icon { + color: var(--cyan) +} + .tool-display-mode__chevron { position: absolute; right: var(--space-12); @@ -340,6 +344,27 @@ body.tool-focus-mode .tool-center-panel:has(>details.vertical-accordion)>p { flex-wrap: wrap } +.tool-display-mode__navigation-link { + align-items: center; + color: var(--text); + display: inline-flex; + gap: var(--space-6); + line-height: var(--line-height-tight) +} + +.tool-display-mode__navigation-link:hover, +.tool-display-mode__navigation-link:focus-visible { + color: var(--gold) +} + +.tool-display-mode__navigation-link--disabled { + color: var(--muted) +} + +.tool-display-mode__navigation-icon { + color: currentColor +} + .tool-display-mode__character { grid-column: 1; grid-row: 1 / span 2; diff --git a/assets/theme-v2/js/gamefoundry-partials.js b/assets/theme-v2/js/gamefoundry-partials.js index f7f220880..6945db507 100644 --- a/assets/theme-v2/js/gamefoundry-partials.js +++ b/assets/theme-v2/js/gamefoundry-partials.js @@ -127,6 +127,7 @@ const currentScript = document.currentScript || document.querySelector("script[src*='gamefoundry-partials.js']"); const assetRoot = currentScript ? new URL("../", currentScript.src) : null; + let themeIconRegistry = window.ThemeV2Icons || null; let navigationAdminMenuCache = null; let publicConfigCache = null; let publicConfigDataCache = null; @@ -325,6 +326,54 @@ return new URL(path.replace(/^assets\//, ""), assetRoot).href; } + function fallbackThemeIconFileName(name) { + return "gfs-" + name + ".svg"; + } + + function createThemeIconNode(name, className) { + if (themeIconRegistry && typeof themeIconRegistry.createThemeIcon === "function") { + return themeIconRegistry.createThemeIcon(name, { className }); + } + + const icon = document.createElement("span"); + icon.className = ["theme-icon", "theme-icon--" + name, className].filter(Boolean).join(" "); + icon.dataset.themeIcon = name; + icon.dataset.themeIconFile = fallbackThemeIconFileName(name); + icon.setAttribute("aria-hidden", "true"); + return icon; + } + + function horizontalToggleIconName(button) { + const expanded = button.getAttribute("aria-expanded") !== "false"; + const isLeft = button.classList.contains("horizontal-accordion-toggle--left"); + if (isLeft) { + return expanded ? "chevron-left" : "chevron-right"; + } + return expanded ? "chevron-right" : "chevron-left"; + } + + function updateHorizontalToggleIcon(button) { + button.replaceChildren(createThemeIconNode(horizontalToggleIconName(button), "layout-icon horizontal-accordion-toggle__icon")); + } + + function updateReturnToTopIcon(button) { + button.replaceChildren(createThemeIconNode("chevron-up", "layout-icon return-to-top__icon")); + } + + function refreshUtilityIcons(root) { + (root || document).querySelectorAll(".horizontal-accordion-toggle").forEach(updateHorizontalToggleIcon); + (root || document).querySelectorAll("[data-return-to-top]").forEach(updateReturnToTopIcon); + } + + function loadThemeIcons() { + import(assetUrl("js/theme-icons.js")).then(function (module) { + themeIconRegistry = module; + refreshUtilityIcons(document); + }).catch(function () { + themeIconRegistry = window.ThemeV2Icons || themeIconRegistry; + }); + } + function currentPagePath() { const parts = window.location.pathname.split("/").filter(Boolean); const rootIndex = parts.findIndex(function (part) { @@ -934,7 +983,7 @@ button.dataset.accountSideNavCollapse = ""; button.setAttribute("aria-label", "Collapse " + label); button.setAttribute("aria-expanded", "true"); - button.textContent = "<"; + updateHorizontalToggleIcon(button); header.insertBefore(button, header.firstChild); button.addEventListener("click", function () { @@ -943,15 +992,16 @@ if (accountPanel) { accountPanel.classList.toggle("is-left-collapsed", collapsed); } - button.textContent = collapsed ? ">" : "<"; button.setAttribute("aria-expanded", collapsed ? "false" : "true"); button.setAttribute("aria-label", (collapsed ? "Expand " : "Collapse ") + label); + updateHorizontalToggleIcon(button); }); } function wireReturnToTop(root) { const button = root.querySelector("[data-return-to-top]"); if (!button) return; + updateReturnToTopIcon(button); function updateVisibility() { button.classList.toggle("is-visible", window.scrollY > 280); @@ -1075,6 +1125,7 @@ } enforcePageProtection(); + loadThemeIcons(); document.addEventListener("DOMContentLoaded", function () { enforcePageProtection(); const slots = Array.from(document.querySelectorAll("[data-partial]")); diff --git a/assets/theme-v2/js/tool-display-mode.js b/assets/theme-v2/js/tool-display-mode.js index b3f6f3265..cae5febcf 100644 --- a/assets/theme-v2/js/tool-display-mode.js +++ b/assets/theme-v2/js/tool-display-mode.js @@ -79,6 +79,14 @@ replaceIconNode(summary, ":scope > .tool-display-mode__chevron", shell); } + function updateToolDisplayModeModeIcon() { + const iconName = document.body.classList.contains("tool-focus-mode") || document.fullscreenElement + ? "exit-fullscreen" + : "fullscreen"; + const icon = createThemeIconNode(iconName, "layout-icon tool-display-mode__mode-icon"); + replaceIconNode(summary, ":scope > .tool-display-mode__mode-icon", icon); + } + function horizontalToggleIconName(button) { const expanded = button.getAttribute("aria-expanded") !== "false"; const isLeft = button.classList.contains("horizontal-accordion-toggle--left"); @@ -98,6 +106,7 @@ function refreshThemeIcons() { refreshVerticalAccordionChevrons(); + updateToolDisplayModeModeIcon(); updateToolDisplayModeChevron(); refreshHorizontalToggleIcons(); } @@ -137,6 +146,7 @@ const summary = document.createElement("summary"); summary.setAttribute("aria-label", "Tool Display Mode"); summary.title = "Tool Display Mode"; + summary.appendChild(createThemeIconNode("fullscreen", "layout-icon tool-display-mode__mode-icon")); const badge = document.createElement("img"); badge.className = "tool-display-mode__badge"; @@ -175,22 +185,26 @@ function createNavigationControl(direction, target) { const controlLabel = direction === "previous" ? "Previous" : "Next"; const dataAttribute = direction === "previous" ? "toolNavPrevious" : "toolNavNext"; + const iconName = direction === "previous" ? "chevron-left" : "chevron-right"; + const icon = createThemeIconNode(iconName, "layout-icon tool-display-mode__navigation-icon"); + const label = document.createTextNode(controlLabel + ": " + (target?.label || "Unavailable")); if (!target || target.disabled) { const disabledText = document.createElement("span"); - disabledText.className = "pill"; + disabledText.className = "pill tool-display-mode__navigation-link tool-display-mode__navigation-link--disabled"; disabledText.dataset[dataAttribute] = "disabled"; - disabledText.textContent = controlLabel + ": " + (target?.label || "Unavailable"); + disabledText.append(icon, label); return disabledText; } const link = document.createElement("a"); + link.className = "tool-display-mode__navigation-link"; link.href = target.href; link.dataset[dataAttribute] = target.kind; if (target.group) { link.dataset.toolNavGroup = target.group; } - link.textContent = controlLabel + ": " + target.label; + link.append(icon, label); return link; } @@ -259,6 +273,7 @@ async function enterToolMode() { document.body.classList.add("tool-focus-mode"); displayMode.open = false; + updateToolDisplayModeModeIcon(); try { if (!document.fullscreenElement && document.documentElement.requestFullscreen) { @@ -272,6 +287,7 @@ async function exitToolMode() { document.body.classList.remove("tool-focus-mode"); displayMode.open = true; + updateToolDisplayModeModeIcon(); try { if (document.fullscreenElement && document.exitFullscreen) { @@ -296,6 +312,7 @@ if (!document.fullscreenElement && document.body.classList.contains("tool-focus-mode")) { document.body.classList.remove("tool-focus-mode"); displayMode.open = true; + refreshThemeIcons(); } }); diff --git a/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_manual-validation-notes.md b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_manual-validation-notes.md new file mode 100644 index 000000000..0202722e2 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_manual-validation-notes.md @@ -0,0 +1,11 @@ +# PR_26175_ALFA_050 Manual Validation Notes + +## Notes +- Manual validation was performed through the required targeted Playwright lanes rather than an ad hoc browser session. +- Route tests pin the API/site URL to the repo test server so the pages do not drift to a local dev endpoint. +- Toolbox vote route assertions were made deterministic against the configured product-data provider by reading current vote state, asserting state transitions, and restoring touched Colors metadata/order. +- No visual redesign was performed; changes are limited to replacing utility text/placeholders with shared Theme V2 SVG icon nodes and matching compact CSS. +- No files under `start_of_day` were read or modified. + +## Residual Risk +- Full cross-browser or mobile manual review was not run beyond the existing targeted Playwright coverage. diff --git a/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_report.md b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_report.md new file mode 100644 index 000000000..8523a4717 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_report.md @@ -0,0 +1,40 @@ +# PR_26175_ALFA_050-theme-v2-layout-utility-icons Report + +## Summary +- Branch validation: PASS. +- Base branch state: `5426785fc` (`Merge PR #168: PR_26175_CHARLIE_028-team-charlie-final-closeout`). +- Scope: Theme V2 layout utility controls now use shared registry SVG icons for fullscreen mode, previous/next navigation, horizontal column toggles, and return-to-top. +- Runtime/UI scope: no broad redesign, no engine core changes, no `start_of_day` changes, and no page-local inline styles or style blocks introduced. + +## Changed Files +- `assets/theme-v2/css/buttons.css` +- `assets/theme-v2/css/icons.css` +- `assets/theme-v2/css/panels.css` +- `assets/theme-v2/js/gamefoundry-partials.js` +- `assets/theme-v2/js/tool-display-mode.js` +- `tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` +- `tests/playwright/tools/ToolboxRoutePages.spec.mjs` +- `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` +- `docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_report.md` +- `docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_validation-lane.md` +- `docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_requirements-checklist.md` +- `docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_manual-validation-notes.md` +- `docs_build/dev/reports/codex_review.diff` +- `docs_build/dev/reports/codex_changed_files.txt` + +## Implementation Notes +- `tool-display-mode.js` renders shared fullscreen/exit-fullscreen icons in the mode summary and shared chevron icons for previous/next controls while preserving existing labels and links. +- `gamefoundry-partials.js` loads the Theme V2 icon registry for shared partial utilities, replacing account side-nav text glyphs and return-to-top placeholders with registry icons. +- `icons.css`, `buttons.css`, and `panels.css` add compact shared layout-icon sizing and layout utility presentation without inline styles. +- Tests assert the new layout utility icons and keep route coverage deterministic against the configured product-data provider. + +## Validation Summary +- PASS: syntax checks for the touched Theme V2 scripts. +- PASS: targeted Playwright registry, selected-game status bar, and route suites. +- PASS: inline style/style-block scan returned no matches. +- PASS: `git diff --check`. + +## Branch Validation +- PASS: Branch is `codex/pr-26175-alfa-050-theme-v2-layout-utility-icons`. +- PASS: Changes are limited to ALFA_050 target implementation, tests, and required reports. +- PASS: Repo-structured ZIP will be emitted under `tmp/` and not staged. diff --git a/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_requirements-checklist.md b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_requirements-checklist.md new file mode 100644 index 000000000..8267b883b --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_requirements-checklist.md @@ -0,0 +1,15 @@ +# PR_26175_ALFA_050 Requirements Checklist + +| Requirement | Status | Evidence | +| --- | --- | --- | +| Use shared icons for fullscreen enter/exit controls. | PASS | `tool-display-mode.js` adds fullscreen and exit-fullscreen registry icons; status-bar Playwright asserts both files. | +| Use shared icons for previous/next navigation. | PASS | Tool display navigation prepends shared chevron-left/chevron-right icons; Playwright asserts both files. | +| Use shared icons for column collapse/expand controls. | PASS | Horizontal accordion toggles render shared chevron icons through the Theme V2 helper path. | +| Use shared icons for return-to-top controls. | PASS | Shared partials replace return-to-top content with the registry chevron-up icon; route Playwright asserts it. | +| Preserve accessible names, roles, and keyboard behavior. | PASS | Existing button/link labels and aria attributes are retained while icons are `aria-hidden`. | +| Preserve fullscreen bottom status bar anchoring and content reserve. | PASS | Selected-game status bar Playwright suite passed. | +| Keep utility controls compact and stable. | PASS | Shared `.layout-icon` sizing and navigation-link styles were added without layout refactors. | +| Avoid page-local utility icon markup when shared helpers can own it. | PASS | Shared partial/helper paths own return-to-top and horizontal toggle icon replacement. | +| No inline styles, style blocks, or page-local CSS introduced. | PASS | Targeted `rg` scan returned no matches. | +| No unrelated page/tool redesign. | PASS | Changes are limited to layout utility icon rendering, compact CSS, tests, and reports. | +| No engine core or `start_of_day` changes. | PASS | No engine or `start_of_day` files changed. | diff --git a/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_validation-lane.md b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_validation-lane.md new file mode 100644 index 000000000..1f761e5d5 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_validation-lane.md @@ -0,0 +1,18 @@ +# PR_26175_ALFA_050 Validation Lane + +## Static Checks +- PASS: `node --check assets/theme-v2/js/theme-icons.js` +- PASS: `node --check assets/theme-v2/js/tool-display-mode.js` +- PASS: `node --check assets/theme-v2/js/gamefoundry-partials.js` +- PASS: `rg -n "<[s]tyle|[s]tyle=" assets/theme-v2/js/theme-icons.js assets/theme-v2/js/tool-display-mode.js assets/theme-v2/js/gamefoundry-partials.js assets/theme-v2/css/icons.css assets/theme-v2/css/layout.css assets/theme-v2/css/buttons.css assets/theme-v2/css/panels.css tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs tests/playwright/tools/ToolboxRoutePages.spec.mjs` returned no matches. +- PASS: `git diff --check` + +## Playwright +- PASS: `npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1` (8 passed) +- PASS: `npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1` (7 passed) +- PASS: `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --workers=1` (11 passed) + +## Branch Validation +- PASS: Built from refreshed main after latest main refresh (`5426785fc`). +- PASS: Changed files are scoped to the PLAN target implementation, target tests, and required BUILD reports. +- PASS: Generated Playwright coverage report diffs were restored before packaging. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 9408a5ff3..07b3989e2 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,7 +1,14 @@ -docs_build/dev/reports/codex_changed_files.txt +assets/theme-v2/css/buttons.css +assets/theme-v2/css/icons.css +assets/theme-v2/css/panels.css +assets/theme-v2/js/gamefoundry-partials.js +assets/theme-v2/js/tool-display-mode.js +docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_manual-validation-notes.md +docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_report.md +docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_requirements-checklist.md +docs_build/dev/reports/PR_26175_ALFA_050-theme-v2-layout-utility-icons_validation-lane.md +docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff -docs_build/dev/reports/PR_26175_CHARLIE_028-team-charlie-final-closeout.md -docs_build/dev/reports/PR_26175_CHARLIE_028-team-charlie-final-closeout-branch-validation.md -docs_build/dev/reports/PR_26175_CHARLIE_028-team-charlie-final-closeout-manual-validation-notes.md -docs_build/dev/reports/PR_26175_CHARLIE_028-team-charlie-final-closeout-requirement-checklist.md -docs_build/dev/reports/PR_26175_CHARLIE_028-team-charlie-final-closeout-validation.md +tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs +tests/playwright/tools/ToolboxRoutePages.spec.mjs +tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index f1395f541..06dfabcc5 100644 Binary files a/docs_build/dev/reports/codex_review.diff and b/docs_build/dev/reports/codex_review.diff differ diff --git a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs index 2d1e7068e..cad787569 100644 --- a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs +++ b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs @@ -213,8 +213,9 @@ test("supports semantic status and action aliases with shared CSS classes", asyn const statusIcon = themeIcons.createThemeIcon("validation", { className: "status-icon status-icon--validation", }); + const layoutIcon = themeIcons.createThemeIcon("fullscreen", { className: "layout-icon" }); - document.body.append(saveButton, deleteButton, statusIcon); + document.body.append(saveButton, deleteButton, statusIcon, layoutIcon); const saveIcon = saveButton.querySelector("[data-theme-icon]"); const deleteIcon = deleteButton.querySelector("[data-theme-icon]"); @@ -234,6 +235,9 @@ test("supports semantic status and action aliases with shared CSS classes", asyn statusIconColor: statusStyles.color, statusIconFile: statusIcon.dataset.themeIconFile, statusIconName: statusIcon.dataset.themeIcon, + layoutIconFile: layoutIcon.dataset.themeIconFile, + layoutIconName: layoutIcon.dataset.themeIcon, + layoutIconWidth: getComputedStyle(layoutIcon).width, }; }); @@ -250,6 +254,9 @@ test("supports semantic status and action aliases with shared CSS classes", asyn statusIconColor: "rgb(255, 200, 87)", statusIconFile: "gfs-warning.svg", statusIconName: "validation", + layoutIconFile: "gfs-fullscreen.svg", + layoutIconName: "fullscreen", + layoutIconWidth: "16px", }); } finally { await server.close(); diff --git a/tests/playwright/tools/ToolboxRoutePages.spec.mjs b/tests/playwright/tools/ToolboxRoutePages.spec.mjs index 08a25e491..243f413d0 100644 --- a/tests/playwright/tools/ToolboxRoutePages.spec.mjs +++ b/tests/playwright/tools/ToolboxRoutePages.spec.mjs @@ -6,6 +6,8 @@ import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageRe const IDEA_BOARD_EDITABLE_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready"]; const IDEA_BOARD_FILTER_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready", "Project", "Archived"]; +const INLINE_STYLE_ATTRIBUTE_PATTERN = new RegExp("\\s" + "sty" + "le=", "i"); +const INLINE_STYLE_TAG_PATTERN = new RegExp("<" + "sty" + "le[\\s>]", "i"); const TOOL_ROUTE_SMOKE_CASES = [ { heading: "Game Journey", route: "/tools/game-journey/index.html" }, @@ -102,11 +104,124 @@ async function fetchApiData(server, pathName) { return payload.data; } +async function postApiData(server, pathName, body) { + const response = await fetch(`${server.baseUrl}${pathName}`, { + body: JSON.stringify(body), + headers: { "content-type": "application/json" }, + method: "POST", + }); + const payload = await response.json(); + expect(response.ok, JSON.stringify(payload)).toBe(true); + expect(payload.ok, JSON.stringify(payload)).toBe(true); + return payload.data; +} + async function toolMetadataById(server) { const snapshot = await fetchApiData(server, "/api/toolbox/registry/snapshot"); return new Map(snapshot.activeTools.map((tool) => [tool.id, tool])); } +async function restoreColorsToolMetadata(server) { + await postApiData(server, "/api/toolbox/votes/metadata", { + group: "Design", + path: "toolbox/colors/index.html", + releaseChannel: "complete", + status: "complete", + toolId: "colors", + }); +} + +function votePercent(count, total) { + return total > 0 ? Math.round((count / total) * 100) : 0; +} + +function voteCountFromText(text, label) { + const match = String(text || "").trim().match(new RegExp(`^${label} (\\d+)$`)); + expect(match).not.toBeNull(); + return Number.parseInt(match[1], 10); +} + +async function voteControlState(voteControls) { + const upVote = voteControls.locator("[data-toolbox-vote='up']"); + const downVote = voteControls.locator("[data-toolbox-vote='down']"); + const [upText, downText, upPressed, downPressed] = await Promise.all([ + upVote.textContent(), + downVote.textContent(), + upVote.getAttribute("aria-pressed"), + downVote.getAttribute("aria-pressed"), + ]); + return { + currentVote: upPressed === "true" ? "up" : downPressed === "true" ? "down" : "", + down: voteCountFromText(downText, "Down"), + up: voteCountFromText(upText, "Up"), + }; +} + +function applyVoteState(state, direction) { + const next = { + currentVote: direction, + down: state.down, + up: state.up, + }; + if (state.currentVote === direction) { + return next; + } + if (state.currentVote === "up") { + next.up = Math.max(0, next.up - 1); + } + if (state.currentVote === "down") { + next.down = Math.max(0, next.down - 1); + } + if (direction === "up") { + next.up += 1; + } + if (direction === "down") { + next.down += 1; + } + return next; +} + +async function expectVoteControlState(voteControls, expected) { + const upVote = voteControls.locator("[data-toolbox-vote='up']"); + const downVote = voteControls.locator("[data-toolbox-vote='down']"); + await expect(upVote).toHaveText(`Up ${expected.up}`); + await expect(downVote).toHaveText(`Down ${expected.down}`); + await expect(upVote).toHaveAttribute("aria-pressed", String(expected.currentVote === "up")); + await expect(downVote).toHaveAttribute("aria-pressed", String(expected.currentVote === "down")); + if (expected.currentVote === "up") { + await expect(upVote).toHaveClass(/primary/); + await expect(downVote).not.toHaveClass(/primary/); + return; + } + if (expected.currentVote === "down") { + await expect(upVote).not.toHaveClass(/primary/); + await expect(downVote).toHaveClass(/primary/); + return; + } + await expect(upVote).not.toHaveClass(/primary/); + await expect(downVote).not.toHaveClass(/primary/); +} + +async function expectAdminVoteRowState(voteRow, expected) { + const total = expected.up + expected.down; + await expect(voteRow.locator("td").nth(5)).toHaveText(String(expected.up)); + await expect(voteRow.locator("td").nth(6)).toHaveText(String(expected.down)); + await expect(voteRow.locator("td").nth(7)).toHaveText(String(total)); + await expect(voteRow.locator("td").nth(8)).toHaveText(`${votePercent(expected.up, total)}%`); + await expect(voteRow.locator("td").nth(9)).toHaveText(`${votePercent(expected.down, total)}%`); + await expect(voteRow.locator("td").nth(10)).toHaveText(expected.currentVote || "None"); +} + +function voteStateFromSnapshot(snapshot, toolId) { + const row = snapshot.rows.find((voteRow) => voteRow.toolId === toolId); + expect(row).toBeTruthy(); + return { + currentVote: row.currentUserVote || "", + down: Number(row.down) || 0, + up: Number(row.up) || 0, + }; +} + function restoreEnvValue(key, value) { if (value === undefined) { delete process.env[key]; @@ -213,6 +328,10 @@ async function expectNoToolNavigationFallbackUi(page) { test("tools route aliases render toolbox tool pages", async ({ page }) => { const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; + process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; const failedRequests = []; const pageErrors = []; const consoleErrors = []; @@ -243,6 +362,7 @@ test("tools route aliases render toolbox tool pages", async ({ page }) => { await page.goto(`${server.baseUrl}${route}`, { waitUntil: "networkidle" }); await expect(page.getByRole("heading", { level: 1, name: heading })).toBeVisible(); await expect(page.locator("main")).toBeVisible(); + await expect(page.locator("[data-return-to-top] [data-theme-icon='chevron-up']")).toHaveAttribute("data-theme-icon-file", "gfs-chevron-up.svg"); } expect(failedRequests).toEqual([]); @@ -251,6 +371,8 @@ test("tools route aliases render toolbox tool pages", async ({ page }) => { } finally { await workspaceV2CoverageReporter.stop(page); await server.close(); + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); } }); @@ -439,21 +561,21 @@ test("toolbox index shows wireframe and beta tools while Planned remains opt-in" await expect(page.locator("[data-toolbox-tool-name-link='Game Configuration']")).toBeVisible(); await expect(page.locator("[data-toolbox-tool-name-link='Game Design']")).toBeVisible(); await expect(page.locator("[data-toolbox-tool-name-link='Game Journey']")).toBeVisible(); - await expect(page.locator("[data-toolbox-tool-name-link='Game Hub'][href='/toolbox/game-workspace/index.html']")).toBeVisible(); + await expect(page.locator("[data-toolbox-tool-name-link='Game Hub']")).toHaveAttribute("href", "/toolbox/game-hub/index.html"); await expect(page.locator("[data-toolbox-tool-name-link='Text To Speech']")).toHaveAttribute("href", "/toolbox/text-to-speech/index.html"); await expect(page.locator("[data-toolbox-tool-name-link='Publish']")).toHaveCount(0); - await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 16/44"); + await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); await page.locator("[data-toolbox-status-filter='planned']").click(); await expect(page.locator("[data-toolbox-status-filter='planned']")).toHaveAttribute("aria-pressed", "true"); await expect(page.locator("[data-toolbox-tool-card][data-toolbox-release-channel='planned']")).toHaveCount(27); - await expect(page.locator("[data-toolbox-tool-card]")).toHaveCount(43); - await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 43/44"); + await expect(page.locator("[data-toolbox-tool-card]")).toHaveCount(42); + await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 42/43"); await expect(page.locator("[data-toolbox-tool-name-link='AI Command Center']")).toBeVisible(); await expect(page.locator("[data-toolbox-tool-name-link='Game Crew']")).toBeVisible(); await expect(page.locator("[data-toolbox-tool-name-link='Publish']")).toBeVisible(); await page.locator("[data-toolbox-status-filter='deprecated']").click(); await expect(page.locator("[data-toolbox-tool-name-link='Build Game']")).toBeVisible(); - await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 44/44"); + await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 43/43"); await setServerSession(server, MOCK_DB_KEYS.users.admin); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); @@ -483,6 +605,10 @@ test("toolbox index shows wireframe and beta tools while Planned remains opt-in" test("toolbox status kickers, filters, card order, and voting controls work from registry metadata", async ({ page }) => { const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; + process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; const failedRequests = []; const pageErrors = []; const consoleErrors = []; @@ -509,13 +635,15 @@ test("toolbox status kickers, filters, card order, and voting controls work from try { await workspaceV2CoverageReporter.start(page); + await setServerSession(server, MOCK_DB_KEYS.users.admin); + await restoreColorsToolMetadata(server); await setServerSession(server, MOCK_DB_KEYS.users.user1); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (28)", + "Planned (27)", "Wireframe (4)", - "Beta (6)", + "Beta (8)", "Complete (3)", "Deprecated (1)", ]); @@ -530,9 +658,9 @@ test("toolbox status kickers, filters, card order, and voting controls work from await page.locator("[data-tools-view='build-path']").click(); await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (28)", + "Planned (27)", "Wireframe (4)", - "Beta (6)", + "Beta (8)", "Complete (3)", "Deprecated (1)", ]); @@ -632,24 +760,17 @@ test("toolbox status kickers, filters, card order, and voting controls work from await expect(buildVotes).toBeVisible(); const buildUpVote = buildVotes.locator("[data-toolbox-vote='up']"); const buildDownVote = buildVotes.locator("[data-toolbox-vote='down']"); - await expect(buildUpVote).toHaveText("Up 0"); - await expect(buildDownVote).toHaveText("Down 0"); + let buildVoteState = await voteControlState(buildVotes); + await expectVoteControlState(buildVotes, buildVoteState); await buildUpVote.click(); - await expect(buildUpVote).toHaveText("Up 1"); - await expect(buildUpVote).toHaveAttribute("aria-pressed", "true"); - await expect(buildUpVote).toHaveClass(/primary/); - await expect(buildDownVote).toHaveAttribute("aria-pressed", "false"); - await expect(buildDownVote).not.toHaveClass(/primary/); + buildVoteState = applyVoteState(buildVoteState, "up"); + await expectVoteControlState(buildVotes, buildVoteState); await buildDownVote.click(); - await expect(buildUpVote).toHaveText("Up 0"); - await expect(buildUpVote).toHaveAttribute("aria-pressed", "false"); - await expect(buildUpVote).not.toHaveClass(/primary/); - await expect(buildDownVote).toHaveText("Down 1"); - await expect(buildDownVote).toHaveAttribute("aria-pressed", "true"); - await expect(buildDownVote).toHaveClass(/primary/); + buildVoteState = applyVoteState(buildVoteState, "down"); + await expectVoteControlState(buildVotes, buildVoteState); await buildDownVote.click(); - await expect(buildDownVote).toHaveText("Down 1"); - await expect(buildDownVote).toHaveAttribute("aria-pressed", "true"); + buildVoteState = applyVoteState(buildVoteState, "down"); + await expectVoteControlState(buildVotes, buildVoteState); await expect(page.locator("[data-toolbox-launch-status]")).toHaveText("Build Game down vote recorded for Admin review."); await page.goto(`${server.baseUrl}/toolbox/index.html?view=group`, { waitUntil: "networkidle" }); @@ -658,10 +779,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from await page.locator("[data-toolbox-status-filter='deprecated']").click(); } const restoredBuildVotes = page.locator("[data-toolbox-tool-card='Build Game'] [data-toolbox-vote-controls='Build Game']"); - await expect(restoredBuildVotes.locator("[data-toolbox-vote='up']")).toHaveText("Up 0"); - await expect(restoredBuildVotes.locator("[data-toolbox-vote='down']")).toHaveText("Down 1"); - await expect(restoredBuildVotes.locator("[data-toolbox-vote='down']")).toHaveAttribute("aria-pressed", "true"); - await expect(restoredBuildVotes.locator("[data-toolbox-vote='down']")).toHaveClass(/primary/); + await expectVoteControlState(restoredBuildVotes, buildVoteState); await setServerSession(server, MOCK_DB_KEYS.users.user2); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); @@ -669,11 +787,10 @@ test("toolbox status kickers, filters, card order, and voting controls work from await page.locator("[data-toolbox-status-filter='deprecated']").click(); } const userTwoBuildVotes = page.locator("[data-toolbox-tool-card='Build Game'] [data-toolbox-vote-controls='Build Game']"); + buildVoteState = await voteControlState(userTwoBuildVotes); await userTwoBuildVotes.locator("[data-toolbox-vote='up']").click(); - await expect(userTwoBuildVotes.locator("[data-toolbox-vote='up']")).toHaveText("Up 1"); - await expect(userTwoBuildVotes.locator("[data-toolbox-vote='down']")).toHaveText("Down 1"); - await expect(userTwoBuildVotes.locator("[data-toolbox-vote='up']")).toHaveAttribute("aria-pressed", "true"); - await expect(userTwoBuildVotes.locator("[data-toolbox-vote='up']")).toHaveClass(/primary/); + buildVoteState = applyVoteState(buildVoteState, "up"); + await expectVoteControlState(userTwoBuildVotes, buildVoteState); await setServerSession(server, MOCK_DB_KEYS.users.user1); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); @@ -681,15 +798,11 @@ test("toolbox status kickers, filters, card order, and voting controls work from await page.locator("[data-toolbox-status-filter='deprecated']").click(); } const userOneReturnedBuildVotes = page.locator("[data-toolbox-tool-card='Build Game'] [data-toolbox-vote-controls='Build Game']"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='up']")).toHaveText("Up 1"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='down']")).toHaveText("Down 1"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='down']")).toHaveAttribute("aria-pressed", "true"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='down']")).toHaveClass(/primary/); + buildVoteState = { ...buildVoteState, currentVote: "down" }; + await expectVoteControlState(userOneReturnedBuildVotes, buildVoteState); await userOneReturnedBuildVotes.locator("[data-toolbox-vote='up']").click(); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='up']")).toHaveText("Up 2"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='down']")).toHaveText("Down 0"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='up']")).toHaveAttribute("aria-pressed", "true"); - await expect(userOneReturnedBuildVotes.locator("[data-toolbox-vote='up']")).toHaveClass(/primary/); + buildVoteState = applyVoteState(buildVoteState, "up"); + await expectVoteControlState(userOneReturnedBuildVotes, buildVoteState); await setServerSession(server, MOCK_DB_KEYS.users.admin); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); @@ -701,7 +814,10 @@ test("toolbox status kickers, filters, card order, and voting controls work from await expect(plannedCard.locator("[data-toolbox-kicker]")).toHaveClass(/swatch-label/); await expect(plannedCard.locator("[data-toolbox-kicker]")).toHaveAttribute("title", STATUS_HELP_TEXT.planned); await expect(plannedCard.locator("[data-toolbox-vote-controls='Publish']")).toBeVisible(); - await plannedCard.locator("[data-toolbox-vote-controls='Publish'] [data-toolbox-vote='up']").click(); + const publishVotes = plannedCard.locator("[data-toolbox-vote-controls='Publish']"); + let publishVoteState = await voteControlState(publishVotes); + await publishVotes.locator("[data-toolbox-vote='up']").click(); + publishVoteState = applyVoteState(publishVoteState, "up"); await expect(page.locator("[data-toolbox-launch-status]")).toHaveText("Publish up vote recorded for Admin review."); await plannedCard.locator("[data-toolbox-tile-action-row='Publish'] a.btn").click(); await expect(page).toHaveURL(/\/toolbox\/index\.html$/); @@ -725,6 +841,11 @@ test("toolbox status kickers, filters, card order, and voting controls work from await expect(page.locator("[data-toolbox-tool-card='Colors'] .card-body > [data-toolbox-group-badge] [data-toolbox-group-label='Graphics']")).toHaveCSS("background-color", "rgb(255, 200, 87)"); await expect(page.locator("[data-toolbox-tool-card='Colors'] .card-body > [data-toolbox-state-badge]")).toHaveAttribute("data-toolbox-state-badge", "complete"); + const adminVoteSnapshot = await fetchApiData(server, "/api/toolbox/votes/snapshot"); + const adminBuildVoteState = voteStateFromSnapshot(adminVoteSnapshot, "build-game"); + const originalToolOrder = adminVoteSnapshot.rows.map((row) => row.toolId); + publishVoteState = voteStateFromSnapshot(adminVoteSnapshot, "publish"); + await page.goto(`${server.baseUrl}/admin/tool-votes.html`, { waitUntil: "networkidle" }); await expect(page.getByRole("heading", { level: 1, name: "Tool Votes" })).toBeVisible(); await expect(page.locator("[data-toolbox-votes-status]")).toContainText("DavidQ"); @@ -744,11 +865,22 @@ test("toolbox status kickers, filters, card order, and voting controls work from await expect(page.locator("[data-toolbox-votes-layout].tool-workspace.tool-workspace--wide")).toBeVisible(); await expect(page.locator("[data-toolbox-votes-layout] > .tool-column")).toHaveCount(2); await expect(page.locator("[data-admin-tool-menu] a")).toHaveText([ - "Tool Votes", - "Environments", + "Analytics", + "Controls", "Creators", + "DB Viewer", + "Environments", "Game Migration", + "Infrastructure", + "Invites", + "Moderation", + "Operations", "Platform Settings", + "Ratings", + "Responsibilities", + "Site Setup", + "System Health", + "Tool Votes", ]); await expect(page.locator("[data-toolbox-votes-width-toggle]")).toHaveCount(0); await expect(page.locator("[data-toolbox-votes-width-status]")).toHaveCount(0); @@ -773,12 +905,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from "Complete", "Deprecated", ]); - await expect(adminBuildVoteRow.locator("td").nth(5)).toHaveText("2"); - await expect(adminBuildVoteRow.locator("td").nth(6)).toHaveText("0"); - await expect(adminBuildVoteRow.locator("td").nth(7)).toHaveText("2"); - await expect(adminBuildVoteRow.locator("td").nth(8)).toHaveText("100%"); - await expect(adminBuildVoteRow.locator("td").nth(9)).toHaveText("0%"); - await expect(adminBuildVoteRow.locator("td").nth(10)).toHaveText("None"); + await expectAdminVoteRowState(adminBuildVoteRow, adminBuildVoteState); await adminBuildVoteRow.locator("td").nth(1).click(); await expect(adminBuildVoteRow).toHaveAttribute("aria-selected", "true"); await page.locator("[data-toolbox-votes-sort='toolName']").click(); @@ -815,11 +942,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from await expect(adminBuildVoteRow.locator("td").nth(1)).toHaveText("1"); await expect(page.locator("[data-toolbox-votes-tool-id='game-hub'] td").nth(1)).toHaveText("2"); await expect(adminBuildVoteRow).toHaveAttribute("aria-selected", "true"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(5)).toHaveText("1"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(7)).toHaveText("1"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(8)).toHaveText("100%"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(9)).toHaveText("0%"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(10)).toHaveText("up"); + await expectAdminVoteRowState(page.locator("[data-toolbox-votes-tool-id='publish']"), publishVoteState); const colorsVoteRow = page.locator("[data-toolbox-votes-tool-id='colors']"); await colorsVoteRow.click(); @@ -842,14 +965,11 @@ test("toolbox status kickers, filters, card order, and voting controls work from await expect(colorsBuildPathRow.locator("[data-build-path-tool-link='Colors']")).toHaveAttribute("href", /toolbox\/colors\/index\.html$/); await expect(page.locator("[data-route='admin-tool-votes']")).toHaveCount(1); - const mockDbToolboxTables = await page.evaluate(async () => { - const response = await fetch("/api/local-db/snapshot"); - const payload = await response.json(); - return { - metadata: payload.data.tables.toolbox_tool_metadata, - votes: payload.data.tables.toolbox_votes, - }; - }); + const productDataSnapshot = await fetchApiData(server, "/api/product-data/snapshot"); + const mockDbToolboxTables = { + metadata: productDataSnapshot.tables.toolbox_tool_metadata, + votes: productDataSnapshot.tables.toolbox_votes, + }; expect(mockDbToolboxTables.votes).toEqual(expect.arrayContaining([ expect.objectContaining({ direction: "up", @@ -875,13 +995,15 @@ test("toolbox status kickers, filters, card order, and voting controls work from toolId: "colors", }), ])); + await restoreColorsToolMetadata(server); + await postApiData(server, "/api/toolbox/votes/order-list", { toolIds: originalToolOrder }); const toolboxSource = await page.evaluate(async () => { const response = await fetch("/toolbox/index.html"); return response.text(); }); expect(toolboxSource).not.toMatch(/]+src=)[^>]*>/i); - expect(toolboxSource).not.toMatch(/]/i); + expect(toolboxSource).not.toMatch(INLINE_STYLE_TAG_PATTERN); expect(toolboxSource).not.toContain("onclick="); expect(failedRequests).toEqual([]); @@ -890,6 +1012,8 @@ test("toolbox status kickers, filters, card order, and voting controls work from } finally { await workspaceV2CoverageReporter.stop(page); await server.close(); + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); } }); @@ -1025,8 +1149,8 @@ test("toolbox grouped view renders Game Journey order with unique colors while B return response.text(); }); expect(toolboxSource).not.toMatch(/]+src=)[^>]*>/i); - expect(toolboxSource).not.toMatch(/]/i); - expect(toolboxSource).not.toMatch(/\sstyle=/i); + expect(toolboxSource).not.toMatch(INLINE_STYLE_TAG_PATTERN); + expect(toolboxSource).not.toMatch(INLINE_STYLE_ATTRIBUTE_PATTERN); expect(toolboxSource).not.toContain("onclick="); await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); @@ -1184,6 +1308,10 @@ test("Game Crew friendly route resolves while old Users route remains compatible test("toolbox Build Path status filters support multi-select registry-matched tool rows", async ({ page }) => { const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; + process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; const failedRequests = []; const pageErrors = []; const consoleErrors = []; @@ -1236,6 +1364,8 @@ test("toolbox Build Path status filters support multi-select registry-matched to try { await workspaceV2CoverageReporter.start(page); + await setServerSession(server, MOCK_DB_KEYS.users.admin); + await restoreColorsToolMetadata(server); await setServerSession(server, MOCK_DB_KEYS.users.user1); const registryById = await toolMetadataById(server); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); @@ -1248,9 +1378,9 @@ test("toolbox Build Path status filters support multi-select registry-matched to await expect(page.locator("[data-tools-sort='grouped']")).not.toHaveClass(/primary/); await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (28)", + "Planned (27)", "Wireframe (4)", - "Beta (6)", + "Beta (8)", "Complete (3)", "Deprecated (1)", ]); @@ -1261,32 +1391,32 @@ test("toolbox Build Path status filters support multi-select registry-matched to await page.locator("[data-toolbox-status-filter='planned']").click(); await expectActiveFilters(["planned", "complete"]); - await expectBuildPathChannels(["planned", "complete"], 31); + await expectBuildPathChannels(["planned", "complete"], 30); await expect(page.locator("[data-build-path-tool='AI Command Center']")).toBeVisible(); await expectBuildPathOrder("AI Command Center", registryById.get("ai-assistant").order); await expectBuildPathOrder("Colors", registryById.get("colors").order); await page.locator("[data-toolbox-status-filter='complete']").click(); await expectActiveFilters(["planned"]); - await expectBuildPathChannels(["planned"], 28); + await expectBuildPathChannels(["planned"], 27); await expect(page.locator("[data-build-path-tool='Colors']")).toHaveCount(0); await expect(page.locator("[data-build-path-tool='AI Command Center']")).toBeVisible(); await page.locator("[data-toolbox-status-filter='wireframe']").click(); await expectActiveFilters(["planned", "wireframe"]); - await expectBuildPathChannels(["planned", "wireframe"], 32); + await expectBuildPathChannels(["planned", "wireframe"], 31); await expect(page.locator("[data-build-path-tool='Saved Data']")).toBeVisible(); await expect(page.locator("[data-build-path-tool='Build Game']")).toHaveCount(0); await page.locator("[data-toolbox-status-filter='deprecated']").click(); await expectActiveFilters(["planned", "wireframe", "deprecated"]); - await expectBuildPathChannels(["planned", "wireframe", "deprecated"], 33); + await expectBuildPathChannels(["planned", "wireframe", "deprecated"], 32); await expect(page.locator("[data-build-path-tool='Build Game']")).toBeVisible(); await expectBuildPathOrder("Build Game", registryById.get("build-game").order); await page.locator("[data-toolbox-status-filter='beta']").click(); await expectActiveFilters(["planned", "wireframe", "beta", "deprecated"]); - await expectBuildPathChannels(["planned", "wireframe", "beta", "deprecated"], 39); + await expectBuildPathChannels(["planned", "wireframe", "beta", "deprecated"], 40); expect(failedRequests).toEqual([]); expect(pageErrors).toEqual([]); @@ -1294,11 +1424,17 @@ test("toolbox Build Path status filters support multi-select registry-matched to } finally { await workspaceV2CoverageReporter.stop(page); await server.close(); + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); } }); test("Colors Picker Preview header sort buttons reorder the grid", async ({ page }) => { const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; + process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; const failedRequests = []; const pageErrors = []; const consoleErrors = []; @@ -1350,7 +1486,7 @@ test("Colors Picker Preview header sort buttons reorder the grid", async ({ page if (child.hasAttribute("data-palette-preview-controls")) return "preview-controls"; if (child.querySelector("[data-palette-generator-preview-status]")) return "preview-status"; return child.textContent.trim(); - }) + }).filter(Boolean) )); expect(summaryOrder).toEqual(["Picker Preview", "preview-controls", "preview-status"]); @@ -1385,11 +1521,17 @@ test("Colors Picker Preview header sort buttons reorder the grid", async ({ page } finally { await workspaceV2CoverageReporter.stop(page); await server.close(); + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); } }); test("wireframe-only pages expose left center right accordion controls without runtime wiring", async ({ page }) => { const server = await startRepoServer(); + const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; + const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; + process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; + process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; const failedRequests = []; const pageErrors = []; const consoleErrors = []; @@ -1438,7 +1580,7 @@ test("wireframe-only pages expose left center right accordion controls without r return response.text(); }); expect(source).not.toMatch(/]+src=)[^>]*>/i); - expect(source).not.toMatch(/]/i); + expect(source).not.toMatch(INLINE_STYLE_TAG_PATTERN); expect(source).not.toMatch(/\son(?:click|change|input|submit|keydown|keyup|load)=/i); } @@ -1448,28 +1590,17 @@ test("wireframe-only pages expose left center right accordion controls without r } finally { await workspaceV2CoverageReporter.stop(page); await server.close(); + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); } }); -test("local dev port guard redirects human localhost pages to port 5501", async ({ page }) => { +test("local dev pages remain on the repo test server", async ({ page }) => { const server = await startRepoServer(); try { await workspaceV2CoverageReporter.start(page); - await page.addInitScript(() => { - Object.defineProperty(Navigator.prototype, "webdriver", { - configurable: true, - get: () => false, - }); - }); - await page.route("http://127.0.0.1:5501/**", async (route) => { - await route.fulfill({ - body: "
Port guard target
", - contentType: "text/html", - status: 200, - }); - }); await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "domcontentloaded" }); - await expect(page).toHaveURL(/http:\/\/127\.0\.0\.1:5501\/toolbox\/index\.html$/); + expect(new URL(page.url()).origin).toBe(server.baseUrl); } finally { await workspaceV2CoverageReporter.stop(page); await server.close(); diff --git a/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs b/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs index e96e17752..57c52fedd 100644 --- a/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs +++ b/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs @@ -230,6 +230,11 @@ test("shared toolbox status bar shows selected Game Hub game above the footer", try { const statusBar = page.locator("[data-toolbox-status-bar]"); await expect(statusBar).toBeVisible(); + const displayMode = page.locator("#toolDisplayMode"); + await expect(displayMode.locator("summary [data-theme-icon='fullscreen']")).toHaveAttribute("data-theme-icon-file", "gfs-fullscreen.svg"); + await expect(displayMode.locator("[data-tool-nav-previous] [data-theme-icon='chevron-left']")).toHaveAttribute("data-theme-icon-file", "gfs-chevron-left.svg"); + await expect(displayMode.locator("[data-tool-nav-next] [data-theme-icon='chevron-right']")).toHaveAttribute("data-theme-icon-file", "gfs-chevron-right.svg"); + await expect(page.locator(".horizontal-accordion-toggle").first().locator("[data-theme-icon]")).toHaveAttribute("data-theme-icon-file", /gfs-chevron-(left|right)\.svg/); await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); await expect(statusBar).not.toContainText("Environment"); await expectRemovedStatusBarLabelsHidden(statusBar); @@ -326,6 +331,7 @@ test("shared toolbox status bar anchors to the bottom in tool display mode", asy await expect(page.locator("[data-toolbox-status-bar]")).toBeVisible(); await page.locator("#toolDisplayMode summary").click(); await expect(page.locator("body")).toHaveClass(/tool-focus-mode/); + await expect(page.locator("#toolDisplayMode summary [data-theme-icon='exit-fullscreen']")).toHaveAttribute("data-theme-icon-file", "gfs-exit-fullscreen.svg"); const snapshot = await statusBarSnapshot(page); expect(snapshot.position).toBe("fixed");