diff --git a/src/adapters/default/discover.test.ts b/src/adapters/default/discover.test.ts index 16ad50ab..5ca3da30 100644 --- a/src/adapters/default/discover.test.ts +++ b/src/adapters/default/discover.test.ts @@ -46,7 +46,8 @@ describe('discoverDefault', () => { document.querySelector('#root').innerHTML = '
ResourcesContact
'; + 'Resume' + + 'ResourcesContactGuide'; `); }); await new Promise((resolve) => server!.listen(0, '127.0.0.1', resolve)); @@ -76,7 +77,7 @@ describe('discoverDefault', () => { } response.setHeader('content-type', 'text/html'); response.end(`Example - +
Aviso legalThree
`); }); await new Promise((resolve) => server!.listen(0, '127.0.0.1', resolve)); @@ -95,5 +96,6 @@ describe('discoverDefault', () => { `${origin}/five`, `${origin}/aviso-legal`, ]); + expect(inventory.navigation).toContainEqual({ text: 'Resume', href: `${origin}/resume.pdf` }); }); }); diff --git a/src/adapters/default/discover.ts b/src/adapters/default/discover.ts index 9121d5e1..e2832b5c 100644 --- a/src/adapters/default/discover.ts +++ b/src/adapters/default/discover.ts @@ -1,4 +1,4 @@ -import { fetchSitemapWithDiagnostics, classifyUrl, extractSameOriginLinks, routeKey } from '../../lib/extraction/sitemap.js'; +import { fetchSitemapWithDiagnostics, classifyUrl, extractSameOriginLinks, resolvePageLink, routeKey } from '../../lib/extraction/sitemap.js'; import { extractMeta, extractTitle, extractNavLinks } from '../../lib/html-extract/index.js'; import { sourceContextOptions, getPlaywright } from '../../lib/browser-kit/browser-kit.js'; import type { InventoryUrl } from '../shared.js'; @@ -78,13 +78,12 @@ export async function discoverDefault(url: string, _opts: Record { const href = $(el).attr('href')?.trim(); if (!href || href.startsWith('#')) return; - const resolved = resolveAndFilter(href, baseUrl, baseOrigin); + const resolved = resolvePageLink(href, baseUrl, baseOrigin); if (resolved && !seen.has(resolved)) { seen.add(resolved); urls.push(resolved); @@ -363,7 +363,7 @@ async function crawlRenderedNavLinks(baseUrl: string, baseOrigin: string): Promi ); const seen = new Set(); return hrefs.flatMap((href) => { - const resolved = resolveAndFilter(href, baseUrl, baseOrigin); + const resolved = resolvePageLink(href, baseUrl, baseOrigin); if (!resolved || seen.has(resolved)) return []; seen.add(resolved); return [resolved]; @@ -376,7 +376,7 @@ async function crawlRenderedNavLinks(baseUrl: string, baseOrigin: string): Promi } } -function resolveAndFilter(href: string, baseUrl: string, baseOrigin: string): string | null { +export function resolvePageLink(href: string, baseUrl: string, baseOrigin: string): string | null { try { const resolved = new URL(href, baseUrl); if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') return null; diff --git a/src/lib/screenshot/resource-capture.test.ts b/src/lib/screenshot/resource-capture.test.ts index 78374858..a0264c79 100644 --- a/src/lib/screenshot/resource-capture.test.ts +++ b/src/lib/screenshot/resource-capture.test.ts @@ -68,15 +68,18 @@ describe( 'CapturedResourceStore', () => { expect( manifest.icons[ 0 ].src ).toBe( identity.logo.url ); } ); - it( 'exports a linked DOCX as portable bytes beside a missing HTML route', async () => { + it.each( [ + [ 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'PK\x03\x04word document fixture' ], + [ 'PDF', 'application/pdf', '%PDF-1.7\npdf document fixture' ], + ] )( 'exports a linked %s as portable bytes beside a missing HTML route', async ( extension, contentType, content ) => { const outputDir = mkdtempSync( join( tmpdir(), 'dla-linked-docx-' ) ); dirs.push( outputDir ); mkdirSync( join( outputDir, 'html' ) ); mkdirSync( join( outputDir, 'screenshots' ) ); const sourceUrl = 'https://example.com/'; - const download = 'https://example.com/_files/ugd/flyer.docx?dn=Fly%20fishing.docx'; + const download = `https://example.com/_files/ugd/flyer.${ extension }?dn=Fly%20fishing.${ extension }`; const missing = 'https://example.com/missing-page'; - const bytes = Buffer.from( 'PK\x03\x04word document fixture' ); + const bytes = Buffer.from( content ); const html = `FlyerMissing`; writeFileSync( join( outputDir, 'html', 'home.html' ), html ); writeFileSync( join( outputDir, 'screenshots', 'manifest.json' ), JSON.stringify( { @@ -84,7 +87,7 @@ describe( 'CapturedResourceStore', () => { } ) ); const fetchMedia = vi.fn( async ( url: string ) => ( { finalUrl: url, status: 200, - headers: new Headers( { 'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' } ), + headers: new Headers( { 'content-type': contentType } ), body: bytes, } ) ); const store = new CapturedResourceStore( outputDir, sourceUrl, fetchMedia ); diff --git a/src/lib/screenshot/resource-capture.ts b/src/lib/screenshot/resource-capture.ts index 270fc68b..ad3f148b 100644 --- a/src/lib/screenshot/resource-capture.ts +++ b/src/lib/screenshot/resource-capture.ts @@ -305,11 +305,11 @@ export function isAudioLink( reference: string, documentUrl: string ): boolean { } } -/** Linked Word files are downloads, not navigable HTML routes. */ +/** Linked documents are downloads, not navigable HTML routes. */ export function isDocumentDownloadLink( reference: string, documentUrl: string ): boolean { try { const url = new URL( reference.replace( /&/g, '&' ), documentUrl ); - return /^https?:$/.test( url.protocol ) && /\.docx?$/i.test( url.pathname ); + return /^https?:$/.test( url.protocol ) && /\.(?:pdf|docx?)$/i.test( url.pathname ); } catch { return false; } @@ -662,7 +662,7 @@ export class CapturedResourceStore { if ( fetched.body.length === 0 && contentType !== 'text/css' ) throw new Error( 'render dependency response body is empty' ); if ( - ! /^(?:text\/css|image\/|audio\/|video\/|font\/|application\/(?:json|manifest\+json|font|x-font|font-woff|octet-stream|msword|vnd\.openxmlformats-officedocument\.wordprocessingml\.document))/i.test( + ! /^(?:text\/css|image\/|audio\/|video\/|font\/|application\/(?:json|manifest\+json|font|x-font|font-woff|octet-stream|pdf|msword|vnd\.openxmlformats-officedocument\.wordprocessingml\.document))/i.test( contentType ) )