[object Object]

← back to Wallco Ai

studio: persist repeat-inches across composes + browser sessions

eb7e5823e496ec4b6371a6654624c34f29e88a98 · 2026-05-12 14:23:14 -0700 · SteveStudio2

Slider hydrates from localStorage[wallco.studio.repeat-inches] on every
compose and writes the new value on each change. Hydration runs BEFORE
the initial renderStudioRooms() call so the first /api/room batch uses
the saved size, not the markup default of 72".

Two new Playwright cases (M-6, M-7) pin both halves of the contract —
pre-seed localStorage and verify the slider + /api/room reflect it; bump
the slider and verify localStorage is updated. 7/7 pass in ~6.7s.

Files touched

Diff

commit eb7e5823e496ec4b6371a6654624c34f29e88a98
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 14:23:14 2026 -0700

    studio: persist repeat-inches across composes + browser sessions
    
    Slider hydrates from localStorage[wallco.studio.repeat-inches] on every
    compose and writes the new value on each change. Hydration runs BEFORE
    the initial renderStudioRooms() call so the first /api/room batch uses
    the saved size, not the markup default of 72".
    
    Two new Playwright cases (M-6, M-7) pin both halves of the contract —
    pre-seed localStorage and verify the slider + /api/room reflect it; bump
    the slider and verify localStorage is updated. 7/7 pass in ~6.7s.
---
 server.js                                    | 18 ++++++++-
 tests/integration/studio-repeat-size.spec.js | 60 ++++++++++++++++++++++++++++
 2 files changed, 76 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index b8c15b2..5a9c1fa 100644
--- a/server.js
+++ b/server.js
@@ -8637,14 +8637,18 @@ document.getElementById('btn-compose').addEventListener('click', async function(
         });
       });
     }
-    renderStudioRooms();
     // 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).
+    // debounced 250ms). Value persists to localStorage so the user's preferred
+    // repeat size carries across composes and across browser sessions.
+    //
+    // Order matters: hydrate from localStorage BEFORE the initial renderStudioRooms
+    // call so the first /api/room batch uses the saved size, not the markup default.
     var sliderEl = document.getElementById('studio-repeat-size');
     var sliderVal = document.getElementById('studio-repeat-val');
     var tilePrev = document.getElementById('studio-tile-preview');
     var sliderDebounce = null;
+    var REPEAT_KEY = 'wallco.studio.repeat-inches';
     function paintTilePreview(){
       if (!tilePrev || !sliderEl) return;
       // 120" reference wall; clamp 5%-100% so extremes stay readable.
@@ -8652,16 +8656,26 @@ document.getElementById('btn-compose').addEventListener('click', async function(
       tilePrev.style.backgroundSize = pct.toFixed(1) + '% auto';
     }
     if (sliderEl && sliderVal) {
+      // Hydrate from localStorage if the user has a preferred repeat size.
+      try {
+        var saved = parseInt(localStorage.getItem(REPEAT_KEY) || '', 10);
+        if (saved >= parseInt(sliderEl.min,10) && saved <= parseInt(sliderEl.max,10)) {
+          sliderEl.value = String(saved);
+          sliderVal.textContent = saved + '" wide';
+        }
+      } catch(_){}
       sliderEl.addEventListener('input', function(){
         sliderVal.textContent = sliderEl.value + '" wide';
         paintTilePreview();
       });
       sliderEl.addEventListener('change', function(){
+        try { localStorage.setItem(REPEAT_KEY, sliderEl.value); } catch(_){}
         if (sliderDebounce) clearTimeout(sliderDebounce);
         sliderDebounce = setTimeout(renderStudioRooms, 250);
       });
       paintTilePreview();
     }
+    renderStudioRooms();
     // Delete button — wired by R0d.
     var delBtn = document.getElementById('studio-delete-btn');
     if (delBtn) delBtn.addEventListener('click', function(){
diff --git a/tests/integration/studio-repeat-size.spec.js b/tests/integration/studio-repeat-size.spec.js
index c56d57b..9f2397e 100644
--- a/tests/integration/studio-repeat-size.spec.js
+++ b/tests/integration/studio-repeat-size.spec.js
@@ -63,6 +63,8 @@ test.beforeEach(async ({ page }) => {
   await page.addInitScript(() => {
     try { localStorage.setItem('wallco-age-skipped', '1'); } catch {}
     try { localStorage.setItem('wc-studio-sid', 'test-sid-repeat'); } catch {}
+    // Each test starts with a clean repeat-inches localStorage; M-6 sets its own.
+    try { localStorage.removeItem('wallco.studio.repeat-inches'); } catch {}
   });
   await installStubs(page);
 });
@@ -250,3 +252,61 @@ test('M-5 · rapid slider drags collapse to the last value (no race)', async ({
     null, { timeout: 4000 }
   );
 });
+
+test('M-6 · saved repeat-inches hydrates the slider AND drives the first /api/room batch', async ({ page }) => {
+  // Pre-seed localStorage with a non-default repeat — the slider should adopt
+  // it on compose and the initial 3 room calls should fire at that size,
+  // not at the markup default (72").
+  await page.addInitScript(() => {
+    try { localStorage.setItem('wallco.studio.repeat-inches', '36'); } catch {}
+  });
+  const calls = [];
+  await page.route('**/api/room', async (route) => {
+    const post = route.request().postDataJSON();
+    calls.push(post);
+    await route.fulfill({ status:200, contentType:'application/json',
+      body: JSON.stringify({ ok:true, room_id: calls.length, image_url:`/uploads/rooms/fake_${calls.length}.png`, roomType: post.roomType, design_id: FAKE_DESIGN_ID })
+    });
+  });
+
+  await page.goto('/studio', { waitUntil: 'commit' });
+  await page.click('#btn-compose');
+
+  const slider = page.locator('#studio-repeat-size');
+  await expect(slider).toHaveValue('36');
+  await expect(page.locator('#studio-repeat-val')).toHaveText('36" wide');
+
+  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 });
+
+  expect(calls).toHaveLength(3);
+  for (const c of calls) {
+    expect(c.patternWidth).toBe(36);
+    expect(c.patternHeight).toBe(36);
+  }
+});
+
+test('M-7 · slider change writes the new value to localStorage', async ({ page }) => {
+  await page.route('**/api/room', (route) =>
+    route.fulfill({ status:200, contentType:'application/json',
+      body: JSON.stringify({ ok:true, room_id: 1, image_url:'/uploads/rooms/fake.png', roomType:'bedroom', design_id: FAKE_DESIGN_ID })
+    })
+  );
+
+  await page.goto('/studio', { waitUntil: 'commit' });
+  await page.click('#btn-compose');
+  await page.locator('#studio-repeat-size').waitFor({ state: 'attached', timeout: 8000 });
+
+  await page.evaluate(() => {
+    const s = document.getElementById('studio-repeat-size');
+    s.value = '90';
+    s.dispatchEvent(new Event('input',  { bubbles: true }));
+    s.dispatchEvent(new Event('change', { bubbles: true }));
+  });
+
+  const stored = await page.evaluate(() => localStorage.getItem('wallco.studio.repeat-inches'));
+  expect(stored).toBe('90');
+});

← 6eb8d92 studio + tests: live tile preview, review.spec isolation, de  ·  back to Wallco Ai  ·  tests: re-baseline isolate-visual snapshots after full-suite 1e5cbb8 →