← back to Marketing Command Center

scripts/screenrecord-quickpost.js

152 lines

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');
const { PROJECT, credentials, mockRiskyWrites, waitForQuickPost } = require('./quickpost-test-lib');

const ROOT = path.join(PROJECT, 'screenrecord', 'quickpost');
const LOG = path.join(ROOT, 'debug-log.jsonl');
const REPORT = path.join(ROOT, 'DEBUG-REPORT.md');
fs.mkdirSync(path.join(ROOT, 'rec'), { recursive: true });

const prior = fs.existsSync(LOG)
  ? fs.readFileSync(LOG, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line))
  : [];
const priorBad = new Set(prior.filter(x => !x.ok || (x.errors || []).length).map(x => x.label));
const rows = [];
const append = row => {
  rows.push(row);
  fs.appendFileSync(LOG, JSON.stringify(row) + '\n');
};

async function act(page, run, label, action, assert) {
  const before = page.__errors.length;
  let ok = true;
  let effect = 'expected state observed';
  try {
    await action();
    await page.waitForTimeout(120);
    ok = await assert();
    if (!ok) effect = 'expected state was not observed';
  } catch (error) {
    ok = false;
    effect = error.message;
  }
  append({ run, ts: new Date().toISOString(), label, ok, effect, errors: page.__errors.splice(before) });
  if (!ok) throw new Error(`${label}: ${effect}`);
}

async function seedPicture(page, run) {
  if (run % 2 === 0 && await page.locator('.kp-asset').count()) {
    await act(page, run, 'picture asset', () => page.locator('.kp-asset').first().click({ force: true }), async () => (await page.locator('.kp-asset.on').count()) === 1);
  } else {
    await act(page, run, 'paste picture link', () => page.locator('#kp-media').fill('https://example.com/wallpaper.jpg'), async () => !!(await page.locator('#kp-media').inputValue()));
  }
}

async function completeWords(page, run) {
  await act(page, run, 'topic input', () => page.locator('#kp-topic').fill(`blue grasscloth run ${run + 1}`), async () => (await page.locator('#kp-topic').inputValue()).includes('blue'));
  await act(page, run, 'generate suggestions', () => page.locator('#kp-suggest').click(), async () => { await page.locator('.kp-suggestion').first().waitFor(); return (await page.locator('.kp-suggestion').count()) === 3; });
  await act(page, run, 'choose suggestion', () => page.locator('.kp-suggestion').nth(run % 3).click(), async () => (await page.locator('#kp-caption').inputValue()).length > 20);
}

