← back to Rentv Tour

rentv-recapture.mjs

176 lines

import { chromium } from 'playwright';
import fs from 'fs';

const OUT = '/Users/macstudio3/Projects/rentv-master-reel/media/captures';
const BASE = 'https://rentv.agentabrams.com';
const ADMIN = { username: 'admin', password: 'DW2024!' };
const USER = { username: 'user', password: 'RentvUser2026!' };

const CLEAN_CSS = `#cap-header{position:sticky;top:0;z-index:99999;display:flex;align-items:center;justify-content:space-between;height:64px;padding:0 24px;background:#fff;border-bottom:1px solid #e6eaef;font-family:-apple-system,sans-serif}#cap-header .cap-brand{font-size:23px;font-weight:800;color:#0d1b2a;text-decoration:none}#cap-header .cap-brand span{color:#c0392b}#cap-header button{background:none;border:0;color:#0d1b2a;display:inline-flex;padding:9px}`;

async function normalize(page) {
  try {
    await page.addStyleTag({ content: CLEAN_CSS });
    await page.evaluate(() => {
      const k = e => e.closest('.drawer,#drawer,aside');
      document.querySelectorAll('header,nav,.util,.mast,.topnav').forEach(e => { if (!k(e)) e.style.display = 'none'; });
      if (!document.getElementById('cap-header')) {
        const bar = document.createElement('div');
        bar.id = 'cap-header';
        bar.innerHTML = `<button><svg width=24 height=24 viewBox="0 0 24 24" fill=none stroke=currentColor stroke-width=2.2 stroke-linecap=round><path d="M3 6h18M3 12h18M3 18h18"/></svg></button><a class=cap-brand href="/">REN<span>TV</span></a><button><svg width=21 height=21 viewBox="0 0 24 24" fill=none stroke=currentColor stroke-width=2><circle cx=12 cy=8 r=3.6/><path d="M4.5 20a7.5 7.5 0 0 1 15 0" stroke-linecap=round/></svg></button>`;
        document.body.insertBefore(bar, document.body.firstChild);
      }
      window.scrollTo(0, 0);
    });
    await page.waitForTimeout(300);
  } catch (e) { /* clean page, skip */ }
}

async function shoot(context, name, route, { doNormalize = true, before = null } = {}) {
  const page = await context.newPage();
  let status = 'ERR';
  try {
    const resp = await page.goto(BASE + route, { waitUntil: 'domcontentloaded', timeout: 45000 });
    status = resp ? resp.status() : 'no-resp';
    await page.waitForTimeout(1200);
    if (before) await before(page);
    if (doNormalize) await normalize(page);
    await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: false });
    console.log(`OK  ${name}.png  <- ${route}  [${status}]`);
  } catch (e) {
    console.log(`FAIL ${name}.png <- ${route}  [${status}] ${e.message.split('\n')[0]}`);
    status = 'FAIL:' + status;
  } finally {
    await page.close();
  }
  return { name, route, status };
}

const results = [];

