[object Object]

← back to CelebritySignatures

wear: fix "See on garment" not hiding the editorial worn shot + modernize gallery E2E

d72fd54089327bd97995b8a6c87f8c69df2fcd2f · 2026-09-13 11:51:43 -0700 · Steve

- wear.html: #lifestyleShot had display:block with no [hidden] guard, so
  setLifeView("garment") (.hidden=true) never actually hid the seated
  Signature Edit portrait — for all 50+ mapped signers, "See on garment"
  left the worn shot covering the garment mockup/3D view. Add
  #lifestyleShot[hidden]{display:none}.
- gallery-shopping.e2e.cjs: port the retired murals journey to the shipped
  /wallpaper studio (commit 3d9677d), expect all 9 garments, correct the
  sale state (dad/trucker/bucket/socks are for sale; the two AOP shirts are
  the coming-soon holds), cover the worn-shot -> garment toggle, screenshot
  the always-visible .preview instead of the now-hidden #stage.
- 5x/ink-gate.mjs: supplementary CDP E2E for the modal ink control (PASS).

Both viewports PASS, zero page errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAzDMrVAjLD4umvnMMStjD

Files touched

Diff

commit d72fd54089327bd97995b8a6c87f8c69df2fcd2f
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Sep 13 11:51:43 2026 -0700

    wear: fix "See on garment" not hiding the editorial worn shot + modernize gallery E2E
    
    - wear.html: #lifestyleShot had display:block with no [hidden] guard, so
      setLifeView("garment") (.hidden=true) never actually hid the seated
      Signature Edit portrait — for all 50+ mapped signers, "See on garment"
      left the worn shot covering the garment mockup/3D view. Add
      #lifestyleShot[hidden]{display:none}.
    - gallery-shopping.e2e.cjs: port the retired murals journey to the shipped
      /wallpaper studio (commit 3d9677d), expect all 9 garments, correct the
      sale state (dad/trucker/bucket/socks are for sale; the two AOP shirts are
      the coming-soon holds), cover the worn-shot -> garment toggle, screenshot
      the always-visible .preview instead of the now-hidden #stage.
    - 5x/ink-gate.mjs: supplementary CDP E2E for the modal ink control (PASS).
    
    Both viewports PASS, zero page errors.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01VAzDMrVAjLD4umvnMMStjD
---
 5x/ink-gate.mjs                       | 55 +++++++++++++++++++++++++++
 public/wear.html                      |  1 +
 verification/gallery-shopping.e2e.cjs | 70 ++++++++++++++++++++++++++++-------
 3 files changed, 112 insertions(+), 14 deletions(-)

