refactor: make generate-qr testable with injectable deps + entry guard

Extracts buildQrUrl and generateZoneQrs with injectable I/O deps and adds
tests/scripts/generate-qr.test.ts (20 tests). Refactor + tests originated
from a concurrent working session; this commit adds on top:
- import.meta entry guard — the unconditional top-level main() executed on
  test import and wrote (sometimes truncated) PNGs into public/qr
- typed vi.fn generics in makeDeps so strict tsc passes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
This commit is contained in:
2026-07-12 10:40:38 +08:00
co-authored by Claude Fable 5
parent 646c94be0c
commit 81282fbd8a
2 changed files with 248 additions and 15 deletions
+52 -15
View File
@@ -2,37 +2,74 @@
import QRCode from 'qrcode'
import { mkdirSync } from 'fs'
import path from 'path'
import { pathToFileURL } from 'url'
const BASE_URL = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'
export const DEFAULT_BASE_URL = 'http://localhost:3000'
// Matches seeded zone qr_code_token values in 20260709000008_seed.sql
const ZONES = [
export const ZONES = [
{ token: 'scw1-dock-a-qr-2026', name: 'SCW1 — Dock A' },
{ token: 'scw1-cold-storage-qr-2026', name: 'SCW1 — Cold Storage' },
{ token: 'scw1-loading-bay-qr-2026', name: 'SCW1 — Loading Bay' },
]
async function main() {
const outDir = path.join(process.cwd(), 'public', 'qr')
mkdirSync(outDir, { recursive: true })
export interface Zone {
token: string
name: string
}
for (const zone of ZONES) {
const url = `${BASE_URL}/report?zone=${zone.token}`
/** Pure helper: builds the URL encoded in each QR code. */
export function buildQrUrl(baseUrl: string, token: string): string {
return `${baseUrl}/report?zone=${token}`
}
export interface GenerateDeps {
toFile: (outputPath: string, url: string, opts: object) => Promise<void>
mkdirSync: (dir: string, opts: { recursive: boolean }) => void
log: (...args: unknown[]) => void
}
/** Core logic, with injectable deps so tests never touch real I/O. */
export async function generateZoneQrs(
zones: Zone[],
outDir: string,
baseUrl: string,
deps: GenerateDeps,
): Promise<void> {
deps.mkdirSync(outDir, { recursive: true })
for (const zone of zones) {
const url = buildQrUrl(baseUrl, zone.token)
const outputPath = path.join(outDir, `${zone.token}.png`)
await QRCode.toFile(outputPath, url, {
await deps.toFile(outputPath, url, {
width: 400,
margin: 2,
color: { dark: '#000000', light: '#FFFFFF' },
})
console.log(`${zone.name}`)
console.log(`${url}`)
console.log(`${outputPath}`)
deps.log(`${zone.name}`)
deps.log(`${url}`)
deps.log(`${outputPath}`)
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
async function main() {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_BASE_URL
const outDir = path.join(process.cwd(), 'public', 'qr')
await generateZoneQrs(ZONES, outDir, baseUrl, {
toFile: (outputPath, url, opts) => QRCode.toFile(outputPath, url, opts as Parameters<typeof QRCode.toFile>[2]),
mkdirSync,
log: console.log,
})
}
// Only run when executed directly (npm run generate-qr) — importing this
// module (e.g. from tests) must not touch the filesystem.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((err) => {
console.error(err)
process.exit(1)
})
}