test(w05): 数据工单嵌套 data-rows 探针 —— 钉死 collect() 的并行行为,平坦形状是唯一能用的路

W05「核心参数值」想做成外层按候选料分组(<section>)、内层铺参数瓷贴的嵌套 data-rows 版面。
用真实渲染管线(renderView)产出 DOM 后喂给真实 collect() 实测:rowOf() 爬到 host 的直接
子元素时只能爬到候选料的 <section>(瓷贴与 section 之间还隔着内层 data-rows 留下的一层
<div>),于是一颗料的多个参数瓷贴被并成一条记录、后写的列覆盖先写的,悄悄丢数据 ——
证实预读判断(嵌套 = 并行),用两条断言(记录数 2 非 6、input_voltage/output_voltage 两条
参数在输出里完全消失)钉死,并用四条变异验证(候选数变 3→段数跟 / 摘一整段→记录数跟 /
摘主键 data-col→字段消失)确认断言真的在读结构。

同时钉平坦退路形状(瓷贴直接是 data-edit 宿主的子元素,data-rows 不分料铺):collect()
按瓷贴数返回记录、互不覆盖,行容器换成 <div>/<li> 均成立(现有测试全是 <tr>,没人钉过
非表格行容器)。

renderView(apps/api,tsconfig.node.json)与 collect()(packages/ui-kit,tsconfig.web.json:
jsx react-jsx + DOM lib)不能进同一个测试文件——depcruise 放行,但 typecheck 真红
(TS6142: ViewContainer.tsx 用了 JSX,tsconfig.node.json 没设 --jsx),两侧拆开、
中间用同一段渲染输出的 HTML 字符串接力(两处注释互相点名,防副本漂移)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-30 20:22:38 +08:00
co-authored by Claude Opus 5
parent 4076621e04
commit e38631224a
3 changed files with 295 additions and 0 deletions
@@ -0,0 +1,99 @@
/**
* **可编辑区域套两层 `data-rows` 时,渲染器真产出什么** —— W05 数据工单版面探针
* (2026-08-30,「核心参数值」想做成"一个候选一个 section、section 里铺参数瓷贴"这形状,
* 先问渲染管线真产出什么,再问 `collect()` 拿它怎么办)。
*
* `collect()` 住在 `packages/ui-kit`,这份文件住在 `apps/api`——`ui-kit` 是 tsconfig.web.json
* 那一支(`jsx: react-jsx` + DOM lib),`apps/api` 归 tsconfig.node.json(两者都没有对方要的
* 编译选项);`packages-are-leaves` 也不许 `packages/*` 反向 import `apps/*`。两侧各钉一条、
* 中间用**同一段渲染出来的 HTML 字符串**接力,防止两份夹具各写各的、悄悄漂移。
*
* **这一条只钉 `renderView` 的真实输出**——collect() 拿这段 HTML 怎么归行,
* 见 `packages/ui-kit/__tests__/view-container-collect-nested.test.ts`
* (把下面 `NESTED_RENDERED` 这段字符串原样复制过去当夹具)。
*/
import { describe, expect, it } from 'vitest';
import { renderView } from '../internal/domain/view-render.ts';
/**
* 外层 `data-edit="parts.parameter"` 与外层 `data-rows="mine:sub_eval.part"` 共处一个标签
* (照仓里既有的可编辑表格惯例,如 `<tbody data-rows="..." data-edit="...">`)——
* 每颗候选料复制一个 `<section>`;section 里再嵌一层 `data-rows="mine:parts.parameter"
* data-by="internal_pn=internal_pn"` 铺这颗料自己的参数瓷贴,每瓷贴三格
* (`param_key` / `param_value` / 主键 `internal_pn`)。
*
* 行容器标签特意避开重名(`article` 外层、`div` 内层、瓷贴用 `span`)——
* `data-rows` 的正则按标签名非贪婪找**同名**闭合标签,内层若也叫 `article`
* 会被外层第一个提前撞见的闭合标签截断(`view-render.test.ts` 379 行那条注释记过同一个坑)。
*/
const NESTED_TPL =
'<article data-edit="parts.parameter" data-rows="mine:sub_eval.part">' +
'<section>' +
'<h3 data-slot="internal_pn"></h3>' +
'<div data-rows="mine:parts.parameter" data-by="internal_pn=internal_pn">' +
'<span class="pv">' +
'<input data-col="param_key" data-slot="param_key" data-value/>' +
'<input data-col="param_value" data-slot="param_value" data-value/>' +
'<input data-col="internal_pn" data-slot="internal_pn" data-value/>' +
'</span>' +
'</div>' +
'</section>' +
'</article>';
const NESTED_SCOPE = {
'mine:sub_eval.part': [{ internal_pn: 'LM2596S-5.0' }, { internal_pn: 'XL2596S-5.0' }],
'mine:parts.parameter': [
{ internal_pn: 'LM2596S-5.0', param_key: 'input_voltage', param_value: '4.5~40' },
{ internal_pn: 'LM2596S-5.0', param_key: 'output_voltage', param_value: '5' },
{ internal_pn: 'LM2596S-5.0', param_key: 'enable_threshold', param_value: '2.5' },
{ internal_pn: 'XL2596S-5.0', param_key: 'input_voltage', param_value: '4.5~40' },
{ internal_pn: 'XL2596S-5.0', param_key: 'output_voltage', param_value: '5' },
{ internal_pn: 'XL2596S-5.0', param_key: 'enable_threshold', param_value: '3.0' },
],
};
/**
* `renderView` 真实产出的逐字 HTML —— 与下面第一条用例的断言必须**逐字节一致**。
* 这段字符串原样复制进 `view-container-collect-nested.test.ts` 当夹具(注释互相点名)。
*/
export const NESTED_RENDERED =
'<article data-edit="parts.parameter"><section><h3>LM2596S-5.0</h3><div>' +
'<span class="pv"><input data-col="param_key" value="input_voltage"/>' +
'<input data-col="param_value" value="4.5~40"/>' +
'<input data-col="internal_pn" value="LM2596S-5.0"/></span>' +
'<span class="pv"><input data-col="param_key" value="output_voltage"/>' +
'<input data-col="param_value" value="5"/>' +
'<input data-col="internal_pn" value="LM2596S-5.0"/></span>' +
'<span class="pv"><input data-col="param_key" value="enable_threshold"/>' +
'<input data-col="param_value" value="2.5"/>' +
'<input data-col="internal_pn" value="LM2596S-5.0"/></span>' +
'</div></section><section><h3>XL2596S-5.0</h3><div>' +
'<span class="pv"><input data-col="param_key" value="input_voltage"/>' +
'<input data-col="param_value" value="4.5~40"/>' +
'<input data-col="internal_pn" value="XL2596S-5.0"/></span>' +
'<span class="pv"><input data-col="param_key" value="output_voltage"/>' +
'<input data-col="param_value" value="5"/>' +
'<input data-col="internal_pn" value="XL2596S-5.0"/></span>' +
'<span class="pv"><input data-col="param_key" value="enable_threshold"/>' +
'<input data-col="param_value" value="3.0"/>' +
'<input data-col="internal_pn" value="XL2596S-5.0"/></span>' +
'</div></section></article>';
describe('renderView:外层 data-edit+data-rows 共处一个标签,内层 data-rows+data-by 铺瓷贴', () => {
it('产出两个 <section>(host = <article> 的直接子元素),各自嵌一层 <div> 才到瓷贴 —— 逐字节核对', () => {
const out = renderView(NESTED_TPL, NESTED_SCOPE);
expect(out).toBe(NESTED_RENDERED);
});
it('变异验证:换成 3 颗候选料,输出的 <section> 数跟着变成 3 —— 证明上面的断言真的在读结构,不是凑巧写对了字符串', () => {
const out = renderView(NESTED_TPL, {
'mine:sub_eval.part': [{ internal_pn: 'A' }, { internal_pn: 'B' }, { internal_pn: 'C' }],
'mine:parts.parameter': [
{ internal_pn: 'A', param_key: 'k1', param_value: 'v1' },
{ internal_pn: 'B', param_key: 'k1', param_value: 'v1' },
{ internal_pn: 'C', param_key: 'k1', param_value: 'v1' },
],
});
expect(out.split('<section>')).toHaveLength(4); // split 出 [头, s1, s2, s3]
});
});