diff --git a/5x/ink-gate.mjs b/5x/ink-gate.mjs
new file mode 100644
index 0000000..af159cf
--- /dev/null
+++ b/5x/ink-gate.mjs
@@ -0,0 +1,55 @@
+// Supplementary E2E for the modal-gated SIGNATURE INK control, via CDP.
+// Opens the /wear product modal, asserts the ink chips populate and are visible,
+// clicks a preset and Auto, and collects any console/page errors. Exit 0 = pass.
+import { spawn } from 'child_process';
+const PORT = process.argv[2]; const DEBUG = 9337;
+const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+const proc = spawn(CHROME, ['--headless=new','--disable-gpu',`--remote-debugging-port=${DEBUG}`,`--user-data-dir=/tmp/inkgate-${process.pid}`], {stdio:'ignore'});
+await new Promise(r=>setTimeout(r,2000));
+const errors = [];
+try {
+  const list = await (await fetch(`http://localhost:${DEBUG}/json/list`)).json();
+  const tgt = list.find(t=>t.type==='page');
+  const ws = new WebSocket(tgt.webSocketDebuggerUrl);
+  let id=0; const pending=new Map();
+  const send=(m,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:m,params:p}));});
+  await new Promise(r=>ws.onopen=r);
+  ws.onmessage=e=>{const m=JSON.parse(e.data);
+    if(m.method==='Runtime.consoleAPICalled'&&m.params.type==='error')errors.push('console:'+m.params.args.map(a=>a.value||a.description||'').join(' '));
+    if(m.method==='Runtime.exceptionThrown')errors.push('pageerror:'+(m.params.exceptionDetails?.exception?.description||m.params.exceptionDetails?.text||''));
+    if(m.id&&pending.has(m.id))pending.get(m.id)(m.result),pending.delete(m.id);};
+  await send('Page.enable'); await send('Runtime.enable');
+  await send('Page.navigate',{url:`http://localhost:${PORT}/wear`});
+  await new Promise(r=>setTimeout(r,4500));
+  const script=`(()=>{const first=document.querySelector('.card');if(first)first.click();
+    return new Promise(res=>setTimeout(()=>{
+      const ink=document.getElementById('inkChips');
+      const swatches=[...ink.querySelectorAll('[data-ink]')];
+      const burg=ink.querySelector('[data-ink="#6e1f2e"]'); if(burg)burg.click();
+      setTimeout(()=>{
+        const auto=ink.querySelector('[data-ink="auto"]');
+        const afterBurg=window.__wearInk; // not exposed; read selected class instead
+        const burgSel=ink.querySelector('[data-ink="#6e1f2e"]')?.classList.contains('sel');
+        if(auto)auto.click();
+        setTimeout(()=>{
+          const autoSel=ink.querySelector('[data-ink="auto"]')?.classList.contains('sel');
+          res(JSON.stringify({modalOpen:document.getElementById('scrim').classList.contains('on'),chips:swatches.length,inkVisible:ink.offsetParent!==null,burgSelectable:!!burg,burgSel,autoSel}));
+        },400);
+      },500);
+    },1200));})()`;
+  const r=await send('Runtime.evaluate',{expression:script,awaitPromise:true,returnByValue:true});
+  const st=JSON.parse(r.result.value);
+  ws.close();
+  const fails=[];
+  if(!st.modalOpen)fails.push('modal did not open on card click');
+  if(st.chips<9)fails.push(`only ${st.chips} ink chips (expected >=9)`);
+  if(!st.inkVisible)fails.push('ink chips not visible');
+  if(!st.burgSelectable)fails.push('burgundy preset missing');
+  if(!st.burgSel)fails.push('burgundy preset did not select');
+  if(!st.autoSel)fails.push('Auto did not re-select');
+  if(errors.length)fails.push('JS errors: '+errors.join(' | '));
+  console.log('INK-GATE state:',JSON.stringify(st));
+  if(fails.length){console.log('INK-GATE FAIL:',fails.join('; '));process.exitCode=1;}
+  else console.log('INK-GATE PASS');
+} catch(e){ console.log('INK-GATE ERROR:',e.message); process.exitCode=2; }
+finally { proc.kill(); }
diff --git a/public/wear.html b/public/wear.html
index d60be12..6569a83 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -99,6 +99,7 @@
   .view3d-btn:hover { border-color:var(--ink); }
   /* Editorial "worn" shot — the celebrity seated in the full Signature Edit. */
   #lifestyleShot { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; }
+  #lifestyleShot[hidden] { display:none; }  /* the explicit display:block above defeats the [hidden] default — restore it so "See on garment" actually reveals the mockup */
   .lifestyle-toggle { position:absolute; top:14px; right:14px; z-index:6; font:inherit; font-size:12px; background:rgba(255,255,255,.92); border:1px solid var(--line); border-radius:999px; padding:6px 12px; cursor:pointer; }
   .lifestyle-toggle:hover { border-color:var(--ink); }
   @media (max-width:720px){ .preview{ border-radius:16px 16px 0 0; } }
diff --git a/verification/gallery-shopping.e2e.cjs b/verification/gallery-shopping.e2e.cjs
index 38e36b1..234e15b 100644
--- a/verification/gallery-shopping.e2e.cjs
+++ b/verification/gallery-shopping.e2e.cjs
@@ -23,11 +23,16 @@ const checks = [];
       const page = await context.newPage();
       const errors = [];
       page.on('pageerror', e => errors.push(e.message));
+      // All three initial catalog responses can redraw cards independently.
+      const galleryReady = Promise.all(['/api/signatures', '/api/signature-evolution', '/api/portraits'].map(endpoint =>
+        page.waitForResponse(r => new URL(r.url()).pathname === endpoint).then(r => r.finished())));
       await page.goto(base+'/?cat=Politics', { waitUntil:'domcontentloaded' });
+      await galleryReady;
+      await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
       const card = page.locator('.card[data-qid="Q91"]');
       await card.waitFor();
       assert.equal(await page.locator('.gnav a[href="/wear"]').count(), 1);
-      assert.ok(await card.locator('.shop-mural').isVisible());
+      assert.ok(await card.locator('.shop-wallpaper').isVisible());
       assert.ok(await card.locator('.shop-clothing').isVisible());
       assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
       await card.scrollIntoViewIfNeeded();
