← 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
M whimsical-compare/daily/scripts/diagnose_sf_upload.jsM whimsical-compare/daily/scripts/upload_sf.jsM wpb-uploaders/platforms/etsy.js
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 →