import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { extractFonts, isChallengePage, prefetchFromHtml, previewablePrefetchHtml, } from '../src/brands/prefetch.js'; function tmpBrandDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'od-brand-html-')); } // An ≥80-char header SVG so extractInlineHeaderSvg registers it as a logo, // which keeps the harvest fully offline (no favicon-service fallback fetch). const HEADER_SVG = ''; describe('brand prefetch artifacts', () => { it('replaces head-only truncated captures with a visible diagnostic page', () => { const html = `Big SSRreal page`; const preview = previewablePrefetchHtml(html, 64); expect(preview).toContain(''); expect(preview).toContain('Prefetch HTML was truncated before the page body.'); expect(preview).toContain('Big SSR'); expect(preview).not.toContain('real page'); }); it('keeps capped captures when the body is present before the cap', () => { const html = `Ready${'x'.repeat(80)}`; const preview = previewablePrefetchHtml(html, 72); expect(preview).toBe(html.slice(0, 72)); expect(preview).toContain(''); }); }); describe('prefetchFromHtml (extract from already-rendered DOM)', () => { it('does not let icon fonts outrank the real body/display face', () => { const css = [ '.ri{font-family:"Remix Icon"}', '.icon{font-family:"Remix Icon"}', '.button .glyph{font-family:"Remix Icon"}', 'body{font-family:"Albert Sans",system-ui,sans-serif}', 'h1{font-family:"Albert Sans",system-ui,sans-serif}', ].join(''); const { fonts, fontFaceFamilies } = extractFonts(css); expect(fonts[0]?.family).toBe('Albert Sans'); expect(fonts.some((font) => font.family === 'Remix Icon')).toBe(false); expect(fontFaceFamilies.some((font) => font === 'Remix Icon')).toBe(false); }); it('harvests colors, fonts, and copy from provided HTML + CSS without fetching the page', async () => { const html = [ '', 'Acme Inc', '', '', `
${HEADER_SVG}
`, '

Welcome to Acme

Fast, friendly software

', `

${'Acme builds delightful developer tools teams enjoy using every day. '.repeat(2)}

`, '', ].join(''); const css = [ ':root{--brand:#3b5bdb}', 'h1{color:#3b5bdb;font-family:"Inter",sans-serif}', 'body{background:#ffffff;color:#1f2933}', 'a{color:#e8590c}.accent{color:#0ca678}.cta{background:#3b5bdb}', ].join(''); const result = await prefetchFromHtml(html, css, 'https://acme.test/', tmpBrandDir()); expect(result).not.toBeNull(); expect(result?.blocked).toBe(false); expect(result?.thin).toBe(false); expect(result?.title).toContain('Acme'); expect(result?.description).toContain('developer tools'); expect(result?.colors.length ?? 0).toBeGreaterThan(0); expect(result?.fonts.some((f) => /inter/i.test(f.family))).toBe(true); expect((result?.headings ?? []).join(' ')).toContain('Welcome to Acme'); }); it('flags an anti-bot challenge page as blocked and thin', async () => { const html = 'Just a moment...' + '

Checking your browser

'; const result = await prefetchFromHtml(html, '', 'https://walled.test/', tmpBrandDir()); expect(result).not.toBeNull(); expect(result?.blocked).toBe(true); expect(result?.thin).toBe(true); }); it('harvests a content-rich post-wall page that still embeds a Turnstile widget', async () => { // The exact symptom: the user clears the Cloudflare wall in the in-app // browser, the real Economist homepage renders — and that real page STILL // references challenges.cloudflare.com (a Turnstile widget on its // login/subscribe surface). The harvest must treat it as the real site, not // throw the whole capture away as if it were the wall. const nav = [ 'World', 'United States', 'China', 'Business', 'Finance', 'Europe', 'Asia', 'Culture', 'Science', ] .map((label) => `${label}`) .join(''); const html = [ '', 'The Economist', '', '', '', `
${HEADER_SVG}${nav}
`, '

World in brief

Shipping halted on the Strait of Hormuz

Our cover

', `

${'The Economist offers authoritative insight on international news. '.repeat(2)}

`, '
', '', ].join(''); const css = [ ':root{--brand:#e3120b}', 'h1{color:#e3120b;font-family:"Milo",Georgia,serif}', 'body{background:#ffffff;color:#121212}', 'a{color:#0d6efd}.accent{color:#0ca678}.cta{background:#e3120b}', ].join(''); const result = await prefetchFromHtml(html, css, 'https://www.economist.com/', tmpBrandDir()); expect(result).not.toBeNull(); expect(result?.blocked).toBe(false); expect(result?.thin).toBe(false); expect(result?.title).toContain('Economist'); expect((result?.colors.length ?? 0)).toBeGreaterThan(0); expect((result?.headings ?? []).join(' ')).toContain('World in brief'); }); it('returns null for empty HTML', async () => { expect(await prefetchFromHtml('', '', 'https://x.test/', tmpBrandDir())).toBeNull(); }); }); describe('isChallengePage (interstitial vs. real page that embeds a widget)', () => { const richNav = Array.from( { length: 10 }, (_, i) => `Section ${i}`, ).join(''); it('flags a sparse Cloudflare interstitial by its title and copy', () => { const html = 'Just a moment...' + '

Verifying you are human.

' + '

This website uses a security service to protect against malicious bots.

' + 'CloudflarePrivacy' + ''; expect(isChallengePage(html)).toBe(true); }); it('flags a sparse page whose only signal is an embedded Turnstile widget', () => { const html = 'Checking' + '
' + '' + ''; expect(isChallengePage(html)).toBe(true); }); it('does NOT flag a content-rich page that merely embeds a Turnstile widget', () => { const html = 'The Economist' + '' + `
${richNav}
` + '

World in brief

Today\'s top stories

Our cover

' + '
' + ''; expect(isChallengePage(html)).toBe(false); }); it('does NOT flag a content-rich page that embeds a DataDome tag', () => { const html = 'Shop' + '' + `
${richNav}
` + '

New arrivals

Best sellers

On sale

' + ''; expect(isChallengePage(html)).toBe(false); }); it('still flags a definitive challenge marker even on an otherwise rich page', () => { const html = 'Site' + `
${richNav}

a

b

c

` + '' + ''; expect(isChallengePage(html)).toBe(true); }); });