import { describe, it, expect, vi, beforeEach } from 'vitest' import path from 'path' import { buildQrUrl, generateZoneQrs, ZONES, DEFAULT_BASE_URL, type Zone, type GenerateDeps, } from '@/scripts/generate-qr' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeDeps() { return { toFile: vi.fn().mockResolvedValue(undefined), mkdirSync: vi.fn(), log: vi.fn(), } } const OUT_DIR = '/fake/public/qr' // --------------------------------------------------------------------------- // buildQrUrl — pure function // --------------------------------------------------------------------------- describe('buildQrUrl', () => { it('combines baseUrl and token into a /report?zone= URL', () => { expect(buildQrUrl('https://example.com', 'scw1-dock-a-qr-2026')).toBe( 'https://example.com/report?zone=scw1-dock-a-qr-2026', ) }) it('works with localhost base URL', () => { expect(buildQrUrl('http://localhost:3000', 'my-token')).toBe( 'http://localhost:3000/report?zone=my-token', ) }) it('does not add a double slash between baseUrl and /report', () => { const url = buildQrUrl('https://ims.example.com', 'token-xyz') // Strip the protocol so the only // would be an accidental duplicate slash expect(url.replace('https://', '')).not.toContain('//') }) it('handles token with hyphens and digits', () => { const token = 'scw1-cold-storage-qr-2026' expect(buildQrUrl('https://example.com', token)).toContain(token) }) }) // --------------------------------------------------------------------------- // ZONES constant — shape invariants // --------------------------------------------------------------------------- describe('ZONES', () => { it('contains exactly 3 zones', () => { expect(ZONES).toHaveLength(3) }) it('every zone has a non-empty token and name', () => { for (const zone of ZONES) { expect(zone.token).toBeTruthy() expect(zone.name).toBeTruthy() } }) it('tokens match the seeded qr_code_token values from the migration', () => { const tokens = ZONES.map((z) => z.token) expect(tokens).toContain('scw1-dock-a-qr-2026') expect(tokens).toContain('scw1-cold-storage-qr-2026') expect(tokens).toContain('scw1-loading-bay-qr-2026') }) it('all tokens are unique', () => { const tokens = ZONES.map((z) => z.token) expect(new Set(tokens).size).toBe(tokens.length) }) }) // --------------------------------------------------------------------------- // DEFAULT_BASE_URL // --------------------------------------------------------------------------- describe('DEFAULT_BASE_URL', () => { it('falls back to localhost:3000', () => { expect(DEFAULT_BASE_URL).toBe('http://localhost:3000') }) }) // --------------------------------------------------------------------------- // generateZoneQrs — I/O behaviour // --------------------------------------------------------------------------- describe('generateZoneQrs', () => { let deps: ReturnType beforeEach(() => { deps = makeDeps() }) it('creates the output directory with recursive: true', async () => { await generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps) expect(deps.mkdirSync).toHaveBeenCalledWith(OUT_DIR, { recursive: true }) }) it('calls toFile once per zone', async () => { await generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps) expect(deps.toFile).toHaveBeenCalledTimes(ZONES.length) }) it('passes the correct URL to toFile for each zone', async () => { await generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps) for (const zone of ZONES) { const expectedUrl = buildQrUrl(DEFAULT_BASE_URL, zone.token) expect(deps.toFile).toHaveBeenCalledWith( expect.any(String), expectedUrl, expect.any(Object), ) } }) it('writes each PNG to /.png', async () => { await generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps) for (const zone of ZONES) { const expectedPath = path.join(OUT_DIR, `${zone.token}.png`) expect(deps.toFile).toHaveBeenCalledWith( expectedPath, expect.any(String), expect.any(Object), ) } }) it('passes the expected QRCode options (width, margin, color)', async () => { await generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps) const [, , opts] = deps.toFile.mock.calls[0] expect(opts).toMatchObject({ width: 400, margin: 2, color: { dark: '#000000', light: '#FFFFFF' }, }) }) it('uses the supplied baseUrl instead of the default', async () => { const customBase = 'https://ims.setiacorp.com' await generateZoneQrs(ZONES, OUT_DIR, customBase, deps) const firstUrl: string = deps.toFile.mock.calls[0][1] expect(firstUrl.startsWith(customBase)).toBe(true) }) it('logs three lines per zone (check-mark, url, path)', async () => { await generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps) expect(deps.log).toHaveBeenCalledTimes(ZONES.length * 3) }) it('handles a single-zone list correctly', async () => { const single: Zone[] = [{ token: 'test-zone-token', name: 'Test Zone' }] await generateZoneQrs(single, OUT_DIR, DEFAULT_BASE_URL, deps) expect(deps.toFile).toHaveBeenCalledTimes(1) expect(deps.toFile).toHaveBeenCalledWith( path.join(OUT_DIR, 'test-zone-token.png'), `${DEFAULT_BASE_URL}/report?zone=test-zone-token`, expect.any(Object), ) }) it('handles an empty zone list without errors', async () => { await generateZoneQrs([], OUT_DIR, DEFAULT_BASE_URL, deps) expect(deps.toFile).not.toHaveBeenCalled() expect(deps.log).not.toHaveBeenCalled() // mkdir is still called to ensure the directory exists expect(deps.mkdirSync).toHaveBeenCalledOnce() }) it('propagates errors thrown by toFile', async () => { const boom = new Error('disk full') deps.toFile.mockRejectedValueOnce(boom) await expect( generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps), ).rejects.toThrow('disk full') }) it('propagates errors thrown by mkdirSync', async () => { deps.mkdirSync.mockImplementationOnce(() => { throw new Error('permission denied') }) await expect( generateZoneQrs(ZONES, OUT_DIR, DEFAULT_BASE_URL, deps), ).rejects.toThrow('permission denied') }) })