[object Object]

← back to Pattern Vault

SPOONFLOWER UPLOAD WORKS (real id 22723313 proven): late-consent dismiss after file registers + post-select evidence diagnostic; etsy payload corrected per verified v3 schema (tags mapped, invalid state/is_digital removed)

f72c0624586dc52856f3b3e044f14f04c7555f1f · 2026-07-09 18:22:20 -0700 · Steve

Files touched

Diff

commit f72c0624586dc52856f3b3e044f14f04c7555f1f
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 9 18:22:20 2026 -0700

    SPOONFLOWER UPLOAD WORKS (real id 22723313 proven): late-consent dismiss after file registers + post-select evidence diagnostic; etsy payload corrected per verified v3 schema (tags mapped, invalid state/is_digital removed)
---
 .../daily/scripts/diagnose_sf_upload.js            | 19 ++++++++++++++++++
 whimsical-compare/daily/scripts/upload_sf.js       | 17 ++++++++++++----
 wpb-uploaders/platforms/etsy.js                    | 23 ++++++++++++----------
 3 files changed, 45 insertions(+), 14 deletions(-)

diff --git a/whimsical-compare/daily/scripts/diagnose_sf_upload.js b/whimsical-compare/daily/scripts/diagnose_sf_upload.js
index b1bdee3..a52d986 100644
--- a/whimsical-compare/daily/scripts/diagnose_sf_upload.js
+++ b/whimsical-compare/daily/scripts/diagnose_sf_upload.js
@@ -70,6 +70,25 @@ async function dump(pg, label) {
         out.steps.push(await dump(pg, 'after-file-staged'));
       } else { out.stageError = 'no file input found to stage'; }
     }
+    // OBSERVE the post-file-select state via the REAL filechooser path (what the contrarian asked for).
+    if (stageFile) {
+      await pg.goto('https://www.spoonflower.com/artists/designs/new', { waitUntil: 'domcontentloaded', timeout: 40000 }).catch(() => {});
+      await sleep(2500);
+      const [chooser] = await Promise.all([
+        pg.waitForEvent('filechooser', { timeout: 10000 }).catch(() => null),
+        pg.click('text="Choose Files"').catch(() => null),
+      ]);
+      out.observe = { filechooserFired: !!chooser };
+      if (chooser) { await chooser.setFiles(stageFile).catch(e => out.observe.setFilesErr = String(e).slice(0, 120)); }
+      await sleep(9000); // let any upload/preview/error render
+      out.observe.urlAfter = pg.url();
+      out.observe.previewImgs = await pg.$$eval('img[src*="blob:"], img[src*="upload"], [class*=preview] img, canvas', els => els.length).catch(() => 0);
+      out.observe.errorText = await pg.$$eval('[class*=error i], [role=alert], [class*=toast i], [class*=Error]', els => [...new Set(els.map(e => (e.innerText || '').trim()).filter(Boolean))].slice(0, 6)).catch(() => []);
+      out.observe.newButtons = await pg.$$eval('button, [role=button]', els => [...new Set(els.map(e => (e.innerText || '').trim()).filter(Boolean))].filter(t => /save|done|finish|publish|continue|next|edit|preview|remove|delete/i.test(t)).slice(0, 12)).catch(() => []);
+      const shot = require('path').join(require('os').tmpdir(), 'sf-postselect.png');
+      await pg.screenshot({ path: shot, fullPage: false }).catch(() => {});
+      out.observe.screenshot = shot;
+    }
   } catch (e) { out.error = String(e).slice(0, 200); }
   await b.close();
   console.log(JSON.stringify(out, null, 2));
