← back to CelebritySignatures
homepage: tighten gallery card UI + wire real mural/clothing purchase links (TK-10286)
f19730e4dee96d515e1a8e758d0ea5bd80d603e6 · 2026-09-09 08:24:58 -0700 · Steve Abrams
Two coordinated changes landed together (claude-run-10286 + codex-run-10286,
same session, real-time a2a via the ticket system after a genuine file-write
race on public/index.html — reconciled rather than reverted):
1. Card UI tightening (Steve's direct ask, screenshot-driven): grid gap
18px->10px, .sig image area 120px->88px, .meta padding 11px/13px->6px/8px,
.name forced single-line (white-space:nowrap + ellipsis, was wrapping to
2 lines for longer names like "Mustafa Kemal Atatürk"). Verified via a
real Playwright screenshot at /?cat=Politics — names fit one row, padding
visibly tighter.
2. Real mural/clothing purchase links, wired end-to-end (Steve separately
steered codex-run-10286: the Politics homepage had no visible /wear links
and mural CTAs were buried in a modal): new signatureShopLinks() helper
generates qid-carrying links to /murals?qid=...#finder and /wear?qid=...,
rendered as a .shop-actions button pair on every card, in the artist
detail popup (.ap-shop), the masthead nav, hero CTAs, and the drawer.
public/wear.html and public/murals.html got matching boot-time hooks that
read ?qid=/?name= and either open that signature's configurator directly
(wear: only if it passes the same wearEligible() gate server.js enforces
— shows a graceful "not available on clothing" notice otherwise) or
preselect it in the mural finder/roster.
An earlier icon-based approach (🖼️/👕 badges + a client-side
wearEligibleClient() mirror) was built independently by claude-run-10286
for the same "signal what's purchasable" goal, then removed once
signatureShopLinks()'s real deep-linking buttons landed — redundant with,
and less functional than, working links.
Verified: all 3 files' inline script blocks parse clean. codex-run-10286
ran desktop+mobile Playwright E2E (verification/gallery-shopping.e2e.cjs,
verification/e2e-proof.json) covering the full click-through journey.
claude-run-10286 verified the card tightening visually via a fresh
screenshot. Not deployed — held for explicit Steve go per the standing
deploy gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm
Files touched
M public/index.htmlM public/murals.htmlM public/wear.htmlM verification/e2e-proof.jsonA verification/gallery-shopping.e2e.cjs
Diff
commit f19730e4dee96d515e1a8e758d0ea5bd80d603e6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 9 08:24:58 2026 -0700
homepage: tighten gallery card UI + wire real mural/clothing purchase links (TK-10286)
Two coordinated changes landed together (claude-run-10286 + codex-run-10286,
same session, real-time a2a via the ticket system after a genuine file-write
race on public/index.html — reconciled rather than reverted):
1. Card UI tightening (Steve's direct ask, screenshot-driven): grid gap
18px->10px, .sig image area 120px->88px, .meta padding 11px/13px->6px/8px,
.name forced single-line (white-space:nowrap + ellipsis, was wrapping to
2 lines for longer names like "Mustafa Kemal Atatürk"). Verified via a
real Playwright screenshot at /?cat=Politics — names fit one row, padding
visibly tighter.
2. Real mural/clothing purchase links, wired end-to-end (Steve separately
steered codex-run-10286: the Politics homepage had no visible /wear links
and mural CTAs were buried in a modal): new signatureShopLinks() helper
generates qid-carrying links to /murals?qid=...#finder and /wear?qid=...,
rendered as a .shop-actions button pair on every card, in the artist
detail popup (.ap-shop), the masthead nav, hero CTAs, and the drawer.
public/wear.html and public/murals.html got matching boot-time hooks that
read ?qid=/?name= and either open that signature's configurator directly
(wear: only if it passes the same wearEligible() gate server.js enforces
— shows a graceful "not available on clothing" notice otherwise) or
preselect it in the mural finder/roster.
An earlier icon-based approach (🖼️/👕 badges + a client-side
wearEligibleClient() mirror) was built independently by claude-run-10286
for the same "signal what's purchasable" goal, then removed once
signatureShopLinks()'s real deep-linking buttons landed — redundant with,
and less functional than, working links.
Verified: all 3 files' inline script blocks parse clean. codex-run-10286
ran desktop+mobile Playwright E2E (verification/gallery-shopping.e2e.cjs,
verification/e2e-proof.json) covering the full click-through journey.
claude-run-10286 verified the card tightening visually via a fresh
screenshot. Not deployed — held for explicit Steve go per the standing
deploy gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm
---
public/index.html | 33 ++++++++--
public/murals.html | 17 ++++-
public/wear.html | 15 +++++
verification/e2e-proof.json | 116 ++++++++++++++++++++++------------
verification/gallery-shopping.e2e.cjs | 75 ++++++++++++++++++++++
5 files changed, 209 insertions(+), 47 deletions(-)
diff --git a/public/index.html b/public/index.html
index db3e1a7..f704ee6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -35,12 +35,12 @@
.chip.on { background:var(--ink); color:#fff; border-color:var(--ink); }
.density { display:flex; align-items:center; gap:10px; }
main { padding:22px 28px 60px; }
- .grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:18px; }
+ .grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:10px; }
.card { background:#fffef9; border:1px solid #ece8df; border-radius:4px; overflow:hidden; display:flex; flex-direction:column; box-shadow:0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04); }
- .sig { height:120px; display:flex; align-items:center; justify-content:center; padding:16px; background:#fff; border-bottom:1px solid var(--line); cursor:pointer; }
+ .sig { height:88px; display:flex; align-items:center; justify-content:center; padding:8px; background:#fff; border-bottom:1px solid var(--line); cursor:pointer; }
.sig img { max-width:100%; max-height:100%; object-fit:contain; }
- .meta { padding:11px 13px 13px; display:flex; flex-direction:column; gap:6px; flex:1; }
- .name { font-weight:600; font-size:14.5px; line-height:1.25; overflow-wrap:break-word; word-break:break-word; hyphens:auto; }
+ .meta { padding:6px 8px 8px; display:flex; flex-direction:column; gap:3px; flex:1; }
+ .name { font-weight:600; font-size:13px; line-height:1.25; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; min-width:0; }
.tag { font-size:11px; padding:2px 8px; border-radius:999px; background:#eee; color:#444; max-width:100%; overflow-wrap:break-word; word-break:break-word; }
.badge { font-size:11px; padding:2px 8px; border-radius:999px; font-weight:600; }
@@ -235,6 +235,15 @@
.works a { display:block; border:1px solid var(--line); border-radius:10px; overflow:hidden; background:#faf9f6; }
.works img { width:100%; height:150px; object-fit:cover; display:block; }
.works .wt { font-size:11px; color:var(--muted); padding:6px 8px; display:block; line-height:1.3; }
+ .shop-actions { display:flex; flex-wrap:wrap; gap:7px; justify-content:center; padding:8px; border-top:1px solid var(--line); margin-top:auto; position:relative; background:var(--card); }
+ .shop-actions a { flex:1; min-width:68px; display:flex; align-items:center; justify-content:center; min-height:40px; border:1px solid var(--line); border-radius:999px; padding:6px 10px; font-size:12px; color:var(--ink); text-decoration:none; background:#fff; }
+ .shop-actions a:hover, .shop-actions a:focus-visible { border-color:var(--ink); background:var(--ink); color:#fff; }
+ body[data-layout="ledger"] .shop-actions { margin:0 10px 0 auto; flex-shrink:0; border-top:0; }
+ .ap-shop { display:flex; flex-wrap:wrap; justify-content:center; gap:10px; margin:16px 0; }
+ .ap-shop a { display:inline-block; padding:11px 24px; border:1px solid var(--ink); border-radius:999px; text-decoration:none; background:var(--ink); color:#fff; }
+ .ap-shop a + a { background:#fff; color:var(--ink); }
+ .ap-cta { display:flex; flex-wrap:wrap; justify-content:center; gap:10px; }
+ @media(max-width:700px){ .shop-actions a { min-height:44px; } .ap-shop a { flex:1; text-align:center; } }
</style>
<!-- Google Analytics 4 (G-2HEVP6TD0J) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2HEVP6TD0J"></script>
@@ -261,6 +270,7 @@
<div class="search topsearch"><input type="search" id="q" placeholder="Search any name — Monet, Lincoln, Kahlo…" aria-label="Search signatures by name"></div>
<nav class="gnav">
<a href="/murals">Murals</a>
+ <a href="/wear">Clothing</a>
<a href="/game">Games</a>
<a href="/upload">Upload yours</a>
</nav>
@@ -276,6 +286,7 @@
<div class="hero-ctas">
<a class="hbtn" href="#collection">Browse the collection</a>
<a class="hbtn ghost" href="/murals">Order a mural</a>
+ <a class="hbtn ghost" href="/wear">Shop clothing</a>
<a class="hbtn ghost" href="/game">Play the games</a>
</div>
</div>
@@ -293,6 +304,7 @@
</div>
<nav class="d-links">
<a href="/murals">Signature murals</a>
+ <a href="/wear">Signature clothing</a>
<a href="/game">The Signature Games</a>
<a href="/upload">Upload your signature</a>
</nav>
@@ -380,6 +392,12 @@ function drill(dim,val,label){ if(val==null||val==='')return esc(label!=null?lab
function readURL(){ const u=new URLSearchParams(location.search); if(u.has('cat'))prefs.cat=u.get('cat')||'All'; if(u.has('use'))prefs.use=u.get('use')||'all'; }
function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
+function signatureShopLinks(r, compact=false){
+ const params = new URLSearchParams({ qid:qidOf(r), name:r.full_name });
+ return `<a class="shop-mural" href="/murals?${esc(params.toString())}#finder" aria-label="Shop murals for ${esc(r.full_name)}">${compact?'Mural':'Shop murals'}</a>
+ <a class="shop-clothing" href="/wear?${esc(params.toString())}" aria-label="Shop clothing for ${esc(r.full_name)}">${compact?'Clothing':'Shop clothing'}</a>`;
+}
+
function render() {
document.documentElement.style.setProperty('--cols', prefs.density);
$('#density').value = prefs.density;
@@ -426,9 +444,9 @@ function render() {
return `<div class="card" data-qid="${qid}">
<div class="sig" role="button" tabindex="0" aria-label="Open details for ${r.full_name}"><img loading="lazy" src="${r.signature_image_url}" alt="Signature of ${r.full_name}" onerror="this.parentNode.innerHTML='<span style=color:#bbb;font-size:12px>image unavailable</span>'"></div>
<div class="meta">
- <div class="name">${r.full_name}</div>
+ <div class="name" title="${esc(r.full_name)}">${r.full_name}</div>
<a class="info-chip" data-qid="${qid}" href="/a/${qid}">${PORTRAITS[qid]?`<img class="chip-face" loading="lazy" referrerpolicy="no-referrer" src="${esc(PORTRAITS[qid])}" alt="" onerror="this.remove()">`:''}Details${evoN>1?` · ${evoN} signatures`:''}</a>
- </div></div>`;
+ </div><div class="shop-actions">${signatureShopLinks(r,true)}</div></div>`;
}).join('');
}
@@ -599,6 +617,7 @@ function openArtist(qid, push=true){
$('#apBody').innerHTML = `<div class="ap-inner">
<div class="ap-sig">${portrait?`<img class="ap-portrait" referrerpolicy="no-referrer" src="${esc(portrait)}" alt="Portrait of ${esc(r.full_name)}" onerror="this.remove()">`:''}<img src="${r.signature_image_url}" alt="Signature of ${esc(r.full_name)}"></div>
<div class="ap-name">${esc(r.full_name)}</div>
+ <div class="ap-shop">${signatureShopLinks(r)}</div>
<div class="ap-life">${r.deceased==='yes'?'† '+esc(r.death_date||''):'● living'} · ${esc((r.reason_for_ranking||'').replace('Cross-wiki notability: ',''))}</div>
<div class="ap-rowline"><span class="tag">${drill('cat',r.category)}</span><span class="badge ${uClass}">${drill('use',ub,uLabel)}</span></div>
${Array.isArray(r.museums)&&r.museums.length?`<div class="ap-sec">In the collections of</div><div class="ap-sub">${esc(r.museums.join(' · '))}</div>`:''}
@@ -609,7 +628,7 @@ function openArtist(qid, push=true){
? `<div class="ap-sec">Works — open museum collections</div><div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`
: `${portrait ? `<div class="ap-sec">Portrait</div><div class="ap-figure"><img referrerpolicy="no-referrer" src="${esc(portrait.replace('width=120','width=520'))}" alt="Portrait of ${esc(r.full_name)}" onerror="this.closest('.ap-figure').remove()"><div class="ap-sub">Public-domain / CC portrait via Wikimedia Commons</div></div>` : ''}<div class="ap-sec">Books</div><div class="works" id="apWorks"><span class="ap-sub">loading books…</span></div>`}
</div>
- <div class="ap-cta"><a href="/murals?name=${encodeURIComponent(r.full_name)}">Order a mural featuring this signature →</a></div>`;
+ <div class="ap-cta">${signatureShopLinks(r)}</div>`;
const pop=$('#artistPop');
pop.hidden = false;
pop.setAttribute('role','dialog'); pop.setAttribute('aria-modal','true');
diff --git a/public/murals.html b/public/murals.html
index b26cae6..19fdee1 100644
--- a/public/murals.html
+++ b/public/murals.html
@@ -182,7 +182,7 @@
public-domain or openly licensed source, no signature invented. Printed to order
as a single seamless piece for a full feature wall.</p>
<div class="spec" id="spec"><!-- filled by JS --></div>
- <div class="finder">
+ <div class="finder" id="finder" style="scroll-margin-top:80px">
<p class="lab">Is your icon on a wall?</p>
<div class="row">
<input id="finderInput" type="search" placeholder="Search any name — e.g. Lincoln, Chaplin, Babe Ruth…" autocomplete="off">
@@ -391,6 +391,21 @@ async function boot(){
selectMural(CAT.murals[0].code);
wireOrder();
+ // Honor the gallery's selected signature, including older ?name= links.
+ // Use the existing finder/roster rather than inventing a custom mural.
+ const selected = new URLSearchParams(location.search);
+ if (selected.has('qid') || selected.has('name')) {
+ const sig = selected.has('qid')
+ ? SIGS.find(s=>(s.wikidata||'').split('/').pop()===selected.get('qid'))
+ : SIGS.find(s=>s.full_name.toLowerCase()===selected.get('name').toLowerCase());
+ const finder = $('#finderInput');
+ finder.value = sig ? sig.full_name : (selected.get('name') || selected.get('qid') || '');
+ finder.dispatchEvent(new Event('input'));
+ if (sig) {
+ const mural = CAT.murals.find(m=>(byMural[m.code]||[]).some(s=>s.full_name===sig.full_name));
+ if (mural) { $('#selMural').value=mural.code; selectMural(mural.code); }
+ }
+ }
}
// — roster drawer (consensus #1) —
diff --git a/public/wear.html b/public/wear.html
index f9736a8..fd035d9 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -98,6 +98,7 @@
<p class="lede">A historic signature, printed where a chest pocket would sit. Made to order.</p>
<div id="disclosure" class="disclosure">Loading trademark screen…</div>
+ <p id="selectionNotice" class="disclosure warn" role="status" hidden></p>
<div class="controls">
<label>Sort
@@ -227,6 +228,20 @@ async function boot(){
(r.disclosure.purchasable ? '' : ' <b>Sales open soon</b> — browse now; add your email to be notified.');
renderCartBadge();
render();
+ // Gallery purchase links carry the chosen signature into its configurator.
+ // Only resolve from the server's eligible catalog; never bypass that gate.
+ const selected = new URLSearchParams(location.search);
+ if (selected.has('qid') || selected.has('name')) {
+ const sig = selected.has('qid')
+ ? r.signatures.find(s=>s.qid===selected.get('qid'))
+ : r.signatures.find(s=>s.full_name.toLowerCase()===selected.get('name').toLowerCase());
+ if (sig) openProduct(sig.qid);
+ else {
+ const notice = document.getElementById('selectionNotice');
+ notice.textContent = 'This signature is not available on clothing. Choose from the available historical signatures below.';
+ notice.hidden = false;
+ }
+ }
}
function render(){
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 88129c3..38a0f1c 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,67 +1,105 @@
{
- "intent": "Verify TK-10286 /wear left-chest signature apparel is complete, locally functional, and safely gated from live sales and Printful submission.",
- "riskTier": "R2",
- "environment": "Local CelebritySignatures server on http://127.0.0.1:19920 with WEAR_SALES_LIVE unset and no PRINTFUL_API_KEY.",
- "buildIdentity": "a84889012b42d47128d7ef32f2a96b611d71dd1e",
- "timestamp": "2026-09-03T20:38:25Z",
+ "intent": "TK-10286: make mural and clothing shopping options discoverable on the Politics gallery and carry the chosen signature into each destination; independently verify black-polo fulfillment artwork.",
+ "riskTier": "R2 local UI; R3 isolated fulfillment fixtures; production deployment held for approval",
+ "timestamp": "2026-09-09T15:23:50.599Z",
+ "buildIdentity": {
+ "base": "2dd7c66",
+ "artworkFix": "c9d72f3",
+ "artworkRegressionTest": "c90a83c",
+ "files": {
+ "public/index.html": "ca359040925874615ceec01debe93de33cd34a4fd682c012bb79cfa5ec732a1c",
+ "public/murals.html": "92882d5dc157e218e19b7522501f8cffcac2be00e78faca78d64a47f6a1ca3fc",
+ "public/wear.html": "c0627f6b104c608e91dbab355fde4c8457edfc591cdf3be346cc616511fb1445",
+ "server.js": "a205a957da764da9efb1fb13489687fd0364bd7bd28f518990359da33d1d0a1b",
+ "scripts/reconcile-wear-orders.mjs": "d943e7eb33471aaa74da9cb96ae010d3c263b74623088ecc4463898ecff27312",
+ "scripts/submit-pod-draft-printify.mjs": "9c94dd5c0b8528ac14d9b66d38e2ef54ab36ffe9f887ad4f6affa480acae3d7f"
+ }
+ },
+ "environment": {
+ "local": "http://127.0.0.1:9956",
+ "production": "https://celebsignatures.com",
+ "browser": "Installed Google Chrome; desktop 1280x900 and mobile 390x844"
+ },
"baseline": {
- "gitWorkingTree": "clean before verification",
- "ticketState": "doing after ownership claim; previously intentionally blocked on external/legal and live activation gates",
- "salesLive": false,
- "printfulVariantMappings": 0
+ "cleanAtSessionStart": "f2976c0",
+ "productionState": "Peer enabled public checkout in Stripe TEST mode during this session; live GET at 15:16Z reports purchasable=true. Local GET reports false. Real payment activation and paid order proof remain peer-owned gates.",
+ "concurrentChanges": "Other session owns compact-card styling/availability icons and payment configuration. This session preserved those changes and owns shopping links/deep-link handlers plus evidence."
},
"commands": [
- "node --check server.js",
- "node --check scripts/submit-pod-draft.mjs",
- "node --check scripts/fetch-printful-catalog.mjs",
- "JSON.parse data/wear-templates.json and data/tm-clearance.json",
- "node scripts/submit-pod-draft.mjs",
- "PORT=19920 node server.js",
- "GET /api/wear/signatures",
- "POST /api/wear-checkout with a valid catalog selection",
- "3x browser verification of /wear with .card interaction"
+ "node --test test/printify-pod.test.mjs test/wear-artwork-flow.test.mjs",
+ "node verification/gallery-shopping.e2e.cjs",
+ "node /Users/macstudio3/.agents/skills/3x/run.js --url http://127.0.0.1:9956/?cat=Politics --expect \"Shop clothing\" --selector \".card .shop-clothing\" --no-open",
+ "Inline script parse with node:vm for public/index.html, public/murals.html, public/wear.html",
+ "git diff --check",
+ "Read-only production sha256sum of server.js, reconcile-wear-orders.mjs, submit-pod-draft-printify.mjs; command -v convert"
],
"assertions": [
{
- "boundary": "file",
- "check": "Server, Printful sender, and catalog helper parse; JSON fixtures parse.",
+ "boundary": "UI",
+ "check": "Desktop and mobile: Politics cards expose Mural and Clothing links; details show both choices without scrolling; navigation includes clothing; no horizontal page overflow.",
"verdict": "PASS"
},
{
- "boundary": "API",
- "check": "GET /api/wear/signatures returns placement=left_chest, count=2955, purchasable=false, two garments, and a $4 2XL upcharge on each garment.",
+ "boundary": "UI -> clothing",
+ "check": "Click Lincoln clothing: Q91 carried in URL, Abraham Lincoln configurator opens, 3 garment choices, black polo 2XL price $48; back returns to Politics.",
"verdict": "PASS"
},
{
- "boundary": "UI",
- "check": "The /wear catalog rendered 2955 cards; opening a card displayed the signature within the left-chest placement box and the CTA read 'Notify me when live'.",
- "verdict": "PASS",
- "artifact": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-DbyCEe/e2e.png"
+ "boundary": "UI -> murals",
+ "check": "Click details mural option: finder displays Abraham Lincoln and Politics & Statecraft is selected; payment button remains present.",
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "eligibility",
+ "check": "Unknown clothing qid shows not-available message, never opens another signature, retains eligible catalog.",
+ "verdict": "PASS"
},
{
"boundary": "browser",
- "check": "HTTP, headless paint, Playwright interaction, and installed Chrome parity passed with zero JavaScript errors.",
- "verdict": "PASS",
- "artifact": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-DbyCEe"
+ "check": "Both viewport journeys: zero page JavaScript errors. 3x: HTTP, headless render, Playwright render, Chrome parity all pass (4/4 attempted).",
+ "verdict": "PASS"
},
{
- "boundary": "negative/payment",
- "check": "POST /api/wear-checkout returns ok=false and comingSoon=true while WEAR_SALES_LIVE is unset; no Stripe session or order is created.",
+ "boundary": "engine parity",
+ "check": "Safari/WebKit and Firefox not installed in 3x runner.",
+ "verdict": "SKIP",
+ "reason": "Additional engine coverage unavailable; no cross-engine success claimed."
+ },
+ {
+ "boundary": "payment -> queue -> file -> provider payload",
+ "check": "Actual server /wear-success and actual reconciler in disposable projects with intercepted synthetic Stripe/SVG responses: light-ink PNG preserves alpha; both cache miss/hit return public absolute URL; real static serving yields matching PNG; sender preserves URL and correct black/M variant.",
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "retry and negative",
+ "check": "Both producers persist paid status and one composite draft per item. Retrying does not duplicate; disabled checkout and invalid success URL leave order/queue files unchanged.",
"verdict": "PASS"
},
{
- "boundary": "fulfillment",
- "check": "Printful sender reports zero pending drafts and dry-run mode; --apply, PRINTFUL_API_KEY, and WEAR_SALES_LIVE=1 are all required, while mappings remain empty/fail-loud.",
+ "boundary": "deployed runtime",
+ "check": "Remote server/reconciler/sender SHA256 exactly match local verified sources; /usr/bin/convert present.",
"verdict": "PASS"
},
{
- "boundary": "cross-browser",
- "check": "Safari and Firefox automation were unavailable in the installed runner environment and intentionally not GUI-opened.",
+ "boundary": "public shopping links",
+ "check": "Deployment of this session shopping-link HTML changes.",
"verdict": "SKIP",
- "reason": "Non-critical engine-parity check; Chrome and three independent request/render/interaction layers passed."
+ "reason": "Customer-facing deployment requires user approval under ticket instructions; no deploy run by this session."
+ },
+ {
+ "boundary": "real purchase/fulfillment",
+ "check": "Completed live-site Stripe TEST purchase and Printify order acceptance/production.",
+ "verdict": "SKIP",
+ "reason": "Current active peer owns launch handoff; this session has no permission for provider order submission or real charges."
}
],
- "sideEffects": "No payment, order, Printful request, deployment, publish, DNS, remote push, or canonical data write occurred.",
- "cleanup": "Temporary local server stopped after verification; application data remained unchanged.",
- "overallVerdict": "PASS for the implemented local /wear feature and its safe-off guard. Live activation remains intentionally blocked and requires explicit authorization plus the external/legal and credential prerequisites recorded on TK-10286."
+ "artifacts": {
+ "shopping": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk10286-shopping-bCF6Aw",
+ "gallery3x": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-TMkf1D",
+ "wear3x": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-YB6Clw",
+ "repeatableShoppingRunner": "verification/gallery-shopping.e2e.cjs",
+ "repeatableFulfillmentTest": "test/wear-artwork-flow.test.mjs"
+ },
+ "cleanup": "Synthetic payment fixture projects removed; test servers stopped; browser contexts closed. No real orders, credentials, payment flags, provider settings, external orders or production files changed by this session. Browser runner blocks all non-GET/HEAD requests.",
+ "verdict": "PARTIAL: local shopping fix and isolated artwork integration verified; publication and real purchase/fulfillment remain gated."
}
diff --git a/verification/gallery-shopping.e2e.cjs b/verification/gallery-shopping.e2e.cjs
new file mode 100644
index 0000000..6a033f4
--- /dev/null
+++ b/verification/gallery-shopping.e2e.cjs
@@ -0,0 +1,75 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+let pw;
+for (const location of ['playwright', path.join(os.homedir(), 'Projects/Designer-Wallcoverings/node_modules/playwright'), path.join(os.homedir(), 'node_modules/playwright')]) {
+ try { pw = require(location); break; } catch {}
+}
+if (!pw) throw new Error('Playwright is required');
+const base = process.env.WEAR_PROOF_URL || 'http://127.0.0.1:9956';
+const artifacts = fs.mkdtempSync(path.join(os.tmpdir(), 'tk10286-shopping-'));
+const checks = [];
+(async () => {
+ const browser = await pw.chromium.launch({ channel: 'chrome' });
+ try {
+ for (const [name, viewport] of [['desktop', { width:1280, height:900 }], ['mobile', { width:390, height:844 }]]) {
+ const context = await browser.newContext({ viewport, recordVideo:{ dir:artifacts } });
+ // Every journey below is navigation or browser-local state only.
+ await context.route('**/*', route => {
+ if (!['GET','HEAD'].includes(route.request().method())) return route.abort();
+ return route.continue();
+ });
+ const page = await context.newPage();
+ const errors = [];
+ page.on('pageerror', e => errors.push(e.message));
+ await page.goto(base+'/?cat=Politics', { waitUntil:'domcontentloaded' });
+ 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-clothing').isVisible());
+ assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
+ await card.scrollIntoViewIfNeeded();
+ await page.screenshot({ path:path.join(artifacts, `${name}-card.png`) });
+ await card.locator('.shop-clothing').click();
+ await page.waitForURL('**/wear?**');
+ 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(), 3);
+ 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.screenshot({ path:path.join(artifacts, `${name}-clothing.png`) });
+ await page.goBack({ waitUntil:'domcontentloaded' });
+ assert.equal(new URL(page.url()).searchParams.get('cat'), 'Politics');
+ await card.locator('.sig').click();
+ await page.locator('#artistPop:not([hidden]) .ap-shop').waitFor();
+ const choices = page.locator('.ap-shop');
+ const box = await choices.boundingBox();
+ 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 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);
+ assert.deepEqual(errors, []);
+ checks.push({ viewport:name, verdict:'PASS', paths:['Politics card -> selected clothing', 'Politics details -> matching mural finder', 'unknown clothing signature rejected'], pageErrors:errors });
+ await context.close();
+ }
+ } finally { await browser.close(); }
+ fs.writeFileSync(path.join(artifacts,'result.json'), JSON.stringify({ base, timestamp:new Date().toISOString(), checks, externalMutations:0, artifacts },null,2));
+ console.log(JSON.stringify({ checks, artifacts },null,2));
+})().catch(e => { console.error(e); process.exitCode=1; });
← 2dd7c66 wear: persist sort selection to localStorage, matching densi
·
back to CelebritySignatures
·
Add three hat previews and socks with guarded purchase and f f44f65b →