// Unit tests for services/renderers/voiceDiagRenderer.js. Renderer // is pure — no I/O — so the tests just assert on the produced // markdown string. The interesting cases are: // // - severity buckets appear in the right order // - the OK bucket is hidden by default and shown under `detailed` // - the fixable-issues footer counts and lists only remediable // results (an OK check with a remediation still doesn't count, // since we filter status !== 'ok') // - the details block is only rendered under `detailed` // - empty result list yields a benign single-line message import test from 'node:test'; import assert from 'node:assert/strict'; import { renderVoiceDiagMarkdown } from '../services/renderers/voiceDiagRenderer.js'; const R = (id, status, message, remediation = null, details = null, label = null) => ({ id, label: label || `Check ${id}`, status, message, details, remediation, }); const REMEDIATION = { action: 'disable_dnd', title: 'Disable DND', summary: 'Turn DND off.', payload: {}, }; test('renderer: empty results → benign message', () => { const md = renderVoiceDiagMarkdown([], { storeNum: '12345' }); assert.match(md, /Voice Diagnostic - Store 12345/); assert.match(md, /No checks were executed/); }); test('renderer: header includes personLabel + email when provided', () => { const md = renderVoiceDiagMarkdown([], { storeNum: '12345', personLabel: 'Store 12345', email: 'ae12345@ae.com', }); assert.match(md, /user: Store 12345 — ae12345@ae\.com/); }); test('renderer: mixed severities render in error → warn → skipped order (default hides ok)', () => { const results = [ R('c1', 'ok', 'all good'), R('c2', 'warn', 'watch this', REMEDIATION), R('c3', 'error', 'boom'), R('c4', 'skipped', '403 missing scope'), R('c5', 'ok', 'also good'), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99' }); const orderIdx = ['**ERRORS**', '**WARNINGS**', '**SKIPPED**'].map((h) => md.indexOf(h)); assert.ok(orderIdx.every((i) => i > -1), `all severity headings present, got ${orderIdx}`); assert.ok(orderIdx[0] < orderIdx[1], 'ERRORS before WARNINGS'); assert.ok(orderIdx[1] < orderIdx[2], 'WARNINGS before SKIPPED'); assert.equal(md.includes('**OK**'), false, 'OK bucket hidden by default'); assert.match(md, /OK check\(s\) hidden — pass `detailed`/); }); test('renderer: summary line reports every bucket count', () => { const results = [ R('c1', 'ok', 'a'), R('c2', 'warn', 'b'), R('c3', 'warn', 'c'), R('c4', 'error', 'd'), R('c5', 'skipped', 'e'), R('c6', 'ok', 'f'), R('c7', 'ok', 'g'), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99' }); assert.match(md, /Errors \(1\) · Warnings \(2\) · Skipped \(1\) · OK \(3\)/); }); test('renderer: detailed mode surfaces OK bucket + details block', () => { const results = [ R('c1', 'ok', 'all good', null, { enabled: false, mwiEnabled: true }), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99', detailed: true }); assert.match(md, /\*\*OK\*\*/); assert.match(md, /enabled: false, mwiEnabled: true/); }); test('renderer: fixable footer counts non-ok results with a remediation only', () => { const results = [ R('c1', 'warn', 'w1', REMEDIATION), R('c2', 'error', 'e1', REMEDIATION), R('c3', 'ok', 'o1', REMEDIATION), // OK w/ remediation should NOT count R('c4', 'warn', 'w2'), // warn w/o remediation should NOT count ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99' }); assert.match(md, /Fixable issues \(2\)/); const footerLineCount = (md.match(/^- Check c[12]: Disable DND/gm) || []).length; assert.equal(footerLineCount, 2, 'footer lists exactly the fixable ones'); }); test('renderer: no fixable-footer emitted when nothing is fixable', () => { const results = [ R('c1', 'warn', 'no fix'), R('c2', 'error', 'no fix'), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99' }); assert.equal(md.includes('Fixable issues'), false); }); test('renderer: emitFooter=false suppresses trailing timestamp', () => { const md = renderVoiceDiagMarkdown([R('c1', 'ok', 'good')], { storeNum: '99', detailed: true, emitFooter: false, }); assert.equal(md.includes('Last checked'), false); }); test('renderer: emitFooter=true (default) adds an ISO timestamp line', () => { const md = renderVoiceDiagMarkdown([R('c1', 'ok', 'good')], { storeNum: '99', detailed: true, }); assert.match(md, /_Last checked: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); }); test('renderer: details values — arrays truncated past 3 items, nested objects JSON-ified', () => { const results = [ R('c1', 'warn', 'x', null, { long: ['a', 'b', 'c', 'd', 'e'], short: ['a', 'b'], nested: { foo: 'bar', n: 1 }, nada: null, }), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99', detailed: true }); assert.match(md, /long: \[a, b, c, …\+2\]/); assert.match(md, /short: \[a, b\]/); assert.match(md, /nested: \{"foo":"bar","n":1\}/); assert.match(md, /nada: —/); }); test('renderer: --detail off does not include details even when present', () => { const results = [ R('c1', 'warn', 'x', null, { foo: 'bar' }), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99' }); assert.equal(md.includes('foo: bar'), false); }); test('renderer: skipped bucket shown even without detail', () => { const results = [ R('c1', 'skipped', '403 missing scope'), ]; const md = renderVoiceDiagMarkdown(results, { storeNum: '99' }); assert.match(md, /\*\*SKIPPED\*\*/); assert.match(md, /403 missing scope/); });