async function runJourney(run) {
  const recDir = path.join(ROOT, 'rec', `run${run}`);
  fs.mkdirSync(recDir, { recursive: true });
  const browser = await chromium.launch({ headless: true });
  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);

  // Five distinct combinations: odd runs open the paste-link control first;
  // run 2 exercises an early validation error; run 3 revisits Back; run 4
  // prioritizes any label that failed in earlier persisted runs.
  if (run % 2) await act(page, run, 'paste-link details', () => page.locator('.kp-more summary').click(), () => page.locator('#kp-media').isVisible());
  if (run === 2) await act(page, run, 'picture validation', () => page.locator('#kp-next').click(), async () => /Pick a picture/.test(await page.locator('[data-step="1"] .kp-error').textContent()));
  if (run === 4 && priorBad.size) append({ run, ts: new Date().toISOString(), label: 'prior-failure priority', ok: true, effect: [...priorBad].join(', '), errors: [] });
  const filters = run % 2 ? ['upload', 'catalog', 'all'] : ['all', 'catalog', 'upload'];
  for (const filter of filters) await act(page, run, `filter ${filter}`, () => page.locator(`[data-filter="${filter}"]`).click(), () => page.locator(`[data-filter="${filter}"]`).evaluate(x => x.classList.contains('on')));
  await act(page, run, 'picture search', () => page.locator('#kp-asset-search').fill(`run ${run}`), async () => (await page.locator('#kp-asset-search').inputValue()) === `run ${run}`);
  await page.locator('#kp-asset-search').fill('');
  await page.locator('[data-filter="all"]').click();
  await seedPicture(page, run);
  if (!(run % 2)) await act(page, run, 'paste-link details', () => page.locator('.kp-more summary').click(), () => page.locator('#kp-media').isVisible());
  await act(page, run, 'next to music', () => page.locator('#kp-next').click(), () => page.locator('[data-step="2"]').isVisible());
  await act(page, run, 'picture step tab', () => page.locator('[data-go="1"]').click(), () => page.locator('[data-step="1"]').isVisible());
  await act(page, run, 'step tab forward to music', () => page.locator('#kp-next').click(), () => page.locator('[data-step="2"]').isVisible());
  await act(page, run, 'music choice', async () => {
    const choices = page.locator('.kp-choice');
    const index = run % Math.min(2, await choices.count());
    await choices.nth(index).click();
  }, async () => (await page.locator('.kp-choice.on').count()) === 1);
  if (run === 3) {
    await act(page, run, 'back button', () => page.locator('#kp-back').click(), () => page.locator('[data-step="1"]').isVisible());
    await act(page, run, 'return to music', () => page.locator('#kp-next').click(), () => page.locator('[data-step="2"]').isVisible());
  }
  await act(page, run, 'next to words', () => page.locator('#kp-next').click(), () => page.locator('[data-step="3"]').isVisible());
  await completeWords(page, run);
  await act(page, run, 'edit final words', async () => { const c = page.locator('#kp-caption'); await c.fill((await c.inputValue()) + ` Edited ${run}.`); }, async () => (await page.locator('#kp-caption').inputValue()).endsWith(`Edited ${run}.`));
  await act(page, run, 'music step tab', () => page.locator('[data-go="2"]').click(), () => page.locator('[data-step="2"]').isVisible());
  await act(page, run, 'step tab forward to words', () => page.locator('#kp-next').click(), () => page.locator('[data-step="3"]').isVisible());
  await act(page, run, 'next to accounts', () => page.locator('#kp-next').click(), () => page.locator('[data-step="4"]').isVisible());
  if (run === 1) {
    await act(page, run, 'choose all accounts', () => page.locator('#kp-all').click(), async () => (await page.locator('.kp-channel.on').count()) > 0);
    await act(page, run, 'clear accounts', () => page.locator('#kp-none').click(), async () => (await page.locator('.kp-channel.on').count()) === 0);
  }
  if (run % 2 === 0) await act(page, run, 'single account', () => page.locator('.kp-channel:not(.off)').first().click(), async () => (await page.locator('.kp-channel.on').count()) === 1);
  else await act(page, run, 'choose all accounts final', () => page.locator('#kp-all').click(), async () => (await page.locator('.kp-channel.on').count()) > 0);
  await act(page, run, 'account menu', async () => { const s = page.locator('#kp-account'); await s.selectOption({ index: Math.min(run % 2, (await s.locator('option').count()) - 1) }); }, () => page.locator('#kp-account').isEnabled());
  await act(page, run, 'words step tab', () => page.locator('[data-go="3"]').click(), () => page.locator('[data-step="3"]').isVisible());
  await act(page, run, 'step tab forward to accounts', () => page.locator('#kp-next').click(), () => page.locator('[data-step="4"]').isVisible());
  await act(page, run, 'next to check', () => page.locator('#kp-next').click(), () => page.locator('[data-step="5"]').isVisible());
  await act(page, run, 'where step tab', () => page.locator('[data-go="4"]').click(), () => page.locator('[data-step="4"]').isVisible());
  await act(page, run, 'step tab forward to check', () => page.locator('#kp-next').click(), () => page.locator('[data-step="5"]').isVisible());
  await act(page, run, 'check step tab', () => page.locator('[data-go="5"]').click(), () => page.locator('[data-step="5"]').isVisible());
  await act(page, run, 'review picture rendered', () => page.locator('#kp-review img').waitFor(), async () => page.locator('#kp-review img').evaluate(img => img.complete && img.naturalWidth > 0));
  if (run % 2 === 0) {
    await act(page, run, 'confirm checkbox', () => page.locator('#kp-confirm').check(), async () => !(await page.locator('#kp-publish').isDisabled()));
    await act(page, run, 'post now mocked', () => page.locator('#kp-publish').click(), async () => /Done\./.test(await page.locator('#kp-result').textContent()));
  } else {
    await act(page, run, 'save drafts mocked', () => page.locator('#kp-draft').click(), async () => /Saved/.test(await page.locator('#kp-result').textContent()));
  }
  append({ run, ts: new Date().toISOString(), label: 'run complete', ok: page.__errors.length === 0, effect: `combination-${run}`, errors: page.__errors.splice(0) });
  const video = page.video();
  await ctx.close();
  const videoPath = video ? await video.path() : '';
  await browser.close();
  return videoPath;
}

(async () => {
  const videos = [];
  for (let run = 0; run < 5; run++) videos.push(await runJourney(run));
  const bad = rows.filter(x => !x.ok || x.errors.length);
  const report = [
    '# Quick Post five-run screen-record report', '',
    `**Verdict:** ${bad.length ? 'FAIL' : 'PASS'} — ${rows.length - bad.length}/${rows.length} recorded actions clean across five distinct full-control combinations.`, '',
    '**Safety:** Copy generation, live publish, and draft endpoints were intercepted in the browser. No external post, draft write, email, deploy, or spend occurred.', '',
    '## Recordings', '',
    ...videos.map((v, i) => `- Run ${i} (combination-${i}): ${v}`), '',
    '## Combination coverage', '',
    '- Every run: all three filters, search, paste-link control, picture choice, music, topic/suggestions, direct caption edit, all five step tabs, Back/Next, channel selectors, account menu, review action.',
    '- Run 0: DOM-order happy path, asset picker, one account, mocked post.',
    '- Run 1: reverse filter order, paste-link first, choose-all → clear → choose-all, mocked draft.',
    '- Run 2: validation error first, asset picker, alternate suggestion, mocked post.',
    '- Run 3: reverse filter order, paste-link first, extra Back/forward revisit, mocked draft.',
    '- Run 4: persisted prior failures first when present, asset path, mocked post.', '',
    '## Failures and order-dependent issues', '',
    bad.length ? bad.map(x => `- Run ${x.run}: ${x.label} — ${x.effect}; ${(x.errors || []).join('; ')}`).join('\n') : 'None.', ''
  ];
  fs.writeFileSync(REPORT, report.join('\n'));
  console.log(JSON.stringify({ verdict: bad.length ? 'FAIL' : 'PASS', runs: 5, actions: rows.length, failures: bad.length, report: REPORT }));
  if (bad.length) process.exitCode = 1;
})().catch(error => { console.error(error); process.exit(1); });