(async () => {
  const browser = await chromium.launch();
  const vp = { viewport: { width: 1440, height: 810 }, deviceScaleFactor: 2 };

  const userCtx = await browser.newContext({ ...vp, httpCredentials: USER });
  const adminCtx = await browser.newContext({ ...vp, httpCredentials: ADMIN });

  // ---- USER pages ----
  results.push(await shoot(userCtx, 'home', '/'));

  // article: grab first headline href from /news/
  {
    const page = await userCtx.newPage();
    let articleHref = null, status = 'ERR';
    try {
      const resp = await page.goto(BASE + '/news/', { waitUntil: 'domcontentloaded', timeout: 45000 });
      status = resp ? resp.status() : 'no-resp';
      await page.waitForTimeout(1500);
      articleHref = await page.evaluate(() => {
        const sel = ['a[href*="/news/"]', 'article a', '.headline a', 'h2 a', 'h3 a', '.story a', 'a[href*="/article"]'];
        for (const s of sel) {
          for (const a of document.querySelectorAll(s)) {
            const h = a.getAttribute('href') || '';
            if (h && !h.endsWith('/news/') && !h.endsWith('/news') && /news|article|story|\/\d/.test(h)) return h;
          }
        }
        const a = document.querySelector('main a[href], a[href]');
        return a ? a.getAttribute('href') : null;
      });
      console.log('article href ->', articleHref, '(news status ' + status + ')');
    } catch (e) { console.log('news list err', e.message); }
    await page.close();
    if (articleHref) {
      const full = articleHref.startsWith('http') ? articleHref.replace(BASE, '') : articleHref;
      results.push(await shoot(userCtx, 'article', full));
    } else {
      results.push(await shoot(userCtx, 'article', '/news/'));
    }
  }

  results.push(await shoot(userCtx, 'review', '/review'));
  results.push(await shoot(userCtx, 'markets', '/markets'));
  {
    const r = await shoot(userCtx, 'pulse', '/pulse');
    if (String(r.status).startsWith('4')) {
      const r2 = await shoot(userCtx, 'pulse', '/market-pulse.html');
      results.push(r2.status && !String(r2.status).startsWith('4') ? r2 : r);
    } else results.push(r);
  }
  results.push(await shoot(userCtx, 'cretalk', '/cre-talk'));
  results.push(await shoot(userCtx, 'blog', '/blog'));

  // ---- ADMIN pages ----
  results.push(await shoot(adminCtx, 'versions', '/versions'));
  results.push(await shoot(adminCtx, 'v-wire', '/versions/wire.html'));
  results.push(await shoot(adminCtx, 'v-broadsheet', '/versions/broadsheet.html'));
  results.push(await shoot(adminCtx, 'v-terminal', '/versions/terminal.html'));
  results.push(await shoot(adminCtx, 'v-magazine', '/versions/magazine.html'));
  results.push(await shoot(adminCtx, 'v-dashboard', '/versions/dashboard.html'));
  results.push(await shoot(adminCtx, 'admin-index', '/admin'));

  {
    const r = await shoot(adminCtx, 'admin-command', '/admin/command.html');
    if (String(r.status).startsWith('4')) { results.push(await shoot(adminCtx, 'admin-command', '/admin')); }
    else results.push(r);
  }
  {
    const r = await shoot(adminCtx, 'admin-kanban', '/admin/kanban.html');
    results.push(r);
  }
  results.push(await shoot(adminCtx, 'publish', '/admin'));
  results.push(await shoot(adminCtx, 'desk', '/desk'));
  results.push(await shoot(adminCtx, 'social', '/social'));

  // ---- THEME captures (user ctx, home page) ----
  async function themeShot(name, matchers) {
    const page = await userCtx.newPage();
    let status = 'ERR', picked = null;
    try {
      const resp = await page.goto(BASE + '/', { waitUntil: 'domcontentloaded', timeout: 45000 });
      status = resp ? resp.status() : 'no-resp';
      await page.waitForTimeout(1500);
      // try select#rentv-theme
      picked = await page.evaluate((matchers) => {
        const sel = document.querySelector('#rentv-theme') || document.querySelector('select[id*="theme" i]') || document.querySelector('select');
        if (sel && sel.options) {
          for (const opt of sel.options) {
            const t = (opt.textContent + ' ' + opt.value).toLowerCase();
            if (matchers.some(m => t.includes(m))) {
              sel.value = opt.value;
              sel.dispatchEvent(new Event('change', { bubbles: true }));
              return 'select:' + opt.value;
            }
          }
        }
        return null;
      }, matchers);
      if (!picked) {
        // try clickable theme chips/buttons
        picked = await page.evaluate((matchers) => {
          const els = [...document.querySelectorAll('[data-theme],.theme-chip,.theme-option,button,li,a')];
          for (const el of els) {
            const t = ((el.getAttribute('data-theme') || '') + ' ' + el.textContent).toLowerCase();
            if (matchers.some(m => t.includes(m))) { el.click(); return 'click:' + t.trim().slice(0, 30); }
          }
          return null;
        }, matchers);
      }
      await page.waitForTimeout(700);
      await normalize(page);
      await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: false });
      console.log(`OK  ${name}.png  theme pick=${picked} [${status}]`);
    } catch (e) { console.log(`FAIL ${name}.png ${e.message.split('\n')[0]}`); status = 'FAIL'; }
    await page.close();
    return { name, route: '/ (theme)', status: picked ? status : `${status} THEME-NOT-FOUND` };
  }
  results.push(await themeShot('theme-midnight', ['midnight', 'capital']));
  results.push(await themeShot('theme-salmon', ['salmon', 'f.t', 'ft ', 'financial']));

  await browser.close();

  console.log('\n===== SUMMARY =====');
  for (const r of results) console.log(`${r.status}\t${r.name}.png\t<- ${r.route}`);
  fs.writeFileSync('/Users/macstudio3/Projects/rentv-tour/recapture-results.json', JSON.stringify(results, null, 2));
})();