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