[object Object]

← 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

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) &nbsp;·&nbsp; 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 →