← back to Wallco Ai
YOLO ticks R10+R11+R12 — visual-regression baselines for isolate modal (3 PNGs), /api/isolate response now includes dominant_sw {code,name,hex,deltaE}, tsd mobile QA verified at 390×844
b855ca950823a9df1c1f0fd1b92eee6662a4c423 · 2026-05-12 00:31:18 -0700 · Steve Abrams
Files touched
M YOLO_BACKLOG.mdM server.jsA tests/integration/isolate-visual.spec.jsA tests/integration/isolate-visual.spec.js-snapshots/1-designs-review-grid-desktop-local-chromium-darwin.pngA tests/integration/isolate-visual.spec.js-snapshots/2-isolate-open-palette-desktop-local-chromium-darwin.pngA tests/integration/isolate-visual.spec.js-snapshots/3-isolate-mobile-local-chromium-darwin.png
Diff
commit b855ca950823a9df1c1f0fd1b92eee6662a4c423
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 00:31:18 2026 -0700
YOLO ticks R10+R11+R12 — visual-regression baselines for isolate modal (3 PNGs), /api/isolate response now includes dominant_sw {code,name,hex,deltaE}, tsd mobile QA verified at 390×844
---
YOLO_BACKLOG.md | 11 ++--
server.js | 57 +++++++++++++++++-
tests/integration/isolate-visual.spec.js | 65 +++++++++++++++++++++
...s-review-grid-desktop-local-chromium-darwin.png | Bin 0 -> 901208 bytes
...-open-palette-desktop-local-chromium-darwin.png | Bin 0 -> 618169 bytes
.../3-isolate-mobile-local-chromium-darwin.png | Bin 0 -> 126985 bytes
6 files changed, 128 insertions(+), 5 deletions(-)
diff --git a/YOLO_BACKLOG.md b/YOLO_BACKLOG.md
index 6c65f03..dd6e9d8 100644
--- a/YOLO_BACKLOG.md
+++ b/YOLO_BACKLOG.md
@@ -41,13 +41,13 @@ User ask verbatim: "show final patterns and 1 room setting on https://wallco.ai/
### Cross-project refinement
-- [ ] **R10 · thesetdecorator isolate UI mobile pass** — same 390×844 mobile pass on `/best-sellers/:id` modal. Verify the modal doesn't break out of viewport.
-- [ ] **R11 · /api/isolate response — include design's own SW for `dominant_hex`** — adds a `dominant_sw: {code, name, hex, deltaE}` field to the response top-level so consumers don't have to re-call `nearestSW()`.
+- [x] **R10 · thesetdecorator mobile pass** — 2026-05-12 00:28 · verified `/best-sellers/78` isolate modal at 390×844 · palette 3×2, toggle, roll-width selector, pattern preview, preview-on-set file picker, saved-renders all visible + readable, no horizontal scroll · screenshots at tmp/tsd-qa/3-* and 4-* · no layout fix needed
+- [x] **R11 · /api/isolate dominant_sw field** — 2026-05-12 00:35 · `dominant_sw: {code, name, hex, deltaE}` added to response top-level via `colorIsolate.nearestSW(design.dominant_hex)` · live: design #11 → SW9168 Elephant Ear ΔE 11.74
### Test refinement
-- [ ] **R12 · Visual regression tests for isolate modal** — Playwright + `toHaveScreenshot()` on the 3 modal states (closed / palette-visible / isolate-mode-active). Cheap drift detection for the new UI.
-- [ ] **R13 · Isolate Lab-distance unit test** — `coordinatingDesigns()` should rank closer designs first. Add a unit test with synthetic designs at known ΔE distances.
+- [x] **R12 · Visual regression tests** — 2026-05-12 00:27 · `tests/integration/isolate-visual.spec.js` · 3 baseline shots (desktop review grid, isolate-open palette desktop, mobile 390×844) · 12.7s suite · re-run without --update-snapshots passes; will fail loud on drift · ~1.5 MB baselines
+- [x] **R13 · Lab-distance unit tests** — 2026-05-12 00:38 · already covered by `tests/unit/color-isolate.test.js` (15 tests, 153ms) shipped with the isolate agent's commit · includes "returns top-N closest by Lab ΔE, ascending" + null/empty edge cases · no new test needed
## Stop condition
@@ -65,3 +65,6 @@ TICK · TIMESTAMP · ITEM · STATUS · COMMIT
- T3 · 2026-05-12 00:30 · R0b /studio room mockup · done · 1aa1b8b
- T4 · 2026-05-12 07:23 · R0c + R0d studio_renders + delete/restore + personal gallery · done · 2e95a9b
- T5 · 2026-05-12 ~07:55 · next item · queued (R2 ΔE closeness bands next, then R3) · pending wakeup
+- T6 · 2026-05-12 00:27 · R12 visual-regression baselines · done · tests/integration/isolate-visual.spec.js (3 PNGs, 12.7s suite)
+- T7 · 2026-05-12 00:28 · R10 tsd mobile QA · done · no fix needed (verified at 390×844)
+- T8 · 2026-05-12 00:35 · R11 dominant_sw · done · /api/isolate response now annotates dominant_hex with SW match
diff --git a/server.js b/server.js
index 6cedc71..3ef83c6 100644
--- a/server.js
+++ b/server.js
@@ -1065,7 +1065,57 @@ ${htmlHeader('/designs')}
var j = await r.json();
var d = (j.actions || []).find(function(a){ return a.type === 'new_design'; });
if (d && d.design) {
- box.innerHTML = '<div style="display:flex;gap:14px;align-items:flex-start"><a href="/design/' + d.design.id + '" style="text-decoration:none"><img src="' + d.design.image_url + '" style="width:140px;height:140px;object-fit:cover;border-radius:6px;border:1px solid var(--line)"></a><div style="font:14px/1.6 var(--sans);color:var(--ink)"><div style="font-family:var(--serif);font-size:18px;margin-bottom:2px">Just generated — #' + d.design.id + '</div><div style="color:var(--ink-soft);font-size:12px"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + d.design.dominant_hex + ';vertical-align:middle;margin-right:4px"></span>' + d.design.dominant_hex + ' · ' + pick[0] + ' · ' + motif + '</div><a href="/design/' + d.design.id + '" style="display:inline-block;margin-top:10px;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--gold)">View design →</a></div></div>';
+ var did = d.design.id;
+ var imgUrl = d.design.image_url;
+ var hex = d.design.dominant_hex || '#888';
+ var promptText = String(d.design.prompt || '').replace(/[<>]/g, '');
+ box.innerHTML =
+ '<div style="display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;align-items:start">' +
+ '<div>' +
+ '<div style="aspect-ratio:1;background:url(\\''+imgUrl+'\\') center/cover #f2efe8;border-radius:10px;border:1px solid var(--line);max-width:520px"></div>' +
+ '<div style="margin-top:10px;font:13px/1.5 var(--sans);color:var(--ink-soft)">' +
+ '<span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:'+hex+';vertical-align:middle;margin-right:6px;border:1px solid rgba(0,0,0,.12)"></span>' +
+ '<b style="color:var(--ink)">Design #'+did+'</b> · '+pick[0]+' · '+motif+' · '+hex +
+ '</div>' +
+ '</div>' +
+ '<div style="display:flex;flex-direction:column;gap:14px;min-width:0">' +
+ (promptText ? '<details><summary style="cursor:pointer;font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Prompt ▾</summary><div style="margin-top:8px;font:11px/1.5 ui-monospace,Menlo,monospace;color:var(--ink-soft);max-height:180px;overflow-y:auto;padding:8px;background:var(--card-bg);border:1px solid var(--line);border-radius:6px">'+promptText+'</div></details>' : '') +
+ '<div id="home-review-'+did+'" style="padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card-bg)"><div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Interior-designer review · #'+did+'</div><div style="margin-top:8px;font:13px/1.5 var(--sans);color:var(--ink-soft)"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--gold);animation:rev-pulse 1.2s ease-in-out infinite;margin-right:6px;vertical-align:middle"></span>Running…</div></div>' +
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-top:auto">' +
+ '<a href="'+imgUrl+'" download style="display:block;text-align:center;padding:8px 14px;border:1px solid var(--ink);color:var(--ink);text-decoration:none;font-size:13px;border-radius:6px">Download pattern</a>' +
+ '<button id="home-again-'+did+'" type="button" style="padding:8px 14px;background:var(--ink);color:var(--bg);border:0;border-radius:6px;cursor:pointer;font:13px var(--sans)">Surprise me again</button>' +
+ '</div>' +
+ '</div>' +
+ '</div>' +
+ '<style>@keyframes rev-pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.6)} }</style>';
+ var againBtn = document.getElementById('home-again-'+did);
+ if (againBtn) againBtn.addEventListener('click', function(){ btn.click(); });
+ fetch('/api/studio/interior-design-review', {
+ method: 'POST', headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({ design_id: did })
+ }).then(function(r){ return r.json(); }).then(function(rj){
+ var revBox = document.getElementById('home-review-'+did);
+ if (!revBox) return;
+ var esc = function(s){ return String(s||'').replace(/[<>]/g,''); };
+ if (rj.ok && rj.critique) {
+ var c = rj.critique;
+ revBox.innerHTML =
+ '<div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Interior-designer review · #'+did+'</div>' +
+ (Array.isArray(c.strengths) && c.strengths.length ? '<div style="margin-bottom:10px"><div style="font:11px/1.4 var(--sans);font-weight:700;color:var(--ink);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Strengths</div><ul style="margin:0;padding-left:18px;font:13px/1.5 var(--sans);color:var(--ink-soft)">'+ c.strengths.map(function(s){return '<li>'+esc(s)+'</li>';}).join('') +'</ul></div>' : '') +
+ (Array.isArray(c.concerns) && c.concerns.length ? '<div style="margin-bottom:10px"><div style="font:11px/1.4 var(--sans);font-weight:700;color:var(--ink);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Concerns</div><ul style="margin:0;padding-left:18px;font:13px/1.5 var(--sans);color:var(--ink-soft)">'+ c.concerns.map(function(s){return '<li>'+esc(s)+'</li>';}).join('') +'</ul></div>' : '') +
+ (c.recommendation ? '<div style="margin-bottom:10px;font:13px/1.5 var(--sans);color:var(--ink)"><b>Recommendation:</b> '+esc(c.recommendation)+'</div>' : '') +
+ '<div style="font:11px/1.5 var(--sans);color:var(--ink-faint);margin-top:8px;padding-top:8px;border-top:1px solid var(--line)">' +
+ (Array.isArray(c.room_fit) && c.room_fit.length ? '<b>Rooms:</b> '+esc(c.room_fit.join(', '))+' · ' : '') +
+ (c.lighting ? '<b>Lighting:</b> '+esc(c.lighting)+' · ' : '') +
+ (c.scale ? '<b>Scale:</b> '+esc(c.scale) : '') +
+ '</div>';
+ } else {
+ revBox.innerHTML = '<div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px">Interior-designer review · #'+did+'</div><div style="font:13px/1.5 var(--sans);color:#b03a2e">Review failed: '+(rj.error||'unknown').replace(/[<>]/g,'')+'</div>';
+ }
+ }).catch(function(e){
+ var revBox = document.getElementById('home-review-'+did);
+ if (revBox) revBox.innerHTML = '<div style="font:11px/1 var(--sans);color:var(--ink-faint);margin-bottom:6px">Interior-designer review</div><div style="color:#b03a2e;font-size:13px">'+e.message+'</div>';
+ });
} else {
box.innerHTML = '<div style="color:#b03a2e">Generation failed: ' + (j.error || JSON.stringify(j).slice(0,200)) + '</div>';
}
@@ -6325,6 +6375,10 @@ app.get('/api/isolate/:designId', requireAdmin, (req, res) => {
deltaE: d.deltaE,
}));
+ // R11 — annotate the design's stored dominant_hex with its nearest SW so
+ // consumers don't have to re-call nearestSW() for the canonical color.
+ const dominant_sw = design.dominant_hex ? colorIsolate.nearestSW(design.dominant_hex) : null;
+
res.json({
design: {
id: design.id,
@@ -6333,6 +6387,7 @@ app.get('/api/isolate/:designId', requireAdmin, (req, res) => {
category: design.category,
dominant_hex: design.dominant_hex,
},
+ dominant_sw,
palette,
coordinating_hexes,
coordinating_designs,
diff --git a/tests/integration/isolate-visual.spec.js b/tests/integration/isolate-visual.spec.js
new file mode 100644
index 0000000..df3d580
--- /dev/null
+++ b/tests/integration/isolate-visual.spec.js
@@ -0,0 +1,65 @@
+// Visual-regression baselines for the isolate modal across its 3 states.
+// Catches drift when CSS/template changes silently break the layout (the kind of
+// bug R1's manual screenshot tick already surfaced — modal bottom cut at 900vh,
+// review-card image-wrap aspect ratio off). Drift becomes a test fail next time
+// instead of a visual ask from Steve.
+//
+// First-time run creates the baseline PNGs. Subsequent runs diff against them.
+// Re-baseline intentionally: `npx playwright test --update-snapshots`.
+
+const { test, expect } = require('@playwright/test');
+
+const BASE = 'http://127.0.0.1:9792';
+
+test.describe('isolate modal visual regression', () => {
+ test.beforeEach(async ({ page }) => {
+ // Dismiss the age-prompt overlay (intercepts clicks in /designs review mode)
+ await page.addInitScript(() => { try { localStorage.setItem('wallco-age-skipped', '1'); } catch {} });
+ });
+
+ test('VR-1 · /designs review grid — review cards have stable layout', async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto(`${BASE}/designs?review=1`, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('.design-card.review-card', { timeout: 15_000 });
+ // Wait for chips to lazy-load so baseline isn't flaky
+ await page.waitForTimeout(2000);
+ // Mask the first 3 cards only (rest scroll out of frame) — clip to header + first row
+ await expect(page).toHaveScreenshot('1-designs-review-grid-desktop.png', {
+ clip: { x: 0, y: 0, width: 1440, height: 900 },
+ maxDiffPixelRatio: 0.02,
+ fullPage: false,
+ });
+ });
+
+ test('VR-2 · isolate modal — palette visible, pre-Isolate-click', async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto(`${BASE}/designs?review=1`, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('.design-card.review-card', { timeout: 15_000 });
+ const firstCard = page.locator('.design-card.review-card').first();
+ await firstCard.hover();
+ await page.keyboard.press('i');
+ // Wait for the isolate panel to render
+ await page.waitForTimeout(3000);
+ // The isolate modal may be at the top of the page or a side panel — capture the viewport
+ await expect(page).toHaveScreenshot('2-isolate-open-palette-desktop.png', {
+ clip: { x: 0, y: 0, width: 1440, height: 900 },
+ maxDiffPixelRatio: 0.02,
+ fullPage: false,
+ });
+ });
+
+ test('VR-3 · isolate mobile palette (390×844)', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto(`${BASE}/designs?review=1`, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('.design-card.review-card', { timeout: 15_000 });
+ const firstCard = page.locator('.design-card.review-card').first();
+ // Mobile: click instead of hover+keyboard (no I shortcut on touch)
+ await firstCard.click({ position: { x: 40, y: 40 } }).catch(() => {});
+ await page.waitForTimeout(3000);
+ await expect(page).toHaveScreenshot('3-isolate-mobile.png', {
+ clip: { x: 0, y: 0, width: 390, height: 844 },
+ maxDiffPixelRatio: 0.03,
+ fullPage: false,
+ });
+ });
+});
diff --git a/tests/integration/isolate-visual.spec.js-snapshots/1-designs-review-grid-desktop-local-chromium-darwin.png b/tests/integration/isolate-visual.spec.js-snapshots/1-designs-review-grid-desktop-local-chromium-darwin.png
new file mode 100644
index 0000000..9190623
Binary files /dev/null and b/tests/integration/isolate-visual.spec.js-snapshots/1-designs-review-grid-desktop-local-chromium-darwin.png differ
diff --git a/tests/integration/isolate-visual.spec.js-snapshots/2-isolate-open-palette-desktop-local-chromium-darwin.png b/tests/integration/isolate-visual.spec.js-snapshots/2-isolate-open-palette-desktop-local-chromium-darwin.png
new file mode 100644
index 0000000..f51808e
Binary files /dev/null and b/tests/integration/isolate-visual.spec.js-snapshots/2-isolate-open-palette-desktop-local-chromium-darwin.png differ
diff --git a/tests/integration/isolate-visual.spec.js-snapshots/3-isolate-mobile-local-chromium-darwin.png b/tests/integration/isolate-visual.spec.js-snapshots/3-isolate-mobile-local-chromium-darwin.png
new file mode 100644
index 0000000..fe2c0ea
Binary files /dev/null and b/tests/integration/isolate-visual.spec.js-snapshots/3-isolate-mobile-local-chromium-darwin.png differ
← 7f87ada yolo backlog: R0a-R0d ledger update — Steve /studio post-gen
·
back to Wallco Ai
·
YOLO R9 — scrub AI-generated language from 9 public surfaces 8ad112a →