[object Object]

← back to Studio Zero

auto-save: 2026-07-25T11:35:57 (1 files) — test/

ce4a40d6e44b5f104bafa33f7bc8d73d3fce9c0f · 2026-07-25 11:36:00 -0700 · Steve Abrams

Files touched

Diff

commit ce4a40d6e44b5f104bafa33f7bc8d73d3fce9c0f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:36:00 2026 -0700

    auto-save: 2026-07-25T11:35:57 (1 files) — test/
---
 test/README.md  | 14 +++++++++++
 test/ui.spec.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+)

diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..cdff731
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,14 @@
+# Studio Zero — UI verification
+
+`ui.spec.js` is a headless Playwright suite (18 checks) covering the wizard: hero,
+7-step stepper, **live** generation via the Max-plan `claude` CLI (mode chip reads
+"LIVE"), markdown render, Step-3 calendar sort+density grid, saved-project cards with
+created date+time, and the download/copy-markdown export.
+
+Run (needs Playwright on NODE_PATH; the app served on $BASE):
+```bash
+SZ_PROVIDER=demo PORT=4321 node ../server.js &        # or omit SZ_PROVIDER for the LIVE path
+SAVEDIR=$(pwd)/.. NODE_PATH=~/Projects/animals/node_modules BASE=http://127.0.0.1:4321 node ui.spec.js
+```
+Exit 0 = all checks pass, 0 console/page errors. NOTE: avoid WHATWG "bad ports"
+(4190, 6000, 5060, …) — undici fetch refuses them ("fetch failed / bad port").
diff --git a/test/ui.spec.js b/test/ui.spec.js
new file mode 100644
index 0000000..278a15a
--- /dev/null
+++ b/test/ui.spec.js
@@ -0,0 +1,72 @@
+const { chromium } = require('playwright');
+(async () => {
+  const errors = [];
+  const browser = await chromium.launch();
+  const page = await browser.newPage();
+  page.on('console', m => { if (m.type() === 'error') errors.push('console:' + m.text()); });
+  page.on('pageerror', e => errors.push('pageerror:' + e.message));
+  const base = process.env.BASE || 'http://localhost:4173';
+  const log = [];
+  const ok = (c, m) => log.push((c ? 'PASS ' : 'FAIL ') + m);
+
+  await page.goto(base, { waitUntil: 'networkidle' });
+  ok(await page.locator('.brand').isVisible(), 'brand renders');
+  const mode = await page.locator('#modeChip').textContent();
+  ok(/DEMO|LIVE/.test(mode), 'mode chip: ' + mode.trim());
+  ok(await page.locator('#startBtn').isVisible(), 'hero CTA visible');
+
+  await page.click('#startBtn');
+  await page.waitForSelector('#stepper .sdot');
+  const dots = await page.locator('#stepper .sdot').count();
+  ok(dots === 7, 'stepper has 7 nodes (' + dots + ')');
+  ok((await page.locator('#panel h2').textContent()).includes('Niche'), 'step 1 = Niche');
+
+  // Step 1 generate (no inputs)
+  await page.click('#genBtn');
+  await page.waitForFunction(() => { const o = document.querySelector('#mdOut'); return o && o.textContent && !/working/i.test(o.textContent); }, null, { timeout: 175000, polling: 800 });
+  ok((await page.locator('#mdOut').textContent()).length > 40, 'step1 deliverable rendered');
+  ok(await page.locator('#outBadge').isVisible(), 'output badge shown');
+  // table rendered from markdown?
+  ok((await page.locator('#mdOut').textContent()).length > 200, 'step1 deliverable substantial');
+
+  // Jump to step 3 (roadmap) via stepper dot #3
+  await page.locator('#stepper .sdot').nth(2).click();
+  ok((await page.locator('#panel h2').textContent()).includes('Roadmap'), 'step 3 = Roadmap');
+  // fill niche field
+  const nicheField = page.locator('#panel input').first();
+  await nicheField.fill('personal finance for freelancers');
+  await page.click('#genBtn');
+  await page.waitForFunction(() => document.querySelector('#calGrid') && document.querySelectorAll('#calGrid .vcard').length > 2, null, { timeout: 175000, polling: 800 });
+  const vcount = await page.locator('#calGrid .vcard').count();
+  ok(vcount >= 8, 'calendar grid rendered (' + vcount + ' cards, expect ~12)');
+  // sort control
+  await page.selectOption('#calSort', 'az');
+  ok((await page.locator('#calSort').inputValue()) === 'az', 'calendar sort switched to A→Z');
+  // density slider
+  await page.locator('#calDens').fill('2');
+  const cols = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--cols').trim());
+  ok(cols === '2', 'density slider set --cols=2 (' + cols + ')');
+
+  // export: toolbar present + plan doc content + real download
+  ok(await page.locator('#builderbar #pname').isVisible(), 'builder toolbar (rename) present');
+  const dlEnabled = !(await page.locator('#dlBtn').getAttribute('class')).includes('done0');
+  ok(dlEnabled, 'download button enabled after a step is done');
+  const planLen = await page.evaluate(() => planMarkdown().length);
+  const planHasTitle = await page.evaluate(() => /Step 3 · 90-Day Content Roadmap/.test(planMarkdown()));
+  ok(planLen > 300, 'plan markdown assembled (' + planLen + ' chars)');
+  ok(planHasTitle, 'plan includes generated step deliverable');
+  const [dl] = await Promise.all([ page.waitForEvent('download', { timeout: 10000 }), page.click('#dlBtn') ]);
+  ok(/^studio-zero-.*\.html$/.test(dl.suggestedFilename()), 'download fires: ' + dl.suggestedFilename());
+
+  // back home, saved card with created date+time
+  await page.click('#homeBtn');
+  await page.waitForSelector('#savedGrid .pcard');
+  const when = await page.locator('#savedGrid .pcard .when').first().textContent();
+  ok(/\d/.test(when) && /(AM|PM|:)/.test(when), 'saved card shows created date+time: ' + when.trim());
+
+  await page.screenshot({ path: '/tmp/sz-shot.png', fullPage: false });
+  await browser.close();
+  console.log(log.join('\n'));
+  console.log('\nconsole/page errors: ' + (errors.length ? '\n' + errors.join('\n') : 'NONE'));
+  process.exit(log.some(l => l.startsWith('FAIL')) || errors.length ? 1 : 0);
+})().catch(e => { console.error('HARNESS ERROR', e); process.exit(2); });

← dfe27aa 5x: REPORT — 2 clean sweeps (favicon + conditional export bu  ·  back to Studio Zero  ·  studio-zero: first Kamatera deploy config (pm2 studio-zero : 6e679f7 →