Archived
汇报流:活动按步序位置推视图,容器沙箱渲染
「flow:按**步序位置**推送声明的模版」(AM 07)。原先这条链只有写面没有读面,
活动的 viewTemplates 是一行占位字符串,工作台视图区永远只显示一份硬编码概览。
补齐整条链:
· 步声明新增 `view`:这一步呈现哪张模版 —— 推什么由声明给定,不由临场决定。
· 新增模版填数的纯函数(interaction/domain):`data-slot` 取值、`data-rows` 逐条展开。
**填数只做取值与转义**,没有表达式、没有分支、没有循环 —— 一旦模版能算,
它就成了代码,容器的「无脚本执行权」也就形同虚设。填充**消费槽位**,
交出去的是干净 HTML,不是带模板属性的半成品。
· interaction 补读面 `viewOf`;desk 路由把推送的 HTML 交给前端;
ViewRegion 经 `@ui` 的 ViewContainer(sandbox="" 的 iframe)渲染 —— feature 不碰 innerHTML。
· A1 写了三张真模版(事态全景 / 打标判据 / 成果预览),内联自带样式
(沙箱页面不继承工作台 CSS),按步序位置挂在 S3 / S5 / S6 上。
沿路两处修:
① 取值路径遇到**本身带点号的键**(台账地址 `域.对象`)会被拆成两级、永远取不到。
改为整键优先、找不到再下行;`view-render` 与 `step-sequence` 两处同修。
② 「本单交付」不能画成台账全表:任务视图是正式版 + 我的草稿的合并结果,
直接拿来列会把**别单**放行过的记录一并列出。本单自己写下的行单独放在
`mine:域.对象` 下(整键,取值不歧义)。
另:视图推送失败原先静默吞掉 —— 会让「视图区一直空着」变成查不出原因的怪事;
改为留痕(仍不影响步序推进,呈现是投影)。
实测:一封 RFQ 入站 → 放行前那一屏列出本单交付 LM358DR / TI / 12000 / 8.2 / 10周,
以及 Type A「直接快答」的分类结论与四项打标判据。
gates: tsc / eslint / depcruise(321 模块)/ api-report / 423 测试 全绿
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/** 视图模版填数 —— 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 = '<b data-slot="x"></b>';
|
||||
const out = renderView(html, { x: '<img src=x onerror=alert(1)>' });
|
||||
// 标签没能成型:尖括号全被转义,留下的是惰性文本
|
||||
expect(out).not.toContain('<img');
|
||||
expect(out).toBe('<b><img src=x onerror=alert(1)></b>');
|
||||
});
|
||||
|
||||
it('净化门查的是**作者写的模版**,不是填完数据的成品', () => {
|
||||
// 模版本身必须无脚本、无事件处理器、无外链
|
||||
expect(templateIsSafe('<b data-slot="x"></b>')).toBe(true);
|
||||
expect(templateIsSafe('<b onclick="x()"></b>')).toBe(false);
|
||||
// 成品里出现「onerror=」这串字面文本是转义后的惰性文本,不该再拿模版规则去卡它 ——
|
||||
// 否则任何正文提到事件处理器的业务数据都会被误伤。安全由「模版过门 + 沙箱容器」两道保证。
|
||||
expect(renderView('<b data-slot="x"></b>', { x: 'onclick=1' })).toBe('<b>onclick=1</b>');
|
||||
});
|
||||
|
||||
it('未命中的槽位填「—」,不把占位文字漏到成品上', () => {
|
||||
expect(renderView('<b data-slot="missing">占位</b>', {})).toBe('<b>—</b>');
|
||||
});
|
||||
|
||||
it('填充消费槽位 —— 交出去的是干净 HTML,不带模板属性', () => {
|
||||
expect(renderView('<b class="k" data-slot="x"></b>', { x: 1 })).toBe('<b class="k">1</b>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('行模版 —— 逐条清单不写死条数', () => {
|
||||
const html = '<table data-rows="records"><tr><td data-slot="pn"></td><td data-slot="eau"></td></tr></table>';
|
||||
|
||||
it('按数组逐项复制,行内槽位相对该行取值', () => {
|
||||
const out = renderView(html, {
|
||||
records: [
|
||||
{ pn: 'A1', eau: 100 },
|
||||
{ pn: 'B2', eau: 200 },
|
||||
],
|
||||
});
|
||||
expect(out).toContain('<td>A1</td>');
|
||||
expect(out).toContain('<td>B2</td>');
|
||||
expect(out).toContain('<td>200</td>');
|
||||
});
|
||||
|
||||
it('空数组渲染成空容器,不留一行占位', () => {
|
||||
const out = renderView(html, { records: [] });
|
||||
expect(out).toBe('<table></table>');
|
||||
});
|
||||
|
||||
it('行先展开再填标量槽 —— 顺序反了行内会被外层作用域填掉', () => {
|
||||
const out = renderView('<ul data-rows="rs"><li data-slot="v"></li></ul><b data-slot="v"></b>', {
|
||||
rs: [{ v: '行内' }],
|
||||
v: '外层',
|
||||
});
|
||||
expect(out).toContain('<li>行内</li>');
|
||||
expect(out).toContain('<b>外层</b>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('转义', () => {
|
||||
it('四个字符各归其位', () => {
|
||||
expect(escapeHtml('<a href="x">&</a>')).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('<tbody data-rows="quote_intake.record"><tr><td data-slot="pn"></td></tr></tbody>', scope);
|
||||
expect(out).toContain('<td>LM358DR</td>');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user