Skip to content

Commit 2c761c1

Browse files
committed
Fix Colors Symbol validation and picker grid columns - PR_26159_032-colors-symbol-grid-fix
1 parent 54b6a5b commit 2c761c1

5 files changed

Lines changed: 203 additions & 31 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# PR_26159_032-colors-symbol-grid-fix Report
2+
3+
Playwright impacted: Yes
4+
5+
## Summary
6+
7+
Fixed the Colors picker grid so available and unavailable picker sections preserve the generated column count when swatches move between sections. Added regression coverage proving active `tools.palette-browser.swatches` validation and Colors add/remove/pin/unpin/update/clear flows work without Symbol.
8+
9+
## Root Cause: Symbol
10+
11+
Active `tools.palette-browser.swatches` validation is owned by `src/dev-runtime/persistence/tool-repositories/palette-workspace-repository.js` through `validatePaletteWorkspacePayload()` and `validatePaletteSwatchInput()`. That path requires `key`, `hex`, and `name`; it does not require Symbol. The blocking regression was not covered by a direct no-Symbol payload/lifecycle assertion, so stale or adjacent validation could reappear unnoticed.
12+
13+
Exact fixed file:
14+
- `tests/playwright/tools/PaletteToolMockRepository.spec.mjs`
15+
16+
Evidence:
17+
- Added direct `validatePaletteWorkspacePayload()` coverage for a `tools.palette-browser.swatches` payload with no Symbol field.
18+
- Added repository lifecycle coverage for add, update, remove, pin, unpin, and clear with no Symbol field.
19+
- Active Symbol scan found no active Colors `Symbol: Enter a symbol`, `data-palette-symbol`, `requireSymbol`, `swatchSymbol`, or `paletteSwatchSymbol` requirement. The only remaining hit is `tests/tools/PaletteManagerV2Baseline.test.mjs`, which is a separate Palette Manager V2 baseline test and not active Colors `palette-browser` validation.
20+
21+
## Root Cause: Picker Grid
22+
23+
`appendPickerGroupRows()` previously rendered rows from filtered group arrays. When one generated swatch moved from Available to Already in Project, that cell disappeared from the Available row, so rows could collapse below the selected Colors column count.
24+
25+
Exact fixed file:
26+
- `toolbox/colors/colors.js`
27+
28+
Fix:
29+
- `appendPickerGroupRows()` now receives the full generated swatch set, filters per section for counts, and inserts placeholder cells for positions owned by the other section.
30+
- Available and unavailable rows now keep `settings.colors` cells per rendered row.
31+
- Unavailable swatches remain true color, are not disabled, do not use a not-allowed cursor, and still block add-clicks through the existing availability guard.
32+
33+
## Requirement Checklist
34+
35+
| Requirement | Status | Evidence |
36+
|---|---|---|
37+
| Remove remaining active Symbol requirement from `tools.palette-browser.swatches` validation | PASS | `validatePaletteWorkspacePayload()` no-Symbol assertion in `tests/playwright/tools/PaletteToolMockRepository.spec.mjs`; targeted Symbol scan |
38+
| Add works without Symbol | PASS | Playwright `Palette Tool adds, updates, validates, and shows project-owned swatches` |
39+
| Remove works without Symbol | PASS | Repository lifecycle assertion for `removeSwatch()` |
40+
| Pin works without Symbol | PASS | Repository lifecycle assertion for `pinSourceSwatch()` and Playwright picker add |
41+
| Unpin works without Symbol | PASS | Repository lifecycle assertion removing a pinned source swatch |
42+
| Update works without Symbol | PASS | Playwright update flow and repository `updateSelectedSwatch()` assertion |
43+
| Clear works without Symbol | PASS | Playwright Clear control and repository `clearProjectData()` assertion |
44+
| Search active source/schemas/validators/fixtures for Symbol | PASS | `rg` scan; only out-of-scope Palette Manager V2 baseline hit remains |
45+
| Colors = 8 renders exactly 8 columns per picker row | PASS | New Playwright test `Palette Tool preserves eight-column picker rows when swatches are unavailable` |
46+
| Missing/unavailable/already-in-project swatches do not collapse grid | PASS | Placeholder cells in `toolbox/colors/colors.js`; Playwright row child-count assertions |
47+
| Available and unavailable sections preserve column count | PASS | Playwright asserts 8-column and 16-column available/unavailable rows |
48+
| Unavailable swatches keep true color and occupy cells | PASS | Playwright opacity/value assertions |
49+
| No red dot/not-allowed cursor for unavailable swatches | PASS | Playwright cursor assertions |
50+
| Unavailable swatches do not add when clicked | PASS | Playwright count remains unchanged and log reports not added |
51+
| Available swatches add when clicked | PASS | Playwright count increases after available picker click |
52+
| Preserve PR025 available top/unavailable bottom viewer | PASS | Existing Available/Already in Project labels and sections preserved |
53+
| Preserve current controls/options and ROYGBIV | PASS | Existing selector/ROYGBIV Playwright assertions still pass |
54+
| Preserve Project Swatches and Picker Swatches accordions | PASS | Existing accordion assertions still pass |
55+
| No console errors | PASS | `expectNoPageFailures()` across targeted Colors Playwright lane |
56+
57+
## Validation
58+
59+
| Lane | Status | Evidence |
60+
|---|---|---|
61+
| Changed JS syntax | PASS | `node --check toolbox/colors/colors.js`; `node --check tests/playwright/tools/PaletteToolMockRepository.spec.mjs` |
62+
| Targeted Colors Playwright | PASS | `npx playwright test tests/playwright/tools/PaletteToolMockRepository.spec.mjs` -> 7 passed |
63+
| Changed-file static validation | PASS | `git diff --check -- toolbox/colors/colors.js tests/playwright/tools/PaletteToolMockRepository.spec.mjs` |
64+
| Active Symbol scan | PASS | No active Colors Symbol requirement found; out-of-scope Palette Manager V2 baseline hit documented |
65+
66+
## Skipped Lanes
67+
68+
Full samples validation was skipped because this PR only changes the Colors tool runtime and its targeted Playwright coverage. No sample loader, shared sample framework, or production game sample path changed.
69+
70+
Full repo Playwright was skipped because the impacted lane is the Colors/Palette Tool lane and no shared Theme V2, server API, DB adapter, or cross-tool integration behavior changed.
71+
72+
## Artifacts
73+
74+
- Review diff: `docs_build/dev/reports/codex_review.diff`
75+
- Changed files: `docs_build/dev/reports/codex_changed_files.txt`
76+
- ZIP: `tmp/PR_26159_032-colors-symbol-grid-fix_delta.zip`

