← back to Pattern Vault
whimsical-compare/daily/scripts/diagnose_sf_upload.js
96 lines
#!/usr/bin/env node
/*
* READ-ONLY Spoonflower upload-page diagnostic — NO design is created, nothing is published.
* Loads the real session, opens the upload page, dumps the actual inputs/buttons, optionally
* stages a file into the dropzone (which does NOT submit), and re-dumps. Reveals the real
* Save/Publish control + completion flow so upload_sf.js can be calibrated. Prints JSON.
*
* Usage: node diagnose_sf_upload.js [optional-png-to-stage]
*/
const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
const path = require('path'), os = require('os');
const STATE = process.env.SF_STATE || path.join(os.homedir(), '.config/pattern-vault/sf-state.json');
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/126 Safari/537.36';
const stageFile = process.argv[2] || null;
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function dump(pg, label) {
const inputs = await pg.$$eval('input, textarea', els => els.map(e => ({
tag: e.tagName.toLowerCase(), type: e.type || '', name: e.name || '', id: e.id || '',
placeholder: e.placeholder || '', aria: e.getAttribute('aria-label') || '',
}))).catch(() => []);
const buttons = await pg.$$eval('button, [role=button], a[href], input[type=submit]', els =>
[...new Set(els.map(e => (e.innerText || e.value || '').trim()).filter(Boolean))].slice(0, 40)).catch(() => []);
const fileInputs = await pg.$$eval('input[type=file]', els => els.length).catch(() => 0);
return { label, url: pg.url(), fileInputs, inputs: inputs.slice(0, 20), buttons };
}
(async () => {
const out = { state: STATE, steps: [] };
const b = await chromium.launch({ headless: true });
const ctx = await b.newContext({ storageState: STATE, userAgent: UA });
const pg = await ctx.newPage();
try {
await pg.goto('https://www.spoonflower.com/en/my_account', { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {});
await sleep(2500);
out.sessionValid = !pg.url().includes('/login');
if (!out.sessionValid) { out.note = 'SESSION EXPIRED — re-capture with capture_sf_session.js'; console.log(JSON.stringify(out, null, 2)); await b.close(); return; }
await pg.goto('https://www.spoonflower.com/artists/designs/new', { waitUntil: 'domcontentloaded', timeout: 40000 }).catch(() => {});
await sleep(3000);
out.steps.push(await dump(pg, 'upload-page-loaded'));
// DEEP: what is the "Choose Files" control, and where does the consent element live?
out.chooseFiles = await pg.evaluate(() => {
const els = [...document.querySelectorAll('button,label,a,div,span,input')];
const cf = els.find(e => /choose files/i.test((e.innerText || e.value || '').trim()));
if (!cf) return { found: false };
return { found: true, tag: cf.tagName.toLowerCase(), htmlFor: cf.getAttribute('for') || null,
role: cf.getAttribute('role') || null, outerHTML: cf.outerHTML.slice(0, 300),
parentTag: cf.parentElement && cf.parentElement.tagName.toLowerCase() };
}).catch(e => ({ err: String(e).slice(0, 100) }));
out.consent = await pg.evaluate(() => {
const els = [...document.querySelectorAll('button,a,div,span')];
const c = els.find(e => /agree & continue|agree and continue/i.test((e.innerText || '').trim()));
return c ? { found: true, tag: c.tagName.toLowerCase(), outerHTML: c.outerHTML.slice(0, 200) } : { found: false, note: 'not in top document (may be in iframe)' };
}).catch(e => ({ err: String(e).slice(0, 100) }));
out.iframes = await pg.evaluate(() => [...document.querySelectorAll('iframe')].map(f => ({ src: (f.src || '').slice(0, 120), id: f.id || '', title: f.title || '' }))).catch(() => []);
// does clicking "Choose Files" fire a native filechooser?
const chooser = await Promise.race([
pg.waitForEvent('filechooser', { timeout: 6000 }).then(() => true).catch(() => false),
pg.click('text="Choose Files"').then(() => sleep(6500)).then(() => false).catch(() => false),
]).catch(() => false);
out.chooseFilesFiresFilechooser = chooser;
if (stageFile) {
const fi = await pg.$('input[type=file]');
if (fi) {
await fi.setInputFiles(stageFile).catch(e => out.stageError = String(e).slice(0, 120));
await sleep(8000); // let the upload/preview process — still NOT submitted
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));
})();