Files
Newsletter-Link-Catalog/tests/web.test.ts

166 lines
5.2 KiB
TypeScript

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<string> {
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"');
});
});