[object Object]

← back to Marketing Command Center

add honest cross browser CTA harness

00e262c056454e71ac77e74530a0826d4979b64c · 2026-08-28 09:50:04 -0700 · Steve Abrams

Files touched

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 →