← 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
M server.jsM tests/integration/studio-repeat-size.spec.js
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 →