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-multihost.test.ts
T
xcsandClaude Fable 5 26db6c685b feat(w05): 按卡独立编辑 + 缺项点名并进顶条 + 区序换回 + 类型层叠图
2026-08-31 用户四连:
① 「各自编辑各自的部分,不要做联动」—— 平台新增 scopedTargets(ui-kit):开合作用域
  跟着触发钮走,三档 —— 钮在本地址宿主的行里→只这一行(A 卡的笔只开 A 卡);钮在别的
  宿主的行里→行内属于本地址的宿主(这张卡的卡背);外部闸行→全宿主(老行为,② 的闸
  不受影响)。收值与保存不随作用域收窄(collect 仍按地址全量,「只写真改了的行」在
  服务端把关)。编辑态/锁面/笔盘显隐 CSS 全部改成按卡(.dossier.on);同地址同时只开
  一张卡(笔在别卡编辑中自动隐去,快照键按地址,双开会互相踩)。2 测 + 变异验证。
② 逐块「还差 N 行必填」红带撤下,点名并进顶部「还差什么」—— 同一批基座读数
  (_missing/_missing_keys)在顶条出两行子行,数值 0 自己消失;gate_reason 改口。
③ 比对基准与芯片档案区序换回(基准在前)。
④ 比对基准换「层叠双封装」SVG(后面那枚淡出成剪影 —— 一类而非一颗),不再与芯片卡
  共用同一枚封装图。

测:ui-kit 35(scoped 2 新,变异红过)+ 包内 105 + 2528 全仓;2.11.0 装库五探针核过
(合并条/无旧红带/类型图/按卡 CSS/基准在前);浏览器实测 XL 卡独自翻背进编辑、LM 卡纹丝不动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-31 17:01:06 +08:00

130 lines
7.2 KiB
TypeScript