@@ -38,29 +43,50 @@ const checks = [];
       await page.locator('#scrim.on').waitFor();
       assert.equal(await page.locator('#pName').textContent(), 'Abraham Lincoln');
       assert.equal(new URL(page.url()).searchParams.get('qid'), 'Q91');
-      assert.equal(await page.locator('#garments .chip').count(), 7);
+      assert.deepEqual(await page.locator('#garments .chip').allTextContents(), ['Classic Tee', 'Polo', 'Pocket Tee', 'Dad Cap', 'Trucker Cap', 'Bucket Hat', 'Crew Socks', 'Signature Camp Shirt', 'Signature Long-Sleeve']);
       await page.locator('#garments [data-g="polo"]').click();
       await page.locator('#colors [data-c="black"]').click();
       await page.locator('#sizes [data-z="2XL"]').click();
       assert.match(await page.locator('#price').textContent(), /48\.00/);
       await page.waitForFunction(() => photoCache[state.color.photo]?.naturalWidth > 0 && sigImg?.naturalWidth > 0);
       await page.screenshot({ path:path.join(artifacts, `${name}-clothing.png`) });
+      // The four accessories are now for sale: One size, real price, purchasable, no shirt-only 3D toggle.
+      const accessoryPrice = { 'dad-cap':'$29.00', 'trucker-cap':'$33.00', 'bucket-hat':'$38.00', 'crew-socks':'$28.00' };
       for (const id of ['dad-cap', 'trucker-cap', 'bucket-hat', 'crew-socks']) {
         await page.locator(`#garments [data-g="${id}"]`).click();
         await page.waitForFunction(id => state.garment.id === id && photoCache[state.color.photo]?.naturalWidth > 0, id);
         assert.equal(await page.locator('#sizes .chip').textContent(), 'One size');
+        assert.equal(await page.locator('#buy').isDisabled(), false);
+        assert.notEqual(await page.locator('#buy').textContent(), 'Coming soon');
+        assert.equal(await page.locator('#price').textContent(), accessoryPrice[id]);
+        assert.equal(await page.locator('#view3dToggle').isVisible(), false);
+        assert.match(await page.locator('#productNote').textContent(), /front of the (cap|hat)|both socks/);
+        if (id === 'crew-socks') assert.match(await page.locator('#sizeNote').textContent(), /6–10/);
+        await page.locator('.preview').screenshot({ path:path.join(artifacts, `${name}-${id}-preview.png`) });
+        await page.locator('#buy').scrollIntoViewIfNeeded();
+        await page.screenshot({ path:path.join(artifacts, `${name}-${id}-options.png`) });
+      }
+      // The all-over shirts remain coming-soon holds: multi-size, no price, placement-preview note.
+      for (const id of ['aop-camp-shirt', 'aop-longsleeve-shirt']) {
+        await page.locator(`#garments [data-g="${id}"]`).click();
+        await page.waitForFunction(id => state.garment.id === id && photoCache[state.color.photo]?.naturalWidth > 0, id);
+        assert.ok(await page.locator('#sizes .chip').count() > 1);
         assert.equal(await page.locator('#buy').isDisabled(), true);
         assert.equal(await page.locator('#buy').textContent(), 'Coming soon');
         assert.equal(await page.locator('#price').textContent(), 'Coming soon');
         assert.equal(await page.locator('#view3dToggle').isVisible(), false);
-        assert.match(await page.locator('#productNote').textContent(), /Placement preview/);
-        if (id === 'crew-socks') assert.match(await page.locator('#sizeNote').textContent(), /6–10/);
-        await page.locator('#stage').screenshot({ path:path.join(artifacts, `${name}-${id}-preview.png`) });
+        assert.match(await page.locator('#productNote').textContent(), /coming soon/i);
+        await page.locator('.preview').screenshot({ path:path.join(artifacts, `${name}-${id}-preview.png`) });
         await page.locator('#buy').scrollIntoViewIfNeeded();
         await page.screenshot({ path:path.join(artifacts, `${name}-${id}-options.png`) });
       }
       await page.locator('#garments [data-g="classic-tee"]').click();
       assert.equal(await page.locator('#buy').isDisabled(), false);