docs_build/dev/reports/coverage_changed_js_guardrail.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Missing changed runtime JS files are WARN, not FAIL.
66
Source: Playwright/Chromium built-in V8 coverage from the active Playwright run.
77

88
Changed runtime JS files considered:
9-
(85%) toolbox/colors/colors.js - executed lines 1938/1938; executed functions 165/194
9+
(85%) toolbox/colors/colors.js - executed lines 1953/1953; executed functions 165/194
1010
(100%) toolbox/colors/palette-api-client.js - executed lines 19/19; executed functions 4/4
1111

1212
Guardrail warnings:

docs_build/dev/reports/playwright_v8_coverage_report.txt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,21 @@ Exercised tool entry points detected:
1717
(64%) Theme V2 Shared JS - exercised 2 runtime JS files
1818

1919
Changed runtime JS files covered:
20-
(85%) toolbox/colors/colors.js - executed lines 1938/1938; executed functions 165/194
20+
(85%) toolbox/colors/colors.js - executed lines 1953/1953; executed functions 165/194
2121
(100%) toolbox/colors/palette-api-client.js - executed lines 19/19; executed functions 4/4
2222

2323
Files with executed line/function counts where available:
2424
(58%) src/engine/api/server-api-client.js - executed lines 159/159; executed functions 11/19
2525
(64%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 525/525; executed functions 28/44
2626
(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 201/201; executed functions 9/14
2727
(78%) toolbox/tool-registry-api-client.js - executed lines 148/148; executed functions 21/27
28-
(85%) toolbox/colors/colors.js - executed lines 1938/1938; executed functions 165/194
28+
(85%) toolbox/colors/colors.js - executed lines 1953/1953; executed functions 165/194
2929
(100%) toolbox/colors/palette-api-client.js - executed lines 19/19; executed functions 4/4
3030

3131
Uncovered or low-coverage changed JS files:
3232
(100%) none - no low-coverage changed runtime JS files
3333

3434
Changed JS files considered:
35-
(0%) assets/theme-v2/js/account-achievements.js - changed JS file not collected as browser runtime coverage
36-
(0%) tests/playwright/account/AchievementsPage.spec.mjs - changed JS file not collected as browser runtime coverage
3735
(0%) tests/playwright/tools/PaletteToolMockRepository.spec.mjs - changed JS file not collected as browser runtime coverage
3836
(85%) toolbox/colors/colors.js - changed JS file with browser V8 coverage
3937
(100%) toolbox/colors/palette-api-client.js - changed JS file with browser V8 coverage

tests/playwright/tools/PaletteToolMockRepository.spec.mjs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { expect, test } from "@playwright/test";
22
import {
33
PALETTE_WORKSPACE_PATH,
44
PALETTE_TOOL_TABLES,
5-
createProjectWorkspacePaletteRepository
5+
createProjectWorkspacePaletteRepository,
6+
validatePaletteWorkspacePayload
67
} from "../../../src/dev-runtime/persistence/tool-repositories/palette-workspace-repository.js";
78
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
89
import { clearPlaywrightStorage, installPlaywrightStorageIsolation } from "../../helpers/playwrightStorageIsolation.mjs";
@@ -145,11 +146,19 @@ async function addUserSwatch(page, swatch) {
145146
}
146147

147148
async function currentPreviewHexes(page, row) {
148-
return page.locator(`[data-palette-generator-preview-row='${row}'] [data-palette-generator-color]`).evaluateAll((swatches) => (
149+
return page.locator(`[data-palette-picker-group='available'][data-palette-generator-preview-row='${row}'] [data-palette-generator-color]`).evaluateAll((swatches) => (
149150
swatches.map((swatch) => swatch.value.toUpperCase())
150151
));
151152
}
152153

154+
async function expectPickerRowsToHaveColumnCount(page, group, columns) {
155+
const counts = await page.locator(`[data-palette-picker-group='${group}']`).evaluateAll((rows) => (
156+
rows.map((row) => row.children.length)
157+
));
158+
expect(counts.length).toBeGreaterThan(0);
159+
expect(counts).toEqual(counts.map(() => columns));
160+
}
161+
153162
async function visiblePaletteNames(page) {
154163
return page.locator("[data-palette-user-list] [data-palette-swatch-key]").evaluateAll((swatches) => (
155164
swatches.map((swatch) => swatch.dataset.paletteSwatchName)
@@ -218,6 +227,39 @@ test("Palette repository owns project swatches and protects invalid payloads", a
218227
expect(repository.findSwatch("reference-red")).toMatchObject({ name: "Reference Red", tags: ["batch"] });
219228
expect(repository.findSwatch("reference-green")).toBeNull();
220229

230+
const noSymbolPayloadValidation = validatePaletteWorkspacePayload({
231+
tools: {
232+
"palette-browser": {
233+
swatches: [
234+
{ key: "symbol-free-swatch", hex: "#224466", name: "Symbol Free Swatch", tags: [] }
235+
]
236+
}
237+
}
238+
});
239+
expect(noSymbolPayloadValidation.valid).toBe(true);
240+
expect(noSymbolPayloadValidation.normalized.tools["palette-browser"].swatches[0]).toMatchObject({
241+
hex: "#224466",
242+
key: "symbol-free-swatch",
243+
name: "Symbol Free Swatch"
244+
});
245+
246+
const lifecycleRepository = createProjectWorkspacePaletteRepository({ sourcePaletteRows });
247+
const lifecycleAdd = lifecycleRepository.addSwatch({ hex: "#224466", name: "Lifecycle Blue" });
248+
expect(lifecycleAdd.ok).toBe(true);
249+
const lifecycleKey = lifecycleAdd.snapshot.selectedSwatch.key;
250+
expect(lifecycleRepository.updateSelectedSwatch({ hex: "#224477", name: "Lifecycle Updated" }).ok).toBe(true);
251+
expect(lifecycleRepository.findSwatch(lifecycleKey)).toMatchObject({ hex: "#224477", name: "Lifecycle Updated" });
252+
expect(lifecycleRepository.removeSwatch(lifecycleKey).ok).toBe(true);
253+
expect(lifecycleRepository.findSwatch(lifecycleKey)).toBeNull();
254+
const lifecycleSourceSwatch = lifecycleRepository.listSourceSwatches({ sourceId: "reference" })[0];
255+
expect(lifecycleRepository.pinSourceSwatch(lifecycleSourceSwatch).ok).toBe(true);
256+
expect(lifecycleRepository.findSwatch(lifecycleSourceSwatch.key)).toMatchObject({ name: lifecycleSourceSwatch.name });
257+
expect(lifecycleRepository.removeSwatch(lifecycleSourceSwatch.key).ok).toBe(true);
258+
expect(lifecycleRepository.findSwatch(lifecycleSourceSwatch.key)).toBeNull();
259+
lifecycleRepository.addSwatch({ hex: "#667788", name: "Clearable Swatch" });
260+
lifecycleRepository.clearProjectData();
261+
expect(lifecycleRepository.getTables().palette_colors).toEqual([]);
262+
221263
const invalidPayload = {
222264
tools: {
223265
"palette-browser": {
@@ -507,6 +549,44 @@ test("Palette Tool renders curated swatch selector controls and live preview", a
507549
}
508550
});
509551

552+
test("Palette Tool preserves eight-column picker rows when swatches are unavailable", async ({ page }) => {
553+
const failures = await openRepoPage(page, "/toolbox/colors/index.html");
554+
555+
try {
556+
await page.locator("[data-palette-generator-colors]").selectOption("8");
557+
await page.locator("[data-palette-generator-steps]").selectOption("2");
558+
await expectPickerRowsToHaveColumnCount(page, "available", 8);
559+
const firstAvailableEightColumnSwatch = page.locator("[data-palette-generator-swatch][data-palette-generator-availability='available']").first();
560+
const eightColumnHex = await firstAvailableEightColumnSwatch.getAttribute("data-palette-generator-hex");
561+
await firstAvailableEightColumnSwatch.click();
562+
await expect(page.locator("[data-palette-count]")).toHaveText("1");
563+
564+
await expectPickerRowsToHaveColumnCount(page, "available", 8);
565+
await expectPickerRowsToHaveColumnCount(page, "unavailable", 8);
566+
const unavailableEightColumnSwatch = page.locator(`[data-palette-generator-swatch][data-palette-generator-hex='${eightColumnHex}'][data-palette-generator-availability='unavailable']`);
567+
await expect(unavailableEightColumnSwatch).toHaveAttribute("data-palette-generator-unavailable-reason", "Already in Project");
568+
const unavailableEightColumnVisual = await unavailableEightColumnSwatch.evaluate((swatch) => ({
569+
cursor: getComputedStyle(swatch).cursor,
570+
opacity: getComputedStyle(swatch).opacity,
571+
value: swatch.querySelector("input[type='color']").value
572+
}));
573+
expect(unavailableEightColumnVisual.cursor).not.toBe("not-allowed");
574+
expect(unavailableEightColumnVisual.opacity).toBe("1");
575+
expect(unavailableEightColumnVisual.value.toUpperCase()).toBe((eightColumnHex || "").slice(0, 7).toUpperCase());
576+
577+
await unavailableEightColumnSwatch.click();
578+
await expect(page.locator("[data-palette-count]")).toHaveText("1");
579+
await expect(page.locator("[data-palette-log]")).toContainText("Already in Project picker swatch was not added");
580+
await page.locator("[data-palette-generator-swatch][data-palette-generator-availability='available']").first().click();
581+
await expect(page.locator("[data-palette-count]")).toHaveText("2");
582+
583+
expectNoPageFailures(failures);
584+
} finally {
585+
await workspaceV2CoverageReporter.stop(page);
586+
await failures.server.close();
587+
}
588+
});
589+
510590
test("Palette Tool generated grid swatches can be selected, pinned, and refreshed", async ({ page }) => {
511591
test.setTimeout(120000);
512592
const failures = await openRepoPage(page, "/toolbox/colors/index.html");
@@ -595,6 +675,8 @@ test("Palette Tool generated grid swatches can be selected, pinned, and refreshe
595675
await expect(page.locator("[data-palette-picker-group-label='available']")).toHaveText("Available Picker Swatches (63)");
596676
await expect(page.locator("[data-palette-picker-group-label='unavailable']")).toHaveText("Already in Project (1)");
597677
await expect(page.locator("[data-palette-generator-swatch][data-palette-generator-unavailable='true']")).toHaveCount(1);
678+
await expectPickerRowsToHaveColumnCount(page, "available", 16);
679+
await expectPickerRowsToHaveColumnCount(page, "unavailable", 16);
598680
const unavailableVisual = await unavailablePickerSwatch.evaluate((swatch) => ({
599681
cursor: getComputedStyle(swatch).cursor,
600682
opacity: getComputedStyle(swatch).opacity,

toolbox/colors/colors.js

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,20 @@ function createPickerGroupLabel(group, count) {
989989
return label;
990990
}
991991

992-
function appendPickerGroupRows(fragment, group, items, settings) {
992+
function createPickerGridPlaceholder(row, column) {
993+
const placeholder = document.createElement("span");
994+
placeholder.className = "palette-generator-preview-swatch";
995+
placeholder.dataset.paletteGeneratorPlaceholder = "true";
996+
placeholder.dataset.paletteGeneratorRow = String(row);
997+
placeholder.dataset.paletteGeneratorColumn = String(column);
998+
placeholder.setAttribute("aria-hidden", "true");
999+
return placeholder;
1000+
}
1001+
1002+
function appendPickerGroupRows(fragment, group, allSwatches, settings) {
1003+
const items = allSwatches.filter((item) => (
1004+
group === "available" ? item.available : !item.available
1005+
));
9931006
fragment.append(createPickerGroupLabel(group, items.length));
9941007
if (!items.length) {
9951008
const message = document.createElement("p");
@@ -1004,28 +1017,33 @@ function appendPickerGroupRows(fragment, group, items, settings) {
10041017

10051018
const rows = new Map();
10061019
items.forEach((item) => {
1007-
const rowItems = rows.get(item.row) || [];
1008-
rowItems.push(item);
1020+
const rowItems = rows.get(item.row) || new Map();
1021+
rowItems.set(item.column, item);
10091022
rows.set(item.row, rowItems);
10101023
});
1011-
[...rows.entries()].forEach(([row, rowItems]) => {
1012-
const rowElement = document.createElement("div");
1013-
rowElement.className = "palette-generator-preview-row";
1014-
rowElement.setAttribute("role", "row");
1015-
rowElement.dataset.paletteGeneratorPreviewRow = String(row);
1016-
rowElement.dataset.palettePickerGroup = group;
1017-
rowItems.forEach((item) => {
1018-
rowElement.append(createGeneratorPreviewInput(item.hex, settings.paletteType.name, item.row, item.column, settings, {
1019-
available: item.available,
1020-
pinned: Boolean(item.pinnedSwatch),
1021-
pinnedSwatch: item.pinnedSwatch,
1022-
selected: item.selected,
1023-
unavailableReason: item.unavailableReason,
1024-
pickerSettings: currentPickerSettings(settings)
1025-
}));
1024+
[...rows.entries()]
1025+
.sort(([left], [right]) => left - right)
1026+
.forEach(([row, rowItems]) => {
1027+
const rowElement = document.createElement("div");
1028+
rowElement.className = "palette-generator-preview-row";
1029+
rowElement.setAttribute("role", "row");
1030+
rowElement.dataset.paletteGeneratorPreviewRow = String(row);
1031+
rowElement.dataset.palettePickerGroup = group;
1032+
for (let column = 0; column < settings.colors; column += 1) {
1033+
const item = rowItems.get(column);
1034+
rowElement.append(item
1035+
? createGeneratorPreviewInput(item.hex, settings.paletteType.name, item.row, item.column, settings, {
1036+
available: item.available,
1037+
pinned: Boolean(item.pinnedSwatch),
1038+
pinnedSwatch: item.pinnedSwatch,
1039+
selected: item.selected,
1040+
unavailableReason: item.unavailableReason,
1041+
pickerSettings: currentPickerSettings(settings)
1042+
})
1043+
: createPickerGridPlaceholder(row, column));
1044+
}
1045+
fragment.append(rowElement);
10261046
});
1027-
fragment.append(rowElement);
1028-
});
10291047
}
10301048

10311049
function paletteGeneratorSummary(settings) {
@@ -1076,11 +1094,9 @@ function renderPaletteGeneratorPreview(action = "Palette generator preview updat
10761094
}
10771095
}
10781096

1079-
const availableSwatches = allSwatches.filter((swatch) => swatch.available);
1080-
const unavailableSwatches = allSwatches.filter((swatch) => !swatch.available);
10811097
const fragment = document.createDocumentFragment();
1082-
appendPickerGroupRows(fragment, "available", availableSwatches, settings);
1083-
appendPickerGroupRows(fragment, "unavailable", unavailableSwatches, settings);
1098+
appendPickerGroupRows(fragment, "available", allSwatches, settings);
1099+
appendPickerGroupRows(fragment, "unavailable", allSwatches, settings);
10841100

10851101
elements.generatorPreview.replaceChildren(fragment);
10861102
setText(elements.generatorPreviewStatus, paletteGeneratorSummary(settings));

0 commit comments

Comments
 (0)