← back to Marketing Command Center
add honest cross browser CTA harness
00e262c056454e71ac77e74530a0826d4979b64c · 2026-08-28 09:50:04 -0700 · Steve Abrams
Files touched
A cta/CTA-REPORT.mdM package-lock.jsonM package.jsonA scripts/cta-quickpost.js
Diff
commit 00e262c056454e71ac77e74530a0826d4979b64c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 28 09:50:04 2026 -0700
add honest cross browser CTA harness
---
cta/CTA-REPORT.md | 55 ++++++++++++++++++++++++
package-lock.json | 50 ++++++++++++++++++++++
package.json | 3 ++
scripts/cta-quickpost.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 214 insertions(+)
diff --git a/cta/CTA-REPORT.md b/cta/CTA-REPORT.md
new file mode 100644
index 0000000..44c8563
--- /dev/null
+++ b/cta/CTA-REPORT.md
@@ -0,0 +1,55 @@
+# CTA Report — Kid-friendly Quick Post
+
+**Verdict:** FAIL — 0/2 component-engine checks clean.
+
+**Safety:** Copy generation and publish endpoints were browser-intercepted. No social post, draft write, email, or paid generation occurred.
+
+## Browser coverage
+
+- Chrome/Chromium recording: unavailable
+- Safari/WebKit recording: unavailable
+- Browserbase: skipped by design because the target is localhost and Browserbase cannot reach the Mac-local service.
+
+## Components
+
+| Browser | Component | Rendered | Worked | Errors |
+|---|---|---:|---:|---|
+| chrome | Browser engine startup | ✗ | ✗ | browserType.launch: Executable doesn't exist at /Users/macstudio3/Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell
+╔════════════════════════════════════════════════════════════╗
+║ Looks like Playwright was just installed or updated. ║
+║ Please run the following command to download new browsers: ║
+║ ║
+║ npx playwright install ║
+║ ║
+║ <3 Playwright Team ║
+╚════════════════════════════════════════════════════════════╝ |
+| safari | Browser engine startup | ✗ | ✗ | browserType.launch: Executable doesn't exist at /Users/macstudio3/Library/Caches/ms-playwright/webkit-2336/pw_run.sh
+╔════════════════════════════════════════════════════════════╗
+║ Looks like Playwright was just installed or updated. ║
+║ Please run the following command to download new browsers: ║
+║ ║
+║ npx playwright install ║
+║ ║
+║ <3 Playwright Team ║
+╚════════════════════════════════════════════════════════════╝ |
+
+## Chrome-vs-Safari mismatches
+
+- chrome: Browser engine startup — browserType.launch: Executable doesn't exist at /Users/macstudio3/Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell
+╔════════════════════════════════════════════════════════════╗
+║ Looks like Playwright was just installed or updated. ║
+║ Please run the following command to download new browsers: ║
+║ ║
+║ npx playwright install ║
+║ ║
+║ <3 Playwright Team ║
+╚════════════════════════════════════════════════════════════╝
+- safari: Browser engine startup — browserType.launch: Executable doesn't exist at /Users/macstudio3/Library/Caches/ms-playwright/webkit-2336/pw_run.sh
+╔════════════════════════════════════════════════════════════╗
+║ Looks like Playwright was just installed or updated. ║
+║ Please run the following command to download new browsers: ║
+║ ║
+║ npx playwright install ║
+║ ║
+║ <3 Playwright Team ║
+╚════════════════════════════════════════════════════════════╝
diff --git a/package-lock.json b/package-lock.json
index fcfdec2..202d761 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,9 @@
"dependencies": {
"express": "^4.22.2",
"pg": "^8.22.0"
+ },
+ "devDependencies": {
+ "playwright": "^1.62.1"
}
},
"node_modules/accepts": {
@@ -313,6 +316,21 @@
"node": ">= 0.6"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -654,6 +672,38 @@
"split2": "^4.1.0"
}
},
+ "node_modules/playwright": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
+ "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.62.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
+ "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
diff --git a/package.json b/package.json
index 3bf6f5f..e4b633a 100644
--- a/package.json
+++ b/package.json
@@ -11,5 +11,8 @@
"dependencies": {
"express": "^4.22.2",
"pg": "^8.22.0"
+ },
+ "devDependencies": {
+ "playwright": "^1.62.1"
}
}
diff --git a/scripts/cta-quickpost.js b/scripts/cta-quickpost.js
new file mode 100644
index 0000000..de59835
--- /dev/null
+++ b/scripts/cta-quickpost.js
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const { chromium, webkit } = require('playwright');
+const { PROJECT, credentials, mockRiskyWrites, waitForQuickPost, preparePost } = require('./quickpost-test-lib');
+
+const ROOT = path.join(PROJECT, 'cta');
+fs.mkdirSync(path.join(ROOT, 'shots'), { recursive: true });
+fs.mkdirSync(path.join(ROOT, 'rec'), { recursive: true });
+
+const results = [];
+const slug = s => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
+
+async function shot(page, engine, label, phase) {
+ const dir = path.join(ROOT, 'shots', engine);
+ fs.mkdirSync(dir, { recursive: true });
+ await page.screenshot({ path: path.join(dir, `${slug(label)}-${phase}.png`), fullPage: true });
+}
+
+async function action(page, engine, label, fn, effect) {
+ const beforeErrors = page.__errors.length;
+ let ok = true;
+ let message = '';
+ try {
+ await shot(page, engine, label, 'before');
+ await fn();
+ await page.waitForTimeout(150);
+ if (effect) ok = await effect();
+ await shot(page, engine, label, 'after');
+ } catch (error) {
+ ok = false;
+ message = error.message;
+ }
+ results.push({ engine, label, rendered: true, worked: !!ok, errors: page.__errors.slice(beforeErrors), message });
+}
+
+async function run(engine, browserType) {
+ const recDir = path.join(ROOT, 'rec', engine);
+ fs.mkdirSync(recDir, { recursive: true });
+ let browser;
+ try {
+ browser = await browserType.launch({ headless: true });
+ } catch (error) {
+ results.push({ engine, label: 'Browser engine startup', rendered: false, worked: false, errors: [error.message], message: 'Engine unavailable; install its Playwright browser binary.' });
+ return '';
+ }
+ const ctx = await browser.newContext({
+ viewport: { width: 1440, height: 1000 },
+ httpCredentials: credentials(),
+ recordVideo: { dir: recDir, size: { width: 1440, height: 1000 } }
+ });
+ const page = await ctx.newPage();
+ page.__errors = [];
+ page.on('console', m => m.type() === 'error' && page.__errors.push(`console: ${m.text()}`));
+ page.on('pageerror', e => page.__errors.push(`pageerror: ${e.message}`));
+ await mockRiskyWrites(page);
+ await waitForQuickPost(page);
+
+ await action(page, engine, 'Picture asset', () => page.locator('.kp-asset').first().click(), async () => page.locator('.kp-asset.on').count().then(Boolean));
+ await action(page, engine, 'Paste-link details', () => page.locator('.kp-more summary').click(), async () => page.locator('#kp-media').isVisible());
+ await action(page, engine, 'Step Music', () => page.locator('[data-go="2"]').click(), async () => page.locator('[data-step="2"]').isVisible());
+ await action(page, engine, 'Music choice', async () => { const x = page.locator('.kp-choice').nth(1); if (await x.count()) await x.click(); else await page.locator('.kp-choice').first().click(); }, async () => page.locator('.kp-choice.on').count().then(Boolean));
+ await action(page, engine, 'Back button', () => page.locator('#kp-back').click(), async () => page.locator('[data-step="1"]').isVisible());
+ await action(page, engine, 'Next button', () => page.locator('#kp-next').click(), async () => page.locator('[data-step="2"]').isVisible());
+ await action(page, engine, 'Step Words', () => page.locator('[data-go="3"]').click(), async () => page.locator('[data-step="3"]').isVisible());
+ await page.locator('#kp-topic').fill('a new blue grasscloth wallpaper');
+ await action(page, engine, 'Generate suggestions', () => page.locator('#kp-suggest').click(), async () => { await page.locator('.kp-suggestion').first().waitFor(); return (await page.locator('.kp-suggestion').count()) === 3; });
+ await action(page, engine, 'Choose suggestion', () => page.locator('.kp-suggestion').nth(1).click(), async () => (await page.locator('#kp-caption').inputValue()).length > 20);
+ await action(page, engine, 'Step Where', () => page.locator('[data-go="4"]').click(), async () => page.locator('[data-step="4"]').isVisible());
+ await action(page, engine, 'Choose all accounts', () => page.locator('#kp-all').click(), async () => (await page.locator('.kp-channel.on').count()) > 0);
+ await action(page, engine, 'Clear accounts', () => page.locator('#kp-none').click(), async () => (await page.locator('.kp-channel.on').count()) === 0);
+ const firstChannel = page.locator('.kp-channel:not(.off)').first();
+ await action(page, engine, 'Single social account', () => firstChannel.click(), async () => (await page.locator('.kp-channel.on').count()) === 1);
+ await action(page, engine, 'Instagram account menu', async () => { const select = page.locator('#kp-account'); await select.selectOption({ index: Math.min(1, (await select.locator('option').count()) - 1) }); }, async () => page.locator('#kp-account').isEnabled());
+ await action(page, engine, 'Step Check', () => page.locator('[data-go="5"]').click(), async () => page.locator('[data-step="5"]').isVisible());
+ await action(page, engine, 'Confirm checkbox', () => page.locator('#kp-confirm').check(), async () => !(await page.locator('#kp-publish').isDisabled()));
+ await action(page, engine, 'Post now mocked', () => page.locator('#kp-publish').click(), async () => /Done\./.test(await page.locator('#kp-result').textContent()));
+ await page.locator('[data-go="4"]').click(); await page.locator('#kp-all').click(); await page.locator('[data-go="5"]').click();
+ await action(page, engine, 'Save drafts mocked', () => page.locator('#kp-draft').click(), async () => /Saved/.test(await page.locator('#kp-result').textContent()));
+
+ const video = page.video();
+ await ctx.close();
+ const videoPath = video ? await video.path().catch(() => '') : '';
+ await browser.close();
+ return videoPath;
+}
+
+(async () => {
+ const videos = {};
+ for (const [name, type] of [['chrome', chromium], ['safari', webkit]]) videos[name] = await run(name, type);
+ const failures = results.filter(r => !r.worked || r.errors.length);
+ const lines = [
+ '# CTA Report — Kid-friendly Quick Post', '',
+ `**Verdict:** ${failures.length ? 'FAIL' : 'PASS'} — ${results.length - failures.length}/${results.length} component-engine checks clean.`, '',
+ '**Safety:** Copy generation and publish endpoints were browser-intercepted. No social post, draft write, email, or paid generation occurred.', '',
+ '## Browser coverage', '',
+ `- Chrome/Chromium recording: ${videos.chrome || 'unavailable'}`,
+ `- Safari/WebKit recording: ${videos.safari || 'unavailable'}`,
+ '- Browserbase: skipped by design because the target is localhost and Browserbase cannot reach the Mac-local service.', '',
+ '## Components', '', '| Browser | Component | Rendered | Worked | Errors |', '|---|---|---:|---:|---|',
+ ...results.map(r => `| ${r.engine} | ${r.label} | ${r.rendered ? '✓' : '✗'} | ${r.worked ? '✓' : '✗'} | ${(r.errors.join('; ') || r.message || '—').replace(/\|/g, '\\|')} |`), '',
+ '## Chrome-vs-Safari mismatches', '', failures.length ? failures.map(r => `- ${r.engine}: ${r.label} — ${r.errors.join('; ') || r.message}`).join('\n') : 'None.', ''
+ ];
+ fs.writeFileSync(path.join(ROOT, 'CTA-REPORT.md'), lines.join('\n'));
+ console.log(JSON.stringify({ verdict: failures.length ? 'FAIL' : 'PASS', checks: results.length, failures: failures.length, report: path.join(ROOT, 'CTA-REPORT.md') }));
+})().catch(error => { console.error(error); process.exit(1); });
← 337477c auto-data-snapshot: 2026-08-28T09:39:28 (1 data files) — pub
·
back to Marketing Command Center
·
make quick post choices clearer and safer def5ea9 →