← 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
A test/README.mdA test/ui.spec.js
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 →