import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { AddressInfo } from 'node:net'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { CatalogDatabase } from '../src/database/store.js'; import { createWebApp } from '../src/web/app.js'; let dir = ''; beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'nlc-web-')); }); afterEach(async () => { await rm(dir, { force: true, recursive: true }); }); async function withServer(path: string, pathname: string): Promise { const app = createWebApp(path); const server = app.listen(0); try { const { port } = server.address() as AddressInfo; const response = await fetch(`http://127.0.0.1:${port}${pathname}`); return response.text(); } finally { server.close(); } } function fixtureDatabase(): string { const path = join(dir, 'catalog.sqlite'); const db = new CatalogDatabase(path); db.saveCatalogRun({ mode: 'test', newslettersProcessed: 2, linksExtracted: 3, sponsorCount: 1, deadLinkCount: 1, errors: 0, rows: [ { 'Issue Date': '2026-05-10', Category: 'SQLite', 'Link URL': 'https://sqlite.example/old', Title: 'Older SQLite Post', Description: 'An older database post', 'Page Title + Meta': '', 'Source Newsletter': 'DB Weekly', 'Also In': '' }, { 'Issue Date': '2026-05-17', Category: 'SQLite', 'Link URL': 'https://sqlite.example', Title: 'SQLite Post', Description: 'A local database post', 'Page Title + Meta': '', 'Source Newsletter': 'DB Weekly', 'Also In': '' }, { 'Issue Date': '2026-05-16', Category: 'JavaScript', 'Link URL': 'https://js.example', Title: 'JS Post', Description: 'A JavaScript post', 'Page Title + Meta': '', 'Source Newsletter': 'JS Weekly', 'Also In': '' } ], sponsors: [ { Newsletter: 'DB Weekly', Sponsor: 'Acme', Link: 'https://sponsor.example', Description: 'Blurb' } ], deadLinks: [ { URL: 'https://dead.example', Status: '404', Source: 'DB Weekly', Date: '2026-05-17' } ] }); db.close(); return path; } describe('web app', () => { it('provides newsletter summaries and latest/all issue link scopes', () => { const path = fixtureDatabase(); const db = new CatalogDatabase(path); try { const summaries = db.newsletterSummaries(); expect( summaries.map(({ name, latestIssueDate, issueCount, linkCount, sponsorCount }) => ({ name, latestIssueDate, issueCount, linkCount, sponsorCount })) ).toEqual([ { name: 'DB Weekly', latestIssueDate: '2026-05-17', issueCount: 2, linkCount: 2, sponsorCount: 1 }, { name: 'JS Weekly', latestIssueDate: '2026-05-16', issueCount: 1, linkCount: 1, sponsorCount: 0 } ]); const dbWeekly = summaries.find((newsletter) => newsletter.name === 'DB Weekly'); expect( db.newsletterLinks(dbWeekly?.id ?? 0, { scope: 'latest' }).map((link) => link.title) ).toEqual(['SQLite Post']); expect( db.newsletterLinks(dbWeekly?.id ?? 0, { scope: 'all' }).map((link) => link.title) ).toEqual(['SQLite Post', 'Older SQLite Post']); } finally { db.close(); } }); it('renders dashboard and catalog pages from SQLite', async () => { const path = fixtureDatabase(); await expect(withServer(path, '/')).resolves.toContain('Newsletter Link Catalog'); await expect(withServer(path, '/links')).resolves.toContain('SQLite Post'); await expect(withServer(path, '/links?q=JavaScript')).resolves.toContain('JS Post'); await expect(withServer(path, '/links?q=JavaScript')).resolves.not.toContain('SQLite Post'); await expect(withServer(path, '/sponsors')).resolves.toContain('Acme'); await expect(withServer(path, '/dead-links')).resolves.toContain('https://dead.example'); await expect(withServer(path, '/runs')).resolves.toContain('test'); }); it('renders the two-pane newsletter browser with latest issue by default and all issue mode', async () => { const path = fixtureDatabase(); const latest = await withServer(path, '/newsletters'); expect(latest).toContain('class="app-shell"'); expect(latest).toContain('Search newsletters'); expect(latest).toContain('aria-current="true"'); expect(latest).toContain('Latest Issue'); expect(latest).toContain('All Issues'); expect(latest).toContain('SQLite Post'); expect(latest).not.toContain('Older SQLite Post'); expect(latest).toContain('target="_blank"'); expect(latest).toContain('rel="noopener noreferrer"'); const allIssues = await withServer(path, '/newsletters?scope=all'); expect(allIssues).toContain('SQLite Post'); expect(allIssues).toContain('Older SQLite Post'); expect(allIssues).toContain('2026-05-10'); expect(allIssues).toContain('name="category"'); expect(allIssues).toContain('name="q"'); }); });