← back to Wallco Ai
studio + tests: live tile preview, review.spec isolation, deterministic waits
6eb8d92ce2123f9d6dd651ad50668f12fbc08a74 · 2026-05-12 14:20:41 -0700 · SteveStudio2
server.js
- Add #studio-tile-preview between the slider and the rooms grid. Pure CSS
background-repeat at scale = (slider_inches / 120) * 100 % of a 140px-tall
strip. The input event repaints on every drag tick so the slider gives
60fps feedback while the slow /api/room renders still fire on change.
- Avoid backticks inside the script payload; the whole /studio response is
itself a template literal in res.send(`...`), so any literal backtick
(even inside JS comments) closes the outer literal and throws "missing )
after argument list" at ${FOOTER}.
tests/review.spec.js
- beforeAll snapshots data/{reviews,moodboards,pairings}.json; beforeEach
resets each to {}; afterAll restores. Steve's accumulated decisions are
preserved across suite runs and Keep/Reject/pin toggles always start in
the off state, which fixes tests #2 #3 #5 that previously failed when
prior runs had stuck card decisions into the file.
tests/integration/studio-repeat-size.spec.js
- Wait for #studio-repeat-size to be attached before checking room-cell
state. The previous wait was vacuously true on an empty selector match
and let stale runs slip through, then NPE on the slider lookup. Same
fix applied to M-4 and M-5.
Files touched
M server.jsM tests/integration/studio-repeat-size.spec.jsM tests/review.spec.js
Diff
commit 6eb8d92ce2123f9d6dd651ad50668f12fbc08a74
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 14:20:41 2026 -0700
studio + tests: live tile preview, review.spec isolation, deterministic waits
server.js
- Add #studio-tile-preview between the slider and the rooms grid. Pure CSS
background-repeat at scale = (slider_inches / 120) * 100 % of a 140px-tall
strip. The input event repaints on every drag tick so the slider gives
60fps feedback while the slow /api/room renders still fire on change.
- Avoid backticks inside the script payload; the whole /studio response is
itself a template literal in res.send(`...`), so any literal backtick
(even inside JS comments) closes the outer literal and throws "missing )
after argument list" at ${FOOTER}.
tests/review.spec.js
- beforeAll snapshots data/{reviews,moodboards,pairings}.json; beforeEach
resets each to {}; afterAll restores. Steve's accumulated decisions are
preserved across suite runs and Keep/Reject/pin toggles always start in
the off state, which fixes tests #2 #3 #5 that previously failed when
prior runs had stuck card decisions into the file.
tests/integration/studio-repeat-size.spec.js
- Wait for #studio-repeat-size to be attached before checking room-cell
state. The previous wait was vacuously true on an empty selector match
and let stale runs slip through, then NPE on the slider lookup. Same
fix applied to M-4 and M-5.
---
server.js | 25 +++++++++++++++--
tests/integration/studio-repeat-size.spec.js | 27 ++++++++++--------
tests/review.spec.js | 42 ++++++++++++++++++++++++++--
3 files changed, 79 insertions(+), 15 deletions(-)
diff --git a/server.js b/server.js
index 79e65a9..b8c15b2 100644
--- a/server.js
+++ b/server.js
@@ -8547,7 +8547,7 @@ document.getElementById('btn-compose').addEventListener('click', async function(
// Repeat-size control — pattern stays seamless; user picks how big a single repeat
// shows on the wall. Slider min = lots of small repeats; max = one giant pattern.
// Default sits at the larger end so the first render shows the design near full-size.
- '<div style="padding:10px 14px;background:#fff;border-top:1px solid #eee;border-bottom:1px solid #eee">'+
+ '<div style="padding:10px 14px;background:#fff;border-top:1px solid #eee">'+
'<label style="font-size:11px;color:#444;display:flex;align-items:center;gap:10px;cursor:pointer">'+
'<span style="white-space:nowrap;letter-spacing:.04em">Repeat size</span>'+
'<input type="range" id="studio-repeat-size" min="12" max="108" value="72" step="6" style="flex:1;accent-color:#0f0e0c">'+
@@ -8555,6 +8555,16 @@ document.getElementById('btn-compose').addEventListener('click', async function(
'</label>'+
'<div style="font-size:10px;color:#999;margin-top:3px">← more repeats (small tile) · one giant pattern →. Pattern stays seamless at every size.</div>'+
'</div>'+
+ // Instant client-side tile preview — pure CSS background-repeat so the slider
+ // gives 60fps feedback while the slow /api/room renders fire on the change event.
+ // Wall reference: ~120" wide (10 ft). The preview strip is 360px tall and
+ // proportional. Background-size in % maps slider inches → strip width:
+ // tile_pct = (slider_inches / 120) * 100 → e.g. 72" → 60% wide tile.
+ '<div style="padding:0 14px 12px;background:#fff;border-bottom:1px solid #eee">'+
+ '<div style="font-size:10px;color:#999;letter-spacing:.08em;text-transform:uppercase;margin:8px 0 6px">Live tile preview · 10 ft wide wall</div>'+
+ '<div id="studio-tile-preview" data-pattern-url="'+imgUrl+'" '+
+ 'style="height:140px;border:1px solid #eee;border-radius:4px;background-image:url(\\''+imgUrl+'\\');background-repeat:repeat;background-position:center;background-size:60% auto"></div>'+
+ '</div>'+
// 3 room settings row — one per category, in parallel.
'<div style="padding:14px 14px 12px;background:#fafafa">'+
'<div style="font-size:10px;color:#999;letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px">See it in a room · 3 categories</div>'+
@@ -8628,18 +8638,29 @@ document.getElementById('btn-compose').addEventListener('click', async function(
});
}
renderStudioRooms();
- // Slider wiring — debounce so dragging doesn't fire 60 renders/sec.
+ // Slider wiring — the input event fires continuously during drag (live tile
+ // preview); the change event fires on release (slow /api/room re-render,
+ // debounced 250ms).
var sliderEl = document.getElementById('studio-repeat-size');
var sliderVal = document.getElementById('studio-repeat-val');
+ var tilePrev = document.getElementById('studio-tile-preview');
var sliderDebounce = null;
+ function paintTilePreview(){
+ if (!tilePrev || !sliderEl) return;
+ // 120" reference wall; clamp 5%-100% so extremes stay readable.
+ var pct = Math.max(5, Math.min(100, (parseInt(sliderEl.value, 10) / 120) * 100));
+ tilePrev.style.backgroundSize = pct.toFixed(1) + '% auto';
+ }
if (sliderEl && sliderVal) {
sliderEl.addEventListener('input', function(){
sliderVal.textContent = sliderEl.value + '" wide';
+ paintTilePreview();
});
sliderEl.addEventListener('change', function(){
if (sliderDebounce) clearTimeout(sliderDebounce);
sliderDebounce = setTimeout(renderStudioRooms, 250);
});
+ paintTilePreview();
}
// Delete button — wired by R0d.
var delBtn = document.getElementById('studio-delete-btn');
diff --git a/tests/integration/studio-repeat-size.spec.js b/tests/integration/studio-repeat-size.spec.js
index 41aff9b..c56d57b 100644
--- a/tests/integration/studio-repeat-size.spec.js
+++ b/tests/integration/studio-repeat-size.spec.js
@@ -171,12 +171,16 @@ test('M-4 · cells flip back to loading state on slider change', async ({ page }
await page.goto('/studio', { waitUntil: 'commit' });
await page.click('#btn-compose');
- // Wait for first batch to finish (background images set on all 3).
- await page.waitForFunction(() =>
- Array.from(document.querySelectorAll('#studio-rooms-grid .studio-room-cell'))
- .every(c => /url\(/.test(c.style.backgroundImage)),
- null, { timeout: 8000 }
- );
+ // Two-step wait — slider must be in DOM (proves the payoff frame rendered),
+ // then all 3 cells must have a bg image (proves the first room batch landed).
+ // The bg-image check alone is vacuously true on an empty selector match,
+ // which let stale runs slip through and then NPE the slider lookup below.
+ await page.locator('#studio-repeat-size').waitFor({ state: 'attached', timeout: 8000 });
+ await page.waitForFunction(() => {
+ const cells = document.querySelectorAll('#studio-rooms-grid .studio-room-cell');
+ return cells.length === 3 &&
+ Array.from(cells).every(c => /url\(/.test(c.style.backgroundImage));
+ }, null, { timeout: 8000 });
// Bump the slider and immediately inspect — cells should re-enter loading state.
await page.evaluate(() => {
@@ -208,11 +212,12 @@ test('M-5 · rapid slider drags collapse to the last value (no race)', async ({
await page.goto('/studio', { waitUntil: 'commit' });
await page.click('#btn-compose');
- await page.waitForFunction(() =>
- Array.from(document.querySelectorAll('#studio-rooms-grid .studio-room-cell'))
- .every(c => /url\(/.test(c.style.backgroundImage)),
- null, { timeout: 8000 }
- );
+ await page.locator('#studio-repeat-size').waitFor({ state: 'attached', timeout: 8000 });
+ await page.waitForFunction(() => {
+ const cells = document.querySelectorAll('#studio-rooms-grid .studio-room-cell');
+ return cells.length === 3 &&
+ Array.from(cells).every(c => /url\(/.test(c.style.backgroundImage));
+ }, null, { timeout: 8000 });
const initial = calls.length; // 3
// Fire 4 change events in quick succession — only the last value should land.
diff --git a/tests/review.spec.js b/tests/review.spec.js
index e555ece..e3e6f3c 100644
--- a/tests/review.spec.js
+++ b/tests/review.spec.js
@@ -5,7 +5,27 @@ const { test, expect } = require('@playwright/test');
const fs = require('node:fs');
const path = require('node:path');
-const REVIEW_FILE = path.join(__dirname, '..', 'data', 'reviews.json');
+const DATA_DIR = path.join(__dirname, '..', 'data');
+const REVIEW_FILE = path.join(DATA_DIR, 'reviews.json');
+const MOOD_FILE = path.join(DATA_DIR, 'moodboards.json');
+const PAIR_FILE = path.join(DATA_DIR, 'pairings.json');
+
+// State-file snapshot/restore. The Keep/Reject buttons TOGGLE — clicking a
+// card whose `data-decision` is already 'keep' clears it instead of setting
+// it, which removes the `.visible` class from the why-chip and makes
+// tests #2/#3 fail. Same shape on #5 — .pair-pin is a toggle that
+// un-pins if a prior run already pinned. The fix is to snapshot these
+// three files at suite start, write a clean `{}` before each test, then
+// restore on suite teardown so Steve's actual review state is preserved.
+const SNAPSHOTS = {};
+function snapshotFile(p) {
+ try { SNAPSHOTS[p] = fs.readFileSync(p, 'utf8'); }
+ catch { SNAPSHOTS[p] = null; }
+}
+function restoreFile(p) {
+ if (SNAPSHOTS[p] == null) { try { fs.unlinkSync(p); } catch {} }
+ else { fs.writeFileSync(p, SNAPSHOTS[p]); }
+}
// Helpers ────────────────────────────────────────────────────────────
function readReviews() {
@@ -22,8 +42,26 @@ async function getCardById(page, id) {
test.describe('wallco.ai /designs review flow', () => {
- // Dismiss the wallco.ai age-prompt overlay before any page nav — it intercepts clicks.
+ // Snapshot real review state once; restore once at the end so Steve's
+ // accumulated decisions/moodboards/pairings aren't lost to test runs.
+ test.beforeAll(() => {
+ snapshotFile(REVIEW_FILE);
+ snapshotFile(MOOD_FILE);
+ snapshotFile(PAIR_FILE);
+ });
+ test.afterAll(() => {
+ restoreFile(REVIEW_FILE);
+ restoreFile(MOOD_FILE);
+ restoreFile(PAIR_FILE);
+ });
+
+ // Each test starts with the three files set to `{}` so Keep/Reject and
+ // pin toggles always START in the off state. Also dismiss the
+ // wallco.ai age-prompt overlay — it intercepts clicks.
test.beforeEach(async ({ page }) => {
+ fs.writeFileSync(REVIEW_FILE, '{}');
+ fs.writeFileSync(MOOD_FILE, '{}');
+ fs.writeFileSync(PAIR_FILE, '{}');
await page.addInitScript(() => {
try { localStorage.setItem('wallco-age-skipped', '1'); } catch {}
});
← 8470994 fix(chat-modal): hide #wallco-chat-fab when chat/pair/iso mo
·
back to Wallco Ai
·
studio: persist repeat-inches across composes + browser sess eb7e582 →