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