← back to Rentv Tour

rentv-fullpage.mjs

195 lines

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

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

fs.mkdirSync(OUT, { recursive: true });

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(600);
  } catch (e) { /* clean page, skip */ }
}

// Scroll the whole page top->bottom->top with small waits so lazy content renders.
async function primeLazy(page) {
  try {
    await page.evaluate(async () => {
      const sleep = ms => new Promise(r => setTimeout(r, ms));
      const H = () => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
      const step = Math.max(400, Math.floor(window.innerHeight * 0.85));
      let y = 0;
      const total = H();
      while (y < total + step) { window.scrollTo(0, y); await sleep(120); y += step; }
      window.scrollTo(0, H()); await sleep(300);
      window.scrollTo(0, 0); await sleep(300);
    });
    // one more settle for any images kicked off by scroll
    await page.waitForTimeout(400);
  } catch (e) { /* ignore */ }
}

async function shoot(context, name, route, { doNormalize = true, before = null } = {}) {
  const page = await context.newPage();
  let status = 'ERR', height = 0;
  try {
    const resp = await page.goto(BASE + route, { waitUntil: 'domcontentloaded', timeout: 60000 });
    status = resp ? resp.status() : 'no-resp';
    await page.waitForTimeout(1500);
    if (before) await before(page);
    await primeLazy(page);
    try { await page.waitForLoadState('networkidle', { timeout: 8000 }); } catch (e) {}
    if (doNormalize) await normalize(page);
    // ensure at top before full-page grab
    await page.evaluate(() => window.scrollTo(0, 0));
    await page.waitForTimeout(300);
    await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: true });
    height = await page.evaluate(() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight));
    console.log(`OK  ${name}.png  <- ${route}  [${status}]  h=${height}px`);
  } 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, height };
}

const results = [];

(async () => {
  const browser = await chromium.launch();
  const vp = { viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 1 };

  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: open a REAL /news/<id> or /post/<id> — grab first headline href from HOME (/)
  {
    const page = await userCtx.newPage();
    let articleHref = null, status = 'ERR';
    try {
      const resp = await page.goto(BASE + '/', { waitUntil: 'domcontentloaded', timeout: 60000 });
      status = resp ? resp.status() : 'no-resp';
      await page.waitForTimeout(1800);
      articleHref = await page.evaluate(() => {
        const sel = ['a[href*="/news/"]', 'a[href*="/post/"]', 'a[href*="/article"]', 'a[href*="/story"]', '.headline a', 'h2 a', 'h3 a', '.card a', '.lead a'];
        for (const s of sel) {
          for (const a of document.querySelectorAll(s)) {
            const h = a.getAttribute('href') || '';
            if (/\/(news|post|article|story)\/[^/]+/.test(h)) return h;
          }
        }
        return null;
      });
      console.log('article href ->', articleHref, '(home status ' + status + ')');
    } catch (e) { console.log('home article-scan 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', '/article'));
    }
  }

  results.push(await shoot(userCtx, 'review', '/review'));
  results.push(await shoot(userCtx, 'markets', '/markets'));
  results.push(await shoot(userCtx, 'pulse', '/pulse'));
  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'));
  results.push(await shoot(adminCtx, 'admin-command', '/admin/command.html'));
  results.push(await shoot(adminCtx, 'admin-kanban', '/admin/kanban.html'));
  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). Set the theme.js CSS vars + body class directly. ----
  // From public/theme.js: Midnight Capital (idx 4), FT Salmon (idx 2).
  const THEME_MIDNIGHT = { accent:"#c9a84c", ink:"#f0f2f5", sub:"#8fa3b8", line:"#1e2d40", bg:"#0d1b2a", wash:"#132336", serif:"'DM Serif Display', Georgia, serif", sans:"'Archivo', system-ui, sans-serif", dark:true };
  const THEME_SALMON   = { accent:"#00538b", ink:"#1a1a1a", sub:"#5c5c5c", line:"#d4c9b8", bg:"#fff1e5", wash:"#fde8d1", serif:"'Source Serif 4', Georgia, serif", sans:"'Inter', system-ui, sans-serif", dark:false };

  async function applyTheme(page, t) {
    await page.evaluate((t) => {
      const r = document.documentElement;
      r.style.setProperty('--red', t.accent); r.style.setProperty('--ink', t.ink);
      r.style.setProperty('--sub', t.sub); r.style.setProperty('--line', t.line);
      r.style.setProperty('--bg', t.bg); r.style.setProperty('--wash', t.wash);
      r.style.setProperty('--serif', t.serif); r.style.setProperty('--sans', t.sans);
      document.body.classList.toggle('theme-dark', !!t.dark);
      // dark-surface overrides (mirrors theme.js injected style block)
      if (!document.getElementById('cap-theme-dark-fix')) {
        const st = document.createElement('style'); st.id = 'cap-theme-dark-fix';
        st.textContent =
          "body.theme-dark .util,body.theme-dark footer,body.theme-dark .ns,body.theme-dark .mast{background:var(--wash)!important}" +
          "body.theme-dark .util,body.theme-dark .util a,body.theme-dark footer,body.theme-dark footer a,body.theme-dark footer p{color:var(--sub)!important}" +
          "body.theme-dark footer .logo,body.theme-dark footer h5{color:var(--ink)!important}" +
          "body.theme-dark .ns{color:var(--ink)!important}body.theme-dark .ns p{color:var(--sub)!important}" +
          "body.theme-dark .card .img,body.theme-dark .lead .img{filter:brightness(.85)}";
        document.head.appendChild(st);
      }
    }, t);
    await page.waitForTimeout(700);
  }

  async function themeShot(name, theme) {
    const page = await userCtx.newPage();
    let status = 'ERR', height = 0;
    try {
      const resp = await page.goto(BASE + '/', { waitUntil: 'domcontentloaded', timeout: 60000 });
      status = resp ? resp.status() : 'no-resp';
      await page.waitForTimeout(1500);
      await applyTheme(page, theme);
      await primeLazy(page);
      // re-apply after scroll in case lazy content mounted un-themed nodes
      await applyTheme(page, theme);
      try { await page.waitForLoadState('networkidle', { timeout: 8000 }); } catch (e) {}
      await normalize(page);
      await page.evaluate(() => window.scrollTo(0, 0));
      await page.waitForTimeout(300);
      await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: true });
      height = await page.evaluate(() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight));
      console.log(`OK  ${name}.png  theme applied [${status}]  h=${height}px`);
    } catch (e) { console.log(`FAIL ${name}.png ${e.message.split('\n')[0]}`); status = 'FAIL'; }
    await page.close();
    return { name, route: '/ (theme)', status, height };
  }
  results.push(await themeShot('theme-midnight', THEME_MIDNIGHT));
  results.push(await themeShot('theme-salmon', THEME_SALMON));

  await browser.close();

  console.log('\n===== SUMMARY (name | route | status | height) =====');
  for (const r of results) console.log(`${String(r.status).padEnd(10)} h=${String(r.height).padStart(6)}px  ${r.name}.png\t<- ${r.route}`);
  fs.writeFileSync('/Users/macstudio3/Projects/rentv-tour/fullpage-results.json', JSON.stringify(results, null, 2));
})();