/**
|
* 探针:可自定列的检验表(GroupedQualityTable.columns / headerSpans)与可配字段的样品信息
|
* (SampleInfo.fields)。
|
*
|
* 只读,不改代码、不碰数据库。用法:npx tsx .qc-conformance/verify-columns.ts
|
*
|
* 守三件事:
|
* 1. **属性缺席时产物逐字不变**——这是本轮的第一优先级:存量和已发布模板一个字节都不该动。
|
* 这里比对的是整段产物字符串的 sha256,而不是「列数还是 7」这种弱断言:
|
* 列数不变但 style 顺序变了、属性搬家了,一样会让已发布报告重出时变样。
|
* 2. 属性在场时列数、列序、合并属性落点、两级表头跨列都对得上;
|
* 3. 写错时 validate 报得出、且报的是能改的那一处(不是笼统一句「格式错误」)。
|
*
|
* 直接打在真实 buildContent / validate 的产物上:结构一改这里就红,不必等浏览器。
|
*/
|
import type { QualityProps } from '../src/components/quality/core/types';
|
|
import { createHash } from 'node:crypto';
|
|
import { resolveQualityProps, validateQualityProps } from '../src/components/quality/core/validator';
|
import { buildQualityComponentCatalog } from '../src/components/quality/core/catalog';
|
import { parseColumnSpec } from '../src/components/quality/core/column-spec';
|
import { sampleInfoDefinition } from '../src/components/quality/header/sample-info';
|
import { groupedQualityTableDefinition } from '../src/components/quality/inspection/grouped-quality-table';
|
import { registerAllQualityComponents } from '../src/components/quality';
|
|
const failures: string[] = [];
|
|
function expect(label: string, actual: unknown, wanted: unknown): void {
|
const ok = actual === wanted;
|
if (!ok) {
|
failures.push(`${label}:期望 ${JSON.stringify(wanted)},实际 ${JSON.stringify(actual)}`);
|
}
|
// eslint-disable-next-line no-console
|
console.log(`${ok ? ' ok ' : ' FAIL '} ${label} = ${JSON.stringify(actual)}`);
|
}
|
|
function expectTrue(label: string, actual: boolean, detail?: string): void {
|
if (!actual) {
|
failures.push(`${label}${detail ? `:${detail}` : ''}`);
|
}
|
// eslint-disable-next-line no-console
|
console.log(`${actual ? ' ok ' : ' FAIL '} ${label}`);
|
}
|
|
function sha(text: string): string {
|
return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 16);
|
}
|
|
/** 真实 buildContent 的产物(组件产出的是 HTML 字符串) */
|
function build(
|
definition: typeof groupedQualityTableDefinition | typeof sampleInfoDefinition,
|
props?: QualityProps,
|
): string {
|
const resolved = resolveQualityProps(definition, props);
|
return String(definition.buildContent(resolved).components);
|
}
|
|
function validate(
|
definition: typeof groupedQualityTableDefinition | typeof sampleInfoDefinition,
|
props?: QualityProps,
|
): string[] {
|
return validateQualityProps(definition, props);
|
}
|
|
/** 表头格数:`<th` 会把 `<thead` 也算进去,所以数闭合标签 */
|
function headCellCount(html: string): number {
|
return (html.match(/<\/th>/g) ?? []).length;
|
}
|
|
function theadRowCount(html: string): number {
|
const thead = html.slice(html.indexOf('<thead'), html.indexOf('</thead>'));
|
return (thead.match(/<tr>/g) ?? []).length;
|
}
|
|
const MERGE_ATTRS = ' rowspan="{{item.group.span}}" hidden="{{item.group.hidden}}"';
|
|
// ------------------------------------------------- 1. 缺席 = 逐字不变(第一优先级)
|
|
// 改这轮之前的产物指纹。列数、style 顺序、合并属性落点,任何一处动了这个 hash 都会变。
|
// 之所以冻结 hash 而不是冻结整段 HTML:整段太长,diff 时看不出改的是哪一格。
|
//
|
// 这两个值不是「照着现在的实现抄一遍」,而是把改前的实现(从工作区里还原出来的旧版本)
|
// 与改后的实现跑同一批属性、逐字比对之后取下来的:12 组属性组合(默认 / title 空 /
|
// 各显示开关单独与组合关闭 / 换数据源路径 / 换标题 / 每行 1、3 组 / 关边框)全部 same。
|
const GROUPED_TABLE_BASELINE = '8e803b882a62ba70';
|
const SAMPLE_INFO_BASELINE = 'a07da23e176df130';
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 1. 两个新属性缺席:产物必须与改这轮之前逐字一致 =====');
|
{
|
// 默认 props(defaults 里刻意不放 columns / headerSpans / fields)
|
const table = build(groupedQualityTableDefinition);
|
const sample = build(sampleInfoDefinition);
|
expect('分组表:defaults 里没有 columns', 'columns' in groupedQualityTableDefinition.defaults, false);
|
expect('分组表:defaults 里没有 headerSpans', 'headerSpans' in groupedQualityTableDefinition.defaults, false);
|
expect('样品信息:defaults 里没有 fields', 'fields' in sampleInfoDefinition.defaults, false);
|
expect('分组表默认产物指纹', sha(table), GROUPED_TABLE_BASELINE);
|
expect('样品信息默认产物指纹', sha(sample), SAMPLE_INFO_BASELINE);
|
|
// 属性即使显式传成空串也必须回落:属性面板清空一格是常事,
|
// 空串若被当成「0 列」就会渲染出一张没有列的残表
|
expect('分组表:columns 传空串仍出默认 7 列', headCellCount(build(groupedQualityTableDefinition, { columns: '' })), 7);
|
expect('分组表:headerSpans 传空串仍出单层表头', theadRowCount(build(groupedQualityTableDefinition, { headerSpans: '' })), 1);
|
expect('样品信息:fields 传空串仍出默认 6 项', (build(sampleInfoDefinition, { fields: '' }).match(/<td/g) ?? []).length, 12);
|
|
// 默认列的合并属性仍只挂在「检验项目」那一格上
|
const cells = table.split('<td');
|
expectTrue('默认列含「子项」列', table.includes('>子项</th>'));
|
expectTrue(
|
'默认列:带 rowspan 的格只有 1 个',
|
cells.filter((cell) => cell.includes('rowspan=')).length === 1,
|
);
|
expectTrue('默认列:合并属性在「检验项目」格上', Boolean(cells.find((cell) => cell.includes('{{item.itemName}}'))?.includes(MERGE_ATTRS)));
|
}
|
|
// ------------------------------------------------- 2. columns 在场:列数、列序、合并落点
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 2. columns 在场:整表按它来,显示开关不再生效 =====');
|
{
|
const custom =
|
'序号={{index}}|#检验项目={{item.itemName}}|子项={{item.group.childName}}|' +
|
'检测方法={{item.checkMethod}}|检测要求={{item.requirement}}|实测值={{item.actualValue}}|' +
|
'单位={{item.unit}}|判定={{item.resultText}}';
|
const html = build(groupedQualityTableDefinition, { columns: custom });
|
|
expect('表头 8 列', headCellCount(html), 8);
|
expect('行模板 8 格', (html.match(/<td/g) ?? []).length, 8);
|
expectTrue('新增的「检测方法」列表头在场', html.includes('>检测方法</th>'));
|
expectTrue('「检测方法」列取 item.checkMethod', html.includes('{{item.checkMethod}}'));
|
|
// 列序 = 串里的顺序:检测方法必须夹在「子项」与「检测要求」之间
|
const heads = [...html.matchAll(/<th[^>]*>([^<]*)<\/th>/g)].map((match) => match[1]);
|
expect('表头顺序', heads.join(','), '序号,检验项目,子项,检测方法,检测要求,实测值,单位,判定');
|
|
// 合并属性跟着 `#` 走:加了一列之后它仍必须落在「检验项目」那一格,且只有一格
|
const cells = html.split('<td');
|
expectTrue('合并属性落在 # 标记的那一列', Boolean(cells.find((cell) => cell.includes('{{item.itemName}}'))?.includes(MERGE_ATTRS)));
|
expect(
|
'带合并属性的格数',
|
cells.filter((cell) => cell.includes('rowspan=')).length,
|
1,
|
);
|
|
// 显示开关被 columns 覆盖:把序号关掉、又自己写回序号列,就得出序号(两面打架时以 columns 为准)
|
const withToggleOff = build(groupedQualityTableDefinition, { columns: custom, showIndex: false });
|
expect('关掉「显示序号」也不影响自定义列', headCellCount(withToggleOff), 8);
|
|
// 默认列 7 列、这里 8 列:若解析被忽略,一定是 7
|
expectTrue('确实没有回落到默认列', headCellCount(html) !== 7);
|
}
|
|
// ------------------------------------------------- 3. headerSpans 在场:两级表头
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 3. headerSpans 在场:上层表头 + 下层列名 =====');
|
{
|
const html = build(groupedQualityTableDefinition, {
|
columns: '序号={{index}}|检验项目={{item.itemName}}|子项={{item.group.childName}}|检测要求={{item.requirement}}|实测值={{item.actualValue}}|单位={{item.unit}}|判定={{item.resultText}}',
|
headerSpans: '检验结果^5|结论^2',
|
});
|
|
expect('thead 变成两行', theadRowCount(html), 2);
|
expectTrue('上层第一格跨 5 列', html.includes('colspan="5"'));
|
expectTrue('上层第二格跨 2 列', html.includes('colspan="2"'));
|
expectTrue('上层表头文字在场', html.includes('>检验结果<') && html.includes('>结论<'));
|
|
// 上层 2 格、下层 7 格:上层只加跨列标题,绝不能把下层的列名顶掉
|
const theadRows = html
|
.slice(html.indexOf('<thead'), html.indexOf('</thead>'))
|
.split('<tr>')
|
.slice(1);
|
expect('上层表头 2 格', (theadRows[0]?.match(/<\/th>/g) ?? []).length, 2);
|
expect('下层表头 7 格', (theadRows[1]?.match(/<\/th>/g) ?? []).length, 7);
|
|
// 只有一层时不要凭空多出一行
|
expect('没填 headerSpans 仍是单层', theadRowCount(build(groupedQualityTableDefinition)), 1);
|
|
// 跨列数写不出 ^N 时按 1 列算,靠总数兜底(parser 的既有约定)
|
const implicit = build(groupedQualityTableDefinition, {
|
columns: '序号={{index}}|检验项目={{item.itemName}}|子项={{item.group.childName}}|检测要求={{item.requirement}}|实测值={{item.actualValue}}|单位={{item.unit}}|判定={{item.resultText}}',
|
headerSpans: '检验结果|结论',
|
});
|
expect('七段各按 1 列算,合计 2 ≠ 7 ⇒ 整层丢弃退回单层', theadRowCount(implicit), 1);
|
}
|
|
// --------------------------------- 3b. 纵向合并格:一列自己既是列名又是上格
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 3b. 纵向合并格(结论这类列)合成跨两行的一格 =====');
|
{
|
// 百事原件里最右一列「结论」是自己占满上下两层表头的一格(提取文本里下层那格写着「↑同上」)。
|
// 真模型一直照写 `结论^1`,若不认这个形态,「结论」二字会在上下两层各印一次。
|
const vertical = build(groupedQualityTableDefinition, {
|
columns:
|
'检验项目={{item.itemName}}|子项={{item.group.childName}}|检测方法={{item.checkMethod}}|标准要求={{item.standardValue}}|结果={{item.actualValue}}|结论={{item.resultText}}',
|
headerSpans: '检验结果^5|结论^1',
|
});
|
expectTrue('纵向合并格出 rowspan="2"', vertical.includes('rowspan="2"'));
|
expectTrue('不再出一格跨一列的上格', !vertical.includes('colspan="1"'));
|
expect('「结论」全文只印一次', (vertical.match(/结论/g) ?? []).length, 1);
|
expect('上层 2 格 + 下层 5 格 = 7', headCellCount(vertical), 7);
|
expect('仍然是两行表头', theadRowCount(vertical), 2);
|
|
// 判据是「标题与该列列名一字不差」,不是「排在最后」:不同字就老老实实按分组跨列走
|
const notVertical = build(groupedQualityTableDefinition, {
|
columns: '检验项目={{item.itemName}}|判定={{item.resultText}}',
|
headerSpans: '检验结果^1|结论^1',
|
});
|
expectTrue('上格与列名不同字 ⇒ 不出 rowspan', !notVertical.includes('rowspan="2"'));
|
expect('上格与列名不同字 ⇒ 两层各 2 格', headCellCount(notVertical), 4);
|
}
|
|
// ------------------------------------------------- 3c. 自定列不许把表格撑出纸张
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 3c. 自定列的单元格允许长串折行(否则整表溢出纸张)=====');
|
{
|
// 自定列拿不到任何宽度声明,列宽只能由最小内容宽度倒推;而值是 {{item.standardValue}}
|
// 这类不含空格的长串,最小内容宽度就是整串长度。实测 6 列时整表 810px,
|
// 而 A4 正文宽只有 672px:表格溢出 138px,最右一列被挤成 29px 的竖条
|
// (设计器里就是「结论」两个字竖着排在纸张外面)。加 overflow-wrap:anywhere 后整表 672px。
|
const custom = build(groupedQualityTableDefinition, {
|
columns:
|
'#项目={{item.itemName}}|子项={{item.group.childName}}|检测方法={{item.checkMethod}}|' +
|
'标准要求={{item.standardValue}}|结果={{item.actualValue}}|结论={{item.resultText}}',
|
headerSpans: '检验结果^5|结论^1',
|
});
|
const customCells = custom.split('<td').slice(1);
|
expect('自定列出 6 个单元格', customCells.length, 6);
|
expect(
|
'自定列的格都带 overflow-wrap:anywhere',
|
customCells.filter((cell) => cell.includes('overflow-wrap:anywhere')).length,
|
6,
|
);
|
|
// 默认列反过来:它自带手工调过的宽度,再叠一条折行会让没写宽度的「检验项目」「子项」
|
// 被挤到 30 余像素、行高从 59px 涨到 283px——实测过,不加比加好
|
const fallback = build(groupedQualityTableDefinition);
|
expectTrue('默认列的格不带 overflow-wrap', !fallback.includes('overflow-wrap'));
|
}
|
|
// ------------------------------------------------- 4. 写错时报得出、报得准
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 4. 写错时 validate 报的是能改的那一处 =====');
|
{
|
const badSum = validate(groupedQualityTableDefinition, {
|
columns: '序号={{index}}|检验项目={{item.itemName}}|子项={{item.group.childName}}|检测要求={{item.requirement}}|实测值={{item.actualValue}}',
|
headerSpans: '检验结果^5|结论^2',
|
});
|
expect('跨列数不匹配报 1 条', badSum.length, 1);
|
expectTrue(
|
'文案同时给出合计与实际列数',
|
badSum[0]?.includes('合计 7') && badSum[0]?.includes('实际列数为 5'),
|
badSum[0],
|
);
|
expectTrue('文案给出可执行动作', Boolean(badSum[0]?.includes('^')), badSum[0]);
|
|
const missingEqual = validate(groupedQualityTableDefinition, {
|
columns: '序号={{index}}|检验项目',
|
});
|
expect('缺 = 报 1 条', missingEqual.length, 1);
|
expectTrue('文案按段号定位', Boolean(missingEqual[0]?.includes('第 2 段')), missingEqual[0]);
|
|
const emptyExpr = validate(groupedQualityTableDefinition, { columns: '序号={{index}}|检测方法=' });
|
expectTrue('空的绑定表达式被拦下', emptyExpr.some((line) => line.includes('第 2 段')), JSON.stringify(emptyExpr));
|
|
// 列定义有错时不再叠加跨列数校验:那时列数不可信,报了只会把人往错的方向带
|
const both = validate(groupedQualityTableDefinition, {
|
columns: '序号={{index}}|检验项目',
|
headerSpans: '检验结果^5',
|
});
|
expect('列定义错 + 表头跨列也错 ⇒ 只报列定义', both.length, 1);
|
expectTrue('报的是列定义那条', Boolean(both[0]?.includes('第 2 段')), both[0]);
|
|
// 用默认列 + 两层表头:跨列数按默认的 7 列校验
|
const defaultOk = validate(groupedQualityTableDefinition, { headerSpans: '检验结果^5|结论^2' });
|
expect('默认 7 列 + 检验结果^5|结论^2 通过', defaultOk.length, 0);
|
|
// 隐藏了检测要求却留着判定:只有在用默认列时才算数(填了 columns 整表按 columns 来)
|
const toggleClash = validate(groupedQualityTableDefinition, { showRequirement: false });
|
expectTrue('默认列下才报「隐藏检测要求却保留判定」', toggleClash.length === 1, JSON.stringify(toggleClash));
|
const toggleClashCustom = validate(groupedQualityTableDefinition, {
|
showRequirement: false,
|
columns: '序号={{index}}|判定={{item.resultText}}',
|
});
|
expect('填了 columns 就不再报那条误报', toggleClashCustom.length, 0);
|
}
|
|
// ------------------------------------------------- 5. 样品信息字段可配
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 5. SampleInfo.fields =====');
|
{
|
const html = build(sampleInfoDefinition, {
|
fields:
|
'样品编号={{report.sampleNo}}|产品名称={{report.productName}}|规格={{report.spec}}|' +
|
'生产日期={{report.produceDate}}|批号={{report.batchNo}}|土豆品种={{report.potatoVariety}}|有效日期={{report.expireDate}}',
|
pairsPerRow: 2,
|
});
|
expect('7 项 × 2 格 = 14 个 td', (html.match(/<td/g) ?? []).length, 14);
|
expect('7 项每行 2 组 ⇒ 4 行', (html.match(/<tr>/g) ?? []).length, 4);
|
expectTrue('自定义字段名在场', html.includes('>土豆品种<'));
|
expectTrue('自定义绑定在场', html.includes('{{report.potatoVariety}}'));
|
|
// 每行放几组仍由 pairsPerRow 控制
|
const oneRow = build(sampleInfoDefinition, { fields: 'A={{report.a}}|B={{report.b}}|C={{report.c}}', pairsPerRow: 3 });
|
expect('pairsPerRow=3 ⇒ 1 行', (oneRow.match(/<tr>/g) ?? []).length, 1);
|
expect('pairsPerRow=3 ⇒ 6 格', (oneRow.match(/<td/g) ?? []).length, 6);
|
|
// 末行凑不满一组时,最后一格要横向吃掉余下的列。
|
// 不补的话右侧秃出几个只有边框的空格子——字段数不是「每行组数」整数倍时必然出现,
|
// 而 AI 导入按原件字段填 fields,奇偶根本不由我们定(原件 5 项、每行 2 组是最典型的一种)。
|
const ragged = build(sampleInfoDefinition, {
|
fields:
|
'产品名称={{report.productName}}|规格={{report.spec}}|批号={{report.batchNo}}|' +
|
'检验员={{report.inspector}}|检验日期={{report.inspectDate}}',
|
pairsPerRow: 2,
|
});
|
expect('5 项 ÷ 2 ⇒ 3 行', (ragged.match(/<tr>/g) ?? []).length, 3);
|
expect('补齐不增减格数(仍是 5 对 = 10 格)', (ragged.match(/<td/g) ?? []).length, 10);
|
expect('只多出 1 个 colspan', (ragged.match(/colspan=/g) ?? []).length, 1);
|
expectTrue('补齐落在末行最后一格,跨 3 列(2 列空位 + 自己)', ragged.includes('colspan="3"'), ragged.slice(-300));
|
const raggedTail = ragged.split('<tr>').pop() ?? '';
|
const tailCells = raggedTail.split('<td').slice(1);
|
expect('末行还是 2 格(字段名 + 值)', tailCells.length, 2);
|
expectTrue(
|
'补的是「值」格而不是「字段名」格:补到字段名上这一列的宽度就跟着变,各行左边缘会错开',
|
tailCells[0]?.includes('width:90px') && !tailCells[0]?.includes('colspan'),
|
tailCells[0],
|
);
|
expectTrue(
|
'末行仍是「检验日期 + 它的值」这一对',
|
raggedTail.includes('>检验日期<') && raggedTail.includes('{{report.inspectDate}}'),
|
raggedTail,
|
);
|
|
// 整行凑满时一格都不许补:默认字段正是 6 项(÷2 整行),多补一格就会动到上面冻结的逐字指纹
|
expectTrue('默认 6 项 ÷ 2 全是整行,无 colspan', !build(sampleInfoDefinition).includes('colspan='));
|
expectTrue(
|
'pairsPerRow=1 时每行自成一组,永远不补',
|
!build(sampleInfoDefinition, { pairsPerRow: 1 }).includes('colspan='),
|
);
|
const raggedThree = build(sampleInfoDefinition, {
|
fields: 'A={{report.a}}|B={{report.b}}|C={{report.c}}|D={{report.d}}',
|
pairsPerRow: 3,
|
});
|
expect('4 项 ÷ 3 ⇒ 末行 1 项,跨 5 列补齐(3 列空位 + 自己)', (raggedThree.match(/colspan="\d+"/g) ?? []).join(','), 'colspan="5"');
|
|
// 合并标记在样品信息里没有意义,必须明确拒绝而不是静默丢掉
|
const mergeRejected = validate(sampleInfoDefinition, { fields: '#样品编号={{report.sampleNo}}' });
|
expect('字段名带 # 报 1 条', mergeRejected.length, 1);
|
expectTrue('文案说明 # 是分组表的用法', Boolean(mergeRejected[0]?.includes('分组检验项表')), mergeRejected[0]);
|
expect('没有 # 则通过', validate(sampleInfoDefinition, { fields: '样品编号={{report.sampleNo}}' }).length, 0);
|
}
|
|
// ------------------------------------------------- 6. 注入:列标题/表达式先转义再进 HTML
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 6. 手写的列标题与字段名不能成为注入点 =====');
|
{
|
// 注意标题里不能带 `=`:`=` 是「标题=表达式」的分隔符,带 `=` 的标题在语法上就是非法的
|
// (见 parseColumnSpec 的约定),所以这里用不含 `=` 的标签来验转义
|
const table = build(groupedQualityTableDefinition, {
|
columns: '检验项目={{item.itemName}}|"><script>alert(1)</script>={{item.actualValue}}',
|
});
|
expectTrue(
|
'列标题里的标签被转义',
|
table.includes('"><script>alert(1)</script>'),
|
table.slice(0, 400),
|
);
|
expectTrue('产物里没有活的 script', !table.includes('<script'), table.slice(0, 400));
|
|
const sample = build(sampleInfoDefinition, { fields: '"><script>alert(1)</script>={{report.sampleNo}}' });
|
expectTrue('字段名里的引号与标签被转义', sample.includes('"><script>'), sample.slice(0, 400));
|
expectTrue('没有逃出属性外的 script', !sample.includes('<script>'), sample.slice(0, 400));
|
}
|
|
// ------------------------------------------------- 7. 解析器边界(两个组件共用的那一份)
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 7. 共用解析器:空段跳过、= 只切第一个、^ 从最后切 =====');
|
{
|
expect('首尾多写 | 不报错', parseColumnSpec('|A={{a}}|B={{b}}|').errors.length, 0);
|
expect('首尾多写 | 出 2 列', parseColumnSpec('|A={{a}}|B={{b}}|').items.length, 2);
|
expect('空串出 0 列', parseColumnSpec(' ').items.length, 0);
|
expect('undefined 出 0 列', parseColumnSpec(undefined).items.length, 0);
|
// 表达式里再出现 = 时不能被切成第三段
|
expect('= 只切第一个', parseColumnSpec('A=1=2').items[0]?.expr, '1=2');
|
expect('# 被剥掉并记为合并列', parseColumnSpec('#检验项目={{item.itemName}}').items[0]?.label, '检验项目');
|
expect('没写 # 就不是合并列', parseColumnSpec('检验项目={{item.itemName}}').items[0]?.merge, false);
|
}
|
|
// ------------------------------------------------- 8. aiHint / 积木清单:模型得知道能填这两个属性
|
|
// eslint-disable-next-line no-console
|
console.log('\n===== 8. aiHint 要说「列不一样就用 columns」,而不是「换组件」 =====');
|
{
|
const hint = groupedQualityTableDefinition.aiHint ?? '';
|
expectTrue('hint 点名了 columns', hint.includes('columns'), hint);
|
expectTrue('hint 点名了 headerSpans', hint.includes('headerSpans'), hint);
|
expectTrue(
|
'hint 说清「既有列名又是上格」的列怎么报',
|
hint.includes('↑同上') && hint.includes('跨两行'),
|
hint,
|
);
|
// 这一条是本轮最要紧的措辞:上一轮选型不稳的根因就是提示词里多了一处指向别处的描述,
|
// hint 若暗示「列不一致 ⇒ 换个组件」,等于把刚修好的稳定性再推回去
|
expectTrue('hint 明确「不要改用别的组件」', hint.includes('不要改用别的组件'), hint);
|
expectTrue('hint 仍保留「平铺一层时用 QualityTable」这条边界', hint.includes('QualityTable'), hint);
|
|
// 清单是 aiHint 与新属性进提示词的唯一出口:不在这里出现,模型就不可能填出来
|
registerAllQualityComponents();
|
const entry = buildQualityComponentCatalog().find((item) => item.type === 'GroupedQualityTable');
|
const keys = (entry?.fields ?? []).map((field) => field.key).join(',');
|
expectTrue('columns 随 propertySchema 自动进清单', keys.includes('columns'), keys);
|
expectTrue('headerSpans 随 propertySchema 自动进清单', keys.includes('headerSpans'), keys);
|
expectTrue('columns 声明为 text(属性面板出多行框)',
|
(entry?.fields ?? []).find((field) => field.key === 'columns')?.type === 'text', keys);
|
expectTrue('columns 不可绑定(手写整串,不走选数据字段)',
|
!(entry?.fields ?? []).find((field) => field.key === 'columns')?.bindable, keys);
|
expectTrue('清单带上了 hint', Boolean(entry?.hint?.includes('columns')));
|
|
const sample = buildQualityComponentCatalog().find((item) => item.type === 'SampleInfo');
|
expectTrue('SampleInfo 的 fields 进了清单',
|
(sample?.fields ?? []).some((field) => field.key === 'fields'));
|
}
|
|
// eslint-disable-next-line no-console
|
console.log(
|
failures.length === 0
|
? '\n全部通过:缺席时逐字不变,在场时列数/列序/合并/跨列都对,写错能定位到段,手写文本已转义,清单里带得到新属性。'
|
: `\n有 ${failures.length} 处不符:\n${failures.map((line) => ` - ${line}`).join('\n')}`,
|
);
|
process.exitCode = failures.length === 0 ? 0 : 1;
|