← 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));
})();