This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/packages/ui-kit/__tests__/view-container-collect-div-rows.test.ts
T
xcsandClaude Opus 5 e38631224a 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
2026-08-30 20:22:38 +08:00

107 lines
7.0 KiB
TypeScript

/**
* **平坦退路形状:瓷贴是 `data-edit` 宿主的直接子元素,行容器是 `<div>` 不是 `<tr>`**
* (2026-08-30,W05 数据工单版面探针)。
*
* 对照 `view-container-collect-nested.test.ts` 那条「外层按料分组、内层铺瓷贴」的嵌套形状——
* 那种形状里瓷贴与 host 之间隔了两层(候选料的 `<section>` + 内层 `data-rows` 留下的 `<div>`),
* `rowOf()` 爬到 host 直接子元素时只能到 `<section>`,把一颗料的 3 个瓷贴全部并成一条记录、
* 后写的列覆盖先写的,悄悄丢数据。
*
* 这里是**能用的退路**:`data-rows="mine:parts.parameter"` 直接铺在 host(`data-edit` 那个元素)
* 身上,不分料、不套内层 —— 瓷贴(`<div class="pv">`)因此是 host 的**直接子元素**,`rowOf()`
* 第一步往上爬就撞到 host,一个瓷贴就是一行,互不干扰。W05 若要「一个候选一个卡片」的排版,
* 换个思路做(每颗候选各给一张单独的 `data-edit` 宿主,或纯展示的分组包着这一层可编辑的平铺表),
* 不能靠嵌套 `data-rows` 省一层。
*
* `rowOf()`(`../src/ViewContainer.tsx:178`)按 `parentElement` 一路爬,不认标签名 ——
* 现有 `view-container-collect.test.ts` 的用例全是 `<tr>` 行(表格),这里换成 `<div class="pv">`
* 的瓷贴卡,证明它不是"认表格"这条窄路,是通用的"host 的直接子元素就是行"。
*/
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
import { collect } from '../src/ViewContainer.tsx';
/** 两颗候选料、每颗 3 个参数瓷贴(与嵌套形状那条用同一组事实数据,方便对照) */
const TILES_HOST =
'<div data-edit="parts.parameter">' +
'<div class="pv"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="input_voltage"/><input data-col="param_value" value="4.5~40"/></div>' +
'<div class="pv"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="output_voltage"/><input data-col="param_value" value="5"/></div>' +
'<div class="pv"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="enable_threshold"/><input data-col="param_value" value="2.5"/></div>' +
'<div class="pv"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="param_key" value="input_voltage"/><input data-col="param_value" value="4.5~40"/></div>' +
'<div class="pv"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="param_key" value="output_voltage"/><input data-col="param_value" value="5"/></div>' +
'<div class="pv"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="param_key" value="enable_threshold"/><input data-col="param_value" value="3.0"/></div>' +
'</div>';
function docOf(html: string): Document {
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = html;
return doc;
}
describe('collect():瓷贴(<div class="pv">)是 data-edit 宿主的直接子元素 —— 一瓷贴一记录,互不覆盖', () => {
it('6 个瓷贴收到 6 条记录(不是并成 2 条候选级记录)', () => {
const got = collect(docOf(TILES_HOST), 'parts.parameter');
expect(got).toHaveLength(6);
});
it('每条记录三格各自带对:主键 internal_pn + param_key + param_value,不互相覆盖', () => {
const got = collect(docOf(TILES_HOST), 'parts.parameter');
// 同一颗料的 3 条参数各自独立成行,3 个键值全部保留(与嵌套形状那条"只剩最后一瓷贴"相反)
const lm = got.filter((r) => r['internal_pn'] === 'LM2596S-5.0');
expect(lm).toHaveLength(3);
const byKey = new Map(lm.map((r) => [r['param_key'], r['param_value']]));
expect(byKey.get('input_voltage')).toBe('4.5~40');
expect(byKey.get('output_voltage')).toBe('5');
expect(byKey.get('enable_threshold')).toBe('2.5');
const xl = got.filter((r) => r['internal_pn'] === 'XL2596S-5.0');
expect(xl).toHaveLength(3);
expect(new Map(xl.map((r) => [r['param_key'], r['param_value']])).get('enable_threshold')).toBe('3.0');
});
/*
* 行容器换一个跟表格毫无关系的标签(`<li>`)结果不变 —— 证明 `rowOf` 认的是
* "host 的直接子元素",不是专挑某个标签名。
*
* 特意不拿 `<tr>` 来比:裸 `<tr>`(不包在 `<table><tbody>` 里)会撞上 HTML 解析算法的
* "foster parenting"(表格内容规则规定 `<tr>` 只能直接装 `<td>/<th>`,`<input>` 这类
* 不合规内容会被解析器整体挪到 `<tr>` 外面)——真拿裸 tr 包 input 试过,jsdom 吐出的是
* 9 条(3 个空 `<tr>` + 6 个被挪出来的 `<input>` 各自成行),这是 HTML 解析的固有行为,
* 与 `rowOf` 是否"认标签"无关,拿来当反例只会误导。已有的 `view-container-collect.test.ts`
* 用 `<table><tbody data-edit>` 包着的 `<tr>` 早就证明过 tr 这条路是对的。
*/
it('行容器换成完全不相干的标签(<li>)结果不变 —— 证明 rowOf 认的是"host 的直接子元素",不是专挑某个标签名', () => {
const liHost =
'<div data-edit="parts.parameter">' +
'<li class="pv"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="input_voltage"/><input data-col="param_value" value="4.5~40"/></li>' +
'<li class="pv"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="output_voltage"/><input data-col="param_value" value="5"/></li>' +
'<li class="pv"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="enable_threshold"/><input data-col="param_value" value="2.5"/></li>' +
'</div>';
const got = collect(docOf(liHost), 'parts.parameter');
expect(got).toHaveLength(3);
expect(new Set(got.map((r) => r['param_key']))).toEqual(new Set(['input_voltage', 'output_voltage', 'enable_threshold']));
});
it('变异验证:去掉一个瓷贴整行,记录数跟着从 6 变成 5 —— 证明「6 条」不是巧合,是按瓷贴数记', () => {
const oneLess = TILES_HOST.replace(
'<div class="pv"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="param_key" value="enable_threshold"/><input data-col="param_value" value="3.0"/></div>',
'',
);
const got = collect(docOf(oneLess), 'parts.parameter');
expect(got).toHaveLength(5);
});
it('变异验证:摘掉一个瓷贴的主键 data-col(internal_pn)—— 只有那一条记录缺这一列,其余 5 条不受影响', () => {
const withoutOnePk = TILES_HOST.replace(
'<input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_key" value="enable_threshold"/>',
'<input value="LM2596S-5.0"/><input data-col="param_key" value="enable_threshold"/>',
);
const got = collect(docOf(withoutOnePk), 'parts.parameter');
expect(got).toHaveLength(6);
const missingPk = got.filter((r) => !('internal_pn' in r));
expect(missingPk).toHaveLength(1);
expect(missingPk[0]?.['param_key']).toBe('enable_threshold');
expect(missingPk[0]?.['param_value']).toBe('2.5');
});
});