/**
* **多宿主收值 + 嵌套异地址排除**(2026-08-31,W05 芯片卡翻面逼出的平台扩面)。
*
* 版面要求「参数值印在每张芯片卡的**背面**、就地可改」:参数表的行天然分居两张卡,
* 一个宿主装不下 —— `hostsOf` 把同一地址的全部宿主都数进来,每个宿主各自归行。
*
* 更要紧的是**嵌套异地址排除**:卡背的参数宿主(`data-edit="parts.parameter"`)住在
* 芯片卡宿主(`data-edit="parts.profile"`)的行里 —— 外层收值若把里层宿主的格子也
* 收走,参数值就会被当成主数据的列并进主数据的行(`view-container-collect-nested.test.ts`
* 钉过的静默丢数据,换一副面孔回来)。规则:**格子只归它最近的 `data-edit` 祖先**。
* 嵌套的 `data-rows` 不带 `data-edit` 时行为原样(照旧并行 —— 那两条钉测不动)。
*/
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
import { collect, mirrorTwins, scopedTargets, setOpenIn } from '../src/ViewContainer.tsx';
/** 两张芯片卡:正面是主数据格(profile),背面各住一个参数宿主(parameter) */
const FLIP_CARDS =
'<article data-edit="parts.profile">' +
'<section class="dossier">' +
'<button class="pen-a" data-edit-open="parts.profile"></button>' +
'<div class="front"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="brand_name" value="TI"/></div>' +
'<div class="back"><section class="pback" data-edit="parts.parameter">' +
'<div class="ptile"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_name" value="输出电压"/><input data-col="param_value" value="5.0"/></div>' +
'<div class="ptile"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_name" value="输出电流"/><input data-col="param_value" value="3"/></div>' +
'</section></div>' +
'</section>' +
'<section class="dossier">' +
'<div class="front"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="brand_name" value="XLSEMI"/></div>' +
'<div class="back"><section class="pback" data-edit="parts.parameter">' +
'<div class="ptile"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="param_name" value="输出电压"/><input data-col="param_value" value="5.0"/></div>' +
'<div class="ptile"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="param_name" value="输出电流"/><input data-col="param_value" value="2.5"/></div>' +
'</section></div>' +
'</section>' +
'</article>' +
'<div class="jc-editgate" data-edit-for="parts.parameter"></div>';
function docOf(html: string): Document {
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = html;
return doc;
}
describe('多宿主 collect():同一地址的宿主都数进来,格子只归最近的 data-edit 祖先', () => {
it('parts.parameter 分居两张卡背的两个宿主 —— 4 个瓷贴收到 4 行,主键各自带对', () => {
const got = collect(docOf(FLIP_CARDS), 'parts.parameter');
expect(got).toHaveLength(4);
const xl = got.filter((r) => r['internal_pn'] === 'XL2596S-5.0');
expect(xl).toHaveLength(2);
expect(new Map(xl.map((r) => [r['param_name'], r['param_value']])).get('输出电流')).toBe('2.5');
});
it('parts.profile 收值**不含**卡背参数宿主的格子 —— 两行主数据,一列参数都不许混进来', () => {
const got = collect(docOf(FLIP_CARDS), 'parts.profile');
expect(got).toHaveLength(2);
for (const row of got) {
expect(row['brand_name']).toBeDefined();
/* 静默丢数据的那副面孔:param_value 若出现在主数据行里,就是里层宿主被外层吞了 */
expect(row['param_value']).toBeUndefined();
expect(row['param_name']).toBeUndefined();
}
});
it('setOpenIn:一把闸开合同一地址的**全部**宿主(非表格容器,.on 落在宿主自身)', () => {
const doc = docOf(FLIP_CARDS);
setOpenIn(doc, 'parts.parameter', true);
const hosts = Array.from(doc.querySelectorAll('.pback'));
expect(hosts).toHaveLength(2);
expect(hosts.every((h) => h.classList.contains('on'))).toBe(true);
expect(doc.querySelector('[data-edit-for="parts.parameter"]')?.classList.contains('on')).toBe(true);
/* 外层 profile 宿主不许被顺手点亮 —— 两把闸各管各的 */
expect(doc.querySelector('[data-edit="parts.profile"]')?.classList.contains('on')).toBe(false);
setOpenIn(doc, 'parts.parameter', false);
expect(hosts.every((h) => !h.classList.contains('on'))).toBe(true);
});
});
describe('mirrorTwins:下拉镜像的是选项文案,不是 value(2026-08-31「保存一瞬间变数字」)', () => {
const SEL =
'<div data-edit="parts.profile"><div class="row">' +
'<span class="jc-vcell tsel">旧名</span>' +
'<select class="jc-cell" data-col="chip_type_id">' +
'<option value=""></option><option value="1">DC-DC 降压稳压器</option><option value="2">LDO 线性稳压器</option>' +
'</select>' +
'</div></div>';
it('镜进孪生件的是「DC-DC 降压稳压器」,不是「1」;selected 属性拨到人选的那一项(守卫 CSS 认它)', () => {
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = SEL;
const sel = doc.querySelector('select') as HTMLSelectElement;
sel.value = '1';
mirrorTwins(doc, 'parts.profile');
expect(doc.querySelector('.tsel')?.textContent).toBe('DC-DC 降压稳压器');
expect(doc.querySelector('option[value="1"]')?.hasAttribute('selected')).toBe(true);
expect(doc.querySelector('option[value="2"]')?.hasAttribute('selected')).toBe(false);
});
it('没选(空占位)= 没答:孪生件一个字不动', () => {
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = SEL;
mirrorTwins(doc, 'parts.profile');
expect(doc.querySelector('.tsel')?.textContent).toBe('旧名');
});
});
describe('scopedTargets:开合作用域跟着触发钮走(2026-08-31「不要做联动,各自编辑各自的部分」)', () => {
it('钮在本地址宿主的行里 → 只这一行(A 卡的笔只开 A 卡);钮对别的地址 → 只这张卡里那个宿主', () => {
const doc = docOf(FLIP_CARDS);
const pen = doc.querySelector('.pen-a') as HTMLElement;
// ① profile:钮在 profile 宿主里 → 落到它所在的那一行(第一张卡的 section)
const t1 = scopedTargets(doc, 'parts.profile', pen);
expect(t1).toHaveLength(1);
expect(t1[0]?.className).toBe('dossier');
expect(t1[0]?.contains(pen)).toBe(true);
// ② parameter:钮不在任何 parameter 宿主里,但在 profile 的行里 → 行内那一个 pback,不是两个
const t2 = scopedTargets(doc, 'parts.parameter', pen);
expect(t2).toHaveLength(1);
expect(t2[0]?.classList.contains('pback')).toBe(true);
expect(t1[0]?.contains(t2[0] ?? null)).toBe(true);
});
it('外部闸(不在任何宿主/行里)→ 全部宿主,老行为', () => {
const doc = docOf(FLIP_CARDS);
const gate = doc.querySelector('[data-edit-for="parts.parameter"]') as HTMLElement;
const t = scopedTargets(doc, 'parts.parameter', gate);
expect(t).toHaveLength(2);
expect(t.every((h) => h.classList.contains('pback'))).toBe(true);
});
});