import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import type { Server } from 'bun'; import { createPlugin } from './plugins'; // --- Synthetic ICS --- const pad = (n: number) => n.toString().padStart(2, '0'); const icsDate = (d: Date) => `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}T${pad(d.getHours())}${pad(d.getMinutes())}00`; const now = new Date(); const currentStart = new Date(now.getTime() - 30 * 60_000); const currentEnd = new Date(now.getTime() + 30 * 60_000); const nextStart = new Date(now.getTime() + 60 * 60_000); const nextEnd = new Date(now.getTime() + 120 * 60_000); const laterStart = new Date(now.getTime() + 180 * 60_000); const laterEnd = new Date(now.getTime() + 240 * 60_000); const ics = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT DTSTART:${icsDate(currentStart)} DTEND:${icsDate(currentEnd)} SUMMARY:Current Meeting UID:test-current@test END:VEVENT BEGIN:VEVENT DTSTART:${icsDate(nextStart)} DTEND:${icsDate(nextEnd)} SUMMARY:Next Meeting UID:test-next@test END:VEVENT BEGIN:VEVENT DTSTART:${icsDate(laterStart)} DTEND:${icsDate(laterEnd)} SUMMARY:Later Meeting UID:test-later@test END:VEVENT END:VCALENDAR`; const fakeModel = { width: 800, height: 480, css: { classes: { device: 'device', size: 'size', density: 'density' } }, } as any; // --- ICS server --- let icsServer: Server; let icsUrl: string; beforeAll(() => { icsServer = Bun.serve({ port: 0, hostname: '127.0.0.1', routes: { '/cal.ics': () => new Response(ics, { headers: { 'Content-Type': 'text/calendar' }, }), }, }); icsUrl = `http://127.0.0.1:${icsServer.port}/cal.ics`; }); afterAll(() => { icsServer.stop(); }); // --- Tests --- describe('plugin registry', () => { test('unknown plugin throws', () => { expect(() => createPlugin('nonexistent', {})).toThrow( 'Unknown plugin: nonexistent' ); }); test('calendar plugin creates an ICSRenderable', () => { const plugin = createPlugin('calendar', { urls: [] }); expect(plugin.hash).toBe(''); expect(plugin.nextEvent).toBeUndefined(); }); }); describe('calendar plugin rendering', () => { test('renders events from ICS feed', async () => { const plugin = createPlugin('calendar', { urls: [icsUrl] }); const html = await plugin.render('full', fakeModel); expect(html).toContain('Current Meeting'); expect(html).toContain('Next Meeting'); expect(html).toContain('Later Meeting'); }); test('shows BEZET when there is a current event', async () => { const plugin = createPlugin('calendar', { urls: [icsUrl] }); const html = await plugin.render('full', fakeModel); expect(html).toContain('BEZET'); }); test('computes hash after render', async () => { const plugin = createPlugin('calendar', { urls: [icsUrl] }); await plugin.render('full', fakeModel); expect(plugin.hash).toBeTruthy(); }); test('sets nextEvent to upcoming event start', async () => { const plugin = createPlugin('calendar', { urls: [icsUrl] }); await plugin.render('full', fakeModel); expect(plugin.nextEvent).toBeInstanceOf(Date); const diff = Math.abs(plugin.nextEvent!.getTime() - nextStart.getTime()); expect(diff).toBeLessThan(120_000); }); test('renders VRIJ with no events', async () => { const plugin = createPlugin('calendar', { urls: [] }); const html = await plugin.render('full', fakeModel); expect(html).toContain('VRIJ'); expect(html).not.toContain('BEZET'); }); }); // --- Today plugin test data --- import type { TodayData } from './todayview'; const SAMPLE_TODAY: TodayData = { focus: 'Ship the Q2 retrospective deck — clarity over completeness.', success: 'Three deep-work blocks. One real conversation. Inbox under 10.', agenda: [ { start: '08:30', end: '09:00', title: 'Morning planning', where: 'Office', kind: 'solo' }, { start: '09:30', end: '10:15', title: 'Design review', where: 'Meet', kind: 'call' }, { start: '11:00', end: '12:00', title: '1:1 with Priya', where: 'Walk', kind: 'in-person' }, { start: '13:30', end: '15:00', title: 'Deep work — deck', where: 'Library', kind: 'focus block' }, { start: '16:00', end: '16:30', title: 'Eng standup', where: 'Zoom', kind: 'call' }, ], chores: [ { text: 'Make the bed' }, { text: '20 min reading' }, { text: 'Water the plants' }, { text: 'Walk · 30 min' }, { text: 'Journal · 3 lines' }, { text: 'Lights out by 23:00' }, ], todos_due: [ { text: 'Review Atlas spec comments', tag: 'Atlas', est: '30m', done: false }, { text: 'Draft retro deck — sections 1–3', tag: 'Deck', est: '1h', done: false }, { text: 'Reply to Marcus re: contract', tag: 'Ops', est: '10m', done: false }, { text: 'Submit June expenses', tag: 'Admin', est: '15m', done: false }, ], todos_overdue: [ { text: 'File Q1 receipts', tag: 'Admin', age: '3d' }, { text: 'Renew domain · seva.dev', tag: 'Ops', age: '1d' }, { text: 'Book dentist', tag: 'Health', age: '9d' }, ], }; describe('today plugin', () => { test('renders all sections', async () => { const plugin = createPlugin('today', SAMPLE_TODAY); const html = await plugin.render('full', fakeModel); // Top bar expect(html).toContain('FOCUS'); expect(html).toContain('SUCCESS'); expect(html).toContain('retrospective deck'); // Agenda expect(html).toContain('Agenda'); expect(html).toContain('Morning planning'); expect(html).toContain('Eng standup'); // Chores expect(html).toContain('Chores'); expect(html).toContain('Make the bed'); expect(html).toContain('Water the plants'); // Tasks expect(html).toContain('Due today'); expect(html).toContain('Overdue'); expect(html).toContain('Review Atlas spec comments'); expect(html).toContain('File Q1 receipts'); }); test('renders with minimal data', async () => { const plugin = createPlugin('today', { focus: 'Custom focus text', success: 'Custom success text', agenda: [ { start: '09:00', end: '10:00', title: 'Custom Event', where: 'Room', kind: 'call' }, ], chores: [{ text: 'Custom chore' }], todos_due: [{ text: 'Custom task', tag: 'Test', est: '5m', done: false }], todos_overdue: [], }); const html = await plugin.render('full', fakeModel); expect(html).toContain('Custom focus text'); expect(html).toContain('Custom Event'); expect(html).toContain('Custom chore'); expect(html).toContain('Custom task'); }); test('computes hash after render', async () => { const plugin = createPlugin('today', SAMPLE_TODAY); await plugin.render('full', fakeModel); expect(plugin.hash).toBeTruthy(); }); test('renders today date', async () => { const plugin = createPlugin('today', SAMPLE_TODAY); const html = await plugin.render('full', fakeModel); const dayStr = String(new Date().getDate()).padStart(2, '0'); expect(html).toContain(dayStr); }); });