← 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));
})();