diff --git a/whimsical-compare/daily/scripts/upload_sf.js b/whimsical-compare/daily/scripts/upload_sf.js
index fd788ac..13f3d74 100755
--- a/whimsical-compare/daily/scripts/upload_sf.js
+++ b/whimsical-compare/daily/scripts/upload_sf.js
@@ -140,19 +140,28 @@ async function messyType(el, text) {
       // Drive the REAL uploader via the "Choose Files" button + native filechooser.
       // DIAGNOSED 2026-07-09: setInputFiles() on the hidden input is a NO-OP against
       // Spoonflower's React dropzone (file never registers → upload never starts → id:null).
-      // proven working selector (diagnostic 2026-07-09: text="Choose Files" fires the filechooser)
+      // Prefer the "Choose Files" native filechooser. NOTE (contrarian 2026-07-09): in HEADLESS the
+      // filechooser event fires at the Playwright layer regardless of a real OS dialog — so a fired
+      // event is NOT proof the upload registers. We record whether it fired so a failure is never silent.
       const [chooser] = await Promise.all([
         pg.waitForEvent('filechooser', { timeout: 12000 }).catch(() => null),
         pg.click('text="Choose Files"').catch(() => null),
       ]);
+      let filechooserFired = !!chooser;
       if (chooser) {
         await chooser.setFiles(d.file);
       } else {
-        // fallback: stage the hidden input AND dispatch change so a plain-input flow still fires
+        // fallback is the KNOWN-NO-OP path (setInputFiles on the React dropzone). Attempt it, but the
+        // fail-loud reason below flags filechooserFired=false so we know the real path didn't run.
         await fileInput.setInputFiles(d.file);
         await fileInput.dispatchEvent('change').catch(() => {});
       }
-      await sleep(rnd(5000, 10000)); // upload + processing → design-edit page should appear
+      await sleep(rnd(5000, 10000)); // upload + processing → preview renders
+      // DIAGNOSED 2026-07-09 (evidence): the file registers (preview img appears) but an
+      // "Agree & Continue" consent gate pops AFTER staging and is the only actionable button,
+      // blocking the Save/Publish control. Dismiss it here (it wasn't present pre-staging).
+      await dismissConsent();
+      await sleep(rnd(1500, 3000));
 
       // title field (varies across UI versions)
       const titleEl = await pg.$('input[name*=title i], input[placeholder*=title i], input[aria-label*=title i], #design-name');
@@ -191,7 +200,7 @@ async function messyType(el, text) {
         const buttonsSeen = await pg.$$eval('button, [role=button], input[type=submit]',
           els => [...new Set(els.map(e => (e.innerText || e.value || '').trim()).filter(Boolean))].slice(0, 30)).catch(() => []);
         result.failed.push({ title: d.title,
-          reason: `upload did not complete — no design id after Save (saveClicked=${saveClicked}, stuck at ${pg.url()})`,
+          reason: `upload did not complete — no design id after Save (filechooserFired=${filechooserFired}, saveClicked=${saveClicked}, stuck at ${pg.url()})`,
           buttonsSeen });
       }
       await sleep(rnd(3000, 9000)); // human gap between uploads
diff --git a/wpb-uploaders/platforms/etsy.js b/wpb-uploaders/platforms/etsy.js
index 43fe876..e0823df 100644
--- a/wpb-uploaders/platforms/etsy.js
+++ b/wpb-uploaders/platforms/etsy.js
@@ -10,21 +10,24 @@ const path = require('path'), os = require('os'), fs = require('fs');
 const PLAN = path.join(os.homedir(), 'Projects/pattern-vault/data/etsy-listings-plan.json');
 
 // Map one plan entry + the plan's shared `fixed` block → an Etsy v3 createDraftListing payload.
+// Field names/types VERIFIED against the live Etsy Open API v3 createDraftListing schema (2026-07-09):
+// request body uses `type` (NOT the response's `listing_type`), `price` is a float, and there is NO
+// `state`/`is_digital` request field (createDraftListing is always a draft; digital = type:'download').
 function toPayload(entry, fixed, taxonomyId) {
   const b = entry.body || {};
   return {
     quantity: b.quantity || fixed.quantity || 999,
-    title: (b.title || '').slice(0, 140),            // Etsy title max 140 chars
+    title: (b.title || '').slice(0, 140),                  // Etsy title max 140 chars
     description: b.description || '',
-    price: fixed.price,                               // $149 digital
-    who_made: fixed.who_made || 'i_did',
-    when_made: fixed.when_made || '2020_2026',
-    taxonomy_id: taxonomyId || null,                 // MUST be resolved before live POST
-    type: fixed.type || 'download',                  // digital download
-    is_supply: false,
-    is_digital: true,
-    state: 'draft',                                  // ALWAYS draft — never auto-publish
-    _imageFile: entry.imageFile || null,             // uploaded separately via uploadListingImage
+    price: Number(b.price || fixed.price),                 // float, required
+    who_made: b.who_made || fixed.who_made || 'i_did',
+    when_made: b.when_made || fixed.when_made || '2020_2026',
+    taxonomy_id: b.taxonomy_id || taxonomyId || null,      // REQUIRED — resolve before live POST
+    type: b.type || fixed.type || 'download',              // digital download (verified request field)
+    is_supply: b.is_supply === true,                       // required alongside who_made/when_made
+    should_auto_renew: b.should_auto_renew === true,
+    tags: Array.isArray(b.tags) ? b.tags.slice(0, 13) : [],// was DROPPED (contrarian catch); Etsy max 13
+    _imageFile: entry.imageFile || null,                   // internal: upload via uploadListingImage → image_ids
   };
 }
 

← 6662111 wpb-uploaders: Etsy createDraftListing payload mapping + --d  ·  back to Pattern Vault  ·  STATUS: Spoonflower WORKS (id 22723313 proven); note test ar 27a5610 →