+      // Lincoln opens in the editorial "worn" Signature Edit shot; the 3D toggle is a garment-view control.
+      assert.ok(await page.locator('#lifestyleShot').isVisible());
+      assert.equal(await page.locator('#view3dToggle').isVisible(), false);
+      await page.locator('#lifestyleToggle').click();
+      assert.equal(await page.locator('#lifestyleShot').isVisible(), false);
       assert.equal(await page.locator('#view3dToggle').isVisible(), true);
       await page.goBack({ waitUntil:'domcontentloaded' });
       assert.equal(new URL(page.url()).searchParams.get('cat'), 'Politics');
@@ -71,21 +97,37 @@ const checks = [];
       assert.ok(box && box.y >= 0 && box.y + box.height <= viewport.height, 'product choices visible without scrolling the details');
       assert.equal(await choices.locator('a').count(), 2);
       await page.screenshot({ path:path.join(artifacts, `${name}-details.png`) });
-      await choices.locator('.shop-mural').click();
-      await page.waitForURL('**/murals?**');
-      await page.waitForFunction(() => document.querySelector('#finderInput').value === 'Abraham Lincoln');
-      assert.match(await page.locator('#finderRes').textContent(), /Abraham Lincoln/);
-      assert.match(await page.locator('#selMural option:checked').textContent(), /Politic/i);
-      await page.locator('#finder').scrollIntoViewIfNeeded();
-      await page.screenshot({ path:path.join(artifacts, `${name}-mural.png`) });
-      assert.ok(await page.locator('#payBtn').isVisible());
+      await choices.locator('.shop-wallpaper').click();
+      await page.waitForURL('**/wallpaper?**');
+      assert.equal(new URL(page.url()).searchParams.get('qid'), 'Q91');
+      await page.waitForFunction(() => document.querySelector('#selectedName').textContent === 'Abraham Lincoln');
+      assert.equal(await page.locator('#previewName').textContent(), 'Abraham Lincoln');
+      await page.waitForFunction(() => document.querySelector('#wallpaperPreview').dataset.signature === 'Q91');
+      await page.locator('#previewFrame').scrollIntoViewIfNeeded();
+      await page.screenshot({ path:path.join(artifacts, `${name}-wallpaper.png`) });
       await page.goto(base+'/wear?qid=QUnknownFixture&name=Not+available', { waitUntil:'domcontentloaded' });
       await page.locator('#selectionNotice:not([hidden])').waitFor();
       assert.match(await page.locator('#selectionNotice').textContent(), /not available on clothing/);
       assert.equal(await page.locator('#scrim.on').count(), 0);
       assert.ok(await page.locator('#grid .card').count() > 0);
+      await page.goto(base+'/wear', { waitUntil:'domcontentloaded' });
+      await page.locator('#grid .card').first().waitFor();
+      const total = await page.locator('#grid .card').count();
+      await page.locator('#sigSearch').fill('Lincoln');
+      await page.locator('#ac .ac-item[data-qid="Q91"]').waitFor();
+      assert.ok(await page.locator('#grid .card').count() < total);
+      await page.locator('#sigSearch').press('Enter');
+      await page.locator('#scrim.on').waitFor();
+      assert.equal(await page.locator('#pName').textContent(), 'Abraham Lincoln');
+      await page.locator('#close').click();
+      await page.locator('#sigClear').click();
+      assert.equal(await page.locator('#sigSearch').inputValue(), '');
+      assert.equal(await page.locator('#grid .card').count(), total);
+      await page.locator('#sigSearch').fill('zzzz-no-signature-fixture');
+      assert.match(await page.locator('#empty').textContent(), /No signatures match/);
+      assert.equal(await page.locator('#ac').isVisible(), false);
       assert.deepEqual(errors, []);
-      checks.push({ viewport:name, verdict:'PASS', paths:['Politics card -> selected clothing', 'four accessory photos, sizes and sale holds', 'switch back to shirt restores purchase control', 'Politics details -> matching mural finder', 'unknown clothing signature rejected'], pageErrors:errors });
+      checks.push({ viewport:name, verdict:'PASS', paths:['Politics card -> selected clothing', 'four accessories for sale, two all-over shirts on hold', 'switch back to shirt restores purchase control', 'Politics details -> signature wallpaper studio', 'unknown clothing signature rejected', 'typeahead selection, clear and no-match recovery'], pageErrors:errors });
       await context.close();
     }
   } finally { await browser.close(); }

← a310a07 wear: enable Dad Cap (29) + Trucker Cap (33) for sale, cost/  ·  back to CelebritySignatures  ·  campaign: lead the homepage hero with the full seated Signat 8272a62 →