/** 视图模版填数 —— domain 测试零 mock(AM 07:模版是内容不是代码) */ import { describe, expect, it } from 'vitest'; import { displayValue, escapeHtml, renderView, resolveSlot } from '../internal/domain/view-render.ts'; import { templateIsSafe } from '../internal/domain/control-handover.ts'; describe('取值与展示', () => { it('点号下行 + 数组下标 —— 与步声明的写法一致', () => { const scope = { records: [{ pn: 'LM358DR' }], n: 3 }; expect(resolveSlot(scope, 'records.0.pn')).toBe('LM358DR'); expect(resolveSlot(scope, 'n')).toBe(3); expect(resolveSlot(scope, 'nope.deep')).toBeUndefined(); }); it('空值显示为「—」,免得画面留空洞', () => { expect(displayValue(undefined)).toBe('—'); expect(displayValue('')).toBe('—'); expect(displayValue(null)).toBe('—'); }); it('数组显示条数,布尔显示是/否,小数收敛', () => { expect(displayValue([1, 2, 3])).toBe('3'); expect(displayValue(true)).toBe('是'); expect(displayValue(0.123456789)).toBe('0.1235'); }); }); describe('填数只做取值与转义 —— 模版不得成为代码', () => { it('填入的值被转义,注不进标签', () => { const html = ''; const out = renderView(html, { x: '' }); // 标签没能成型:尖括号全被转义,留下的是惰性文本 expect(out).not.toContain('<img src=x onerror=alert(1)>'); }); it('净化门查的是**作者写的模版**,不是填完数据的成品', () => { // 模版本身必须无脚本、无事件处理器、无外链 expect(templateIsSafe('')).toBe(true); expect(templateIsSafe('')).toBe(false); // 成品里出现「onerror=」这串字面文本是转义后的惰性文本,不该再拿模版规则去卡它 —— // 否则任何正文提到事件处理器的业务数据都会被误伤。安全由「模版过门 + 沙箱容器」两道保证。 expect(renderView('', { x: 'onclick=1' })).toBe('onclick=1'); }); it('未命中的槽位填「—」,不把占位文字漏到成品上', () => { expect(renderView('占位', {})).toBe('—'); }); it('填充消费槽位 —— 交出去的是干净 HTML,不带模板属性', () => { expect(renderView('', { x: 1 })).toBe('1'); }); }); describe('行模版 —— 逐条清单不写死条数', () => { const html = '
'; it('按数组逐项复制,行内槽位相对该行取值', () => { const out = renderView(html, { records: [ { pn: 'A1', eau: 100 }, { pn: 'B2', eau: 200 }, ], }); expect(out).toContain('A1'); expect(out).toContain('B2'); expect(out).toContain('200'); }); it('空数组渲染成空容器,不留一行占位', () => { const out = renderView(html, { records: [] }); expect(out).toBe('
'); }); it('行先展开再填标量槽 —— 顺序反了行内会被外层作用域填掉', () => { const out = renderView('', { rs: [{ v: '行内' }], v: '外层', }); expect(out).toContain('
  • 行内
  • '); expect(out).toContain('外层'); }); }); describe('转义', () => { it('四个字符各归其位', () => { expect(escapeHtml('&')).toBe('<a href="x">&</a>'); }); }); describe('整键优先 —— 台账地址本身带点号', () => { const scope = { 'quote_intake.record': [{ pn: 'LM358DR', eau: 12000 }], quote_intake: { record: [{ pn: '不该取到这个' }] }, a: { b: 'nested' }, }; it('字面整键存在时取它,不拆成两级', () => { const rows = resolveSlot(scope, 'quote_intake.record'); expect(Array.isArray(rows) && (rows[0] as { pn: string }).pn).toBe('LM358DR'); }); it('没有整键才按点号下行', () => { expect(resolveSlot(scope, 'a.b')).toBe('nested'); }); it('行模版用地址键取表', () => { const out = renderView('', scope); expect(out).toContain('LM358DR'); }); });