← back to Marketing Command Center

scripts/cta-quickpost.js

127 lines

#!/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}`));
  page.on('response', response => { if (response.status() === 404) page.__errors.push(`404: ${response.url()}`); });
  await mockRiskyWrites(page);
  await waitForQuickPost(page);

  await action(page, engine, 'All ideas filter', () => page.locator('[data-filter="all"]').click(), async () => page.locator('[data-filter="all"]').evaluate(x => x.classList.contains('on')));
  await action(page, engine, 'DW picks filter', () => page.locator('[data-filter="catalog"]').click(), async () => page.locator('[data-filter="catalog"]').evaluate(x => x.classList.contains('on')));
  await action(page, engine, 'My uploads filter', () => page.locator('[data-filter="upload"]').click(), async () => page.locator('[data-filter="upload"]').evaluate(x => x.classList.contains('on')));
  await action(page, engine, 'Picture search', () => page.locator('#kp-asset-search').fill('blue'), async () => (await page.locator('#kp-asset-search').inputValue()) === 'blue');
  await page.locator('#kp-asset-search').fill('');
  await page.locator('[data-filter="all"]').click();
  await action(page, engine, 'Picture asset', async () => { const asset = page.locator('.kp-asset').first(); if (await asset.count()) await asset.click({force:true}); else await page.locator('#kp-media').fill('https://example.com/wallpaper.jpg'); }, async () => (await page.locator('.kp-asset.on').count()) > 0 || !!(await page.locator('#kp-media').inputValue()));
  await action(page, engine, 'Paste-link details', () => page.locator('.kp-more summary').click(), async () => page.locator('#kp-media').isVisible());
  // Keep the journey deterministic even when the live catalog is empty or slow.
  await page.locator('#kp-media').fill('https://example.com/wallpaper.jpg');
  await action(page, engine, 'Step Music', () => page.locator('#kp-next').click(), async () => page.locator('[data-step="2"]').isVisible());
  await action(page, engine, 'Picture step tab', () => page.locator('[data-go="1"]').click(), async () => page.locator('[data-step="1"]').isVisible());
  await action(page, engine, 'Return to Music', () => page.locator('#kp-next').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('#kp-next').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, 'Edit final words', async () => { const c = page.locator('#kp-caption'); await c.fill((await c.inputValue()) + ' Edited.'); }, async () => /Edited\.$/.test(await page.locator('#kp-caption').inputValue()));
  await action(page, engine, 'Music step tab', () => page.locator('[data-go="2"]').click(), async () => page.locator('[data-step="2"]').isVisible());
  await action(page, engine, 'Return to Words', () => page.locator('#kp-next').click(), async () => page.locator('[data-step="3"]').isVisible());
  await action(page, engine, 'Step Where', () => page.locator('#kp-next').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, 'Words step tab', () => page.locator('[data-go="3"]').click(), async () => page.locator('[data-step="3"]').isVisible());
  await action(page, engine, 'Return to Where', () => page.locator('#kp-next').click(), async () => page.locator('[data-step="4"]').isVisible());
  await action(page, engine, 'Step Check', () => page.locator('#kp-next').click(), async () => page.locator('[data-step="5"]').isVisible());
  await action(page, engine, 'Where step tab', () => page.locator('[data-go="4"]').click(), async () => page.locator('[data-step="4"]').isVisible());
  await action(page, engine, 'Return to Check', () => page.locator('#kp-next').click(), async () => page.locator('[data-step="5"]').isVisible());
  await action(page, engine, 'Check step tab', () => page.locator('[data-go="5"]').click(), async () => page.locator('[data-step="5"]').isVisible());
  await action(page, engine, 'Review picture rendered', () => page.locator('#kp-review img').waitFor(), async () => page.locator('#kp-review img').evaluate(img => img.complete && img.naturalWidth > 0));
  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('#kp-next').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); });