资料文件真产出来了;E04/I01 补到验收线,顺手填了几个「墙拦住却不给路」的坑
**资料文件全链**(用户:审核资料这一步要真产出 word/excel/md/html,前端能渲、点资料名能开)
接手时这一步一个文件都产不出来 —— S3 剧本白纸黑字写着「file_ref 本 MVP 阶段可留空」。
文件是台账那一行的投影,不另存一份:
· 声明 `file:{titleColumn,bodyColumn,formatColumn}`,它同时是授权面(没声明就点不开)
· 现造真字节 —— docx/xlsx 是自己打的 OOXML 包(zip-write:store+CRC32,零依赖)
· 读回来渲 —— 解的就是造出来那份字节,渲得出来才证明文件是真的
· 件上 `data-file-of` → 工作区二级界面(Word 纸面/Excel 网格/渲好的 md/沙箱 html)+ 下载原件
放行落地前把每一份冻进 `sys.task_asset_file`(迁移 0029):归档件点开的必须是签字那一刻
那一份,而资料落在主数据上、试跑单不落正式表 —— 不存这一份,收工后一个都点不开。
工作成果里「产出的资料」自成一摞。Markdown 富文本渲染顺带销账(仍零依赖)。
**Harness**
· 画面按「这一拍办完之后」在办的那一步挑 —— 从前拿拍首的局面决定拍尾的事:
收掉 S4、发出 S5 的卡,而屏上还摆着 S4 的只读件,人无处可填(墙要配路第四次现形)
· 验收「重做」真能退回重做,且打回后台账没动就不许再提交(软硬两段,不锁死单子)
· 立监听的授权面是清单声明;发卡的授权面是调人预算,不是计划项
· 读工具同参重调有兜底;空转写身后还有行动就不废串;finish 的 doneWhen 按写完后的草稿算
**界面**
· 翻件与翻页各归各位(从前二选一,多卡多页时页翻不动);没得翻写「—」不写 0/0
· 工作成果详情能翻份;表宽超出能横滑(E04/I01 抄版式时把 overflow-x 抄丢了)
· 查看面灰底滚过一屏变白 —— 两条独立的路都治了(沙箱内不同色 / 弹性子项不长个)
· 测试台「取不到」不再当成「还活着」
**活动**
E04 关系覆盖、I01 品牌内容各自补到验收线:判据全读基座数、判据指向的东西摆上卡面、
样例满载且名字 ≤14 字;提交前要裁的判据搬进说明书(模型会压拍,压过之后只有说明书还在)。
验收:e2e i01 4/4、e04 3/3 全部 released,资料在办中渲得出、放行后存了档点得开;
门禁 typecheck/lint/depcruise/1628 测全绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
This commit is contained in:
@@ -262,6 +262,68 @@ describe('data-edit / data-col:可改格子的输入孪生件', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('手写的下拉孪生件:当下的值要标 selected', () => {
|
||||
const SELECT_ROW =
|
||||
'<tbody data-rows="rs" data-edit="d.o"><tr><td><select class="jc-cell" data-col="depth">' +
|
||||
'<option value=""></option><option value="deep">深度</option><option value="standard">标准</option>' +
|
||||
'<option value="light">轻度</option></select></td></tr></tbody>';
|
||||
|
||||
it('当下的值标 selected —— 人一进编辑态就看得见 AI 填了什么', () => {
|
||||
const out = renderView(SELECT_ROW, { rs: [{ depth: 'standard' }] });
|
||||
expect(out).toContain('<option value="standard" selected>标准</option>');
|
||||
});
|
||||
|
||||
it('值匹配到具体那个 option,不是随便一个', () => {
|
||||
const out = renderView(SELECT_ROW, { rs: [{ depth: 'light' }] });
|
||||
expect(out).toContain('<option value="light" selected>轻度</option>');
|
||||
expect(out).toContain('<option value="deep">深度</option>');
|
||||
expect(out).toContain('<option value="standard">标准</option>');
|
||||
});
|
||||
|
||||
it('没有值 —— 照旧留白,不选任何一项(没答=不改的规矩不能被这条改掉)', () => {
|
||||
const out = renderView(SELECT_ROW, { rs: [{}] });
|
||||
expect(out).not.toContain('selected');
|
||||
});
|
||||
|
||||
it('值不在选项里 —— 原样留白,不硬凑一个', () => {
|
||||
const out = renderView(SELECT_ROW, { rs: [{ depth: 'unknown-code' }] });
|
||||
expect(out).not.toContain('selected');
|
||||
});
|
||||
|
||||
it('没标 data-edit 的表(归档件)一样标好 selected —— 静态归档也该如实呈现当下的值', () => {
|
||||
const out = renderView(SELECT_ROW.replace(' data-edit="d.o"', ''), { rs: [{ depth: 'deep' }] });
|
||||
expect(out).toContain('<option value="deep" selected>深度</option>');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 手写的多行文本孪生件 —— E04 执行回执的「结果摘要 / 客户反馈」照目标件是 textarea
|
||||
* (一句话装不下「见到的关键人、谈了什么、达成什么」)。与下拉同款:
|
||||
* 渲染时把当下的值填进正文,人一进编辑态看得见 AI/上一轮填了什么。
|
||||
*/
|
||||
describe('手写的多行文本孪生件:当下的值进正文', () => {
|
||||
const TEXTAREA_ROW =
|
||||
'<tbody data-rows="rs" data-edit="d.o"><tr><td>' +
|
||||
'<textarea class="jc-cell" data-col="result_summary" placeholder="谈了什么"></textarea>' +
|
||||
'</td></tr></tbody>';
|
||||
|
||||
it('当下的值进正文 —— 人一进编辑态就看得见已填的内容', () => {
|
||||
const out = renderView(TEXTAREA_ROW, { rs: [{ result_summary: '见到了产品 VP,达成样品测试意向' }] });
|
||||
expect(out).toContain('data-col="result_summary" placeholder="谈了什么">见到了产品 VP,达成样品测试意向</textarea>');
|
||||
});
|
||||
|
||||
it('没有值 —— 正文真空着,placeholder 才显得出来', () => {
|
||||
const out = renderView(TEXTAREA_ROW, { rs: [{}] });
|
||||
expect(out).toContain('placeholder="谈了什么"></textarea>');
|
||||
});
|
||||
|
||||
it('值要转义 —— 正文里混进闭合标签就是逃逸', () => {
|
||||
const out = renderView(TEXTAREA_ROW, { rs: [{ result_summary: '</textarea><script>alert(1)</script>' }] });
|
||||
expect(out).not.toContain('<script>');
|
||||
expect(out).toContain('</textarea>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('渲染两遍不叠加 —— 交出去的是干净 HTML', () => {
|
||||
it('第二遍渲染不会再长一个 input(属性已被消费)', () => {
|
||||
const once = renderView('<tbody data-rows="rs" data-edit="d.o"><tr><td><span data-slot="pn" data-col="pn"></span></td></tr></tbody>', {
|
||||
@@ -278,3 +340,37 @@ describe('渲染两遍不叠加 —— 交出去的是干净 HTML', () => {
|
||||
expect(out).not.toMatch(/<script|\son[a-z]+=/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data-file-of:这一格点开是一份资料文件', () => {
|
||||
const TPL =
|
||||
'<tbody data-rows="mine:content.asset"><tr>' +
|
||||
'<td><span data-file-of="content_id" data-slot="title"></span></td>' +
|
||||
'</tr></tbody>';
|
||||
|
||||
it('换成认得出行的 data-file="地址/主键值",原属性被消费掉', () => {
|
||||
const out = renderView(TPL, {
|
||||
'mine:content.asset': [{ content_id: 'CT-2026-0001', title: '供货能力说明' }],
|
||||
});
|
||||
expect(out).toContain('data-file="content.asset/CT-2026-0001"');
|
||||
expect(out).not.toContain('data-file-of');
|
||||
expect(out).toContain('供货能力说明');
|
||||
});
|
||||
|
||||
it('**地址去掉 `mine:` 前缀** —— 那是取值的路径,不是台账地址', () => {
|
||||
const out = renderView(TPL, { 'mine:content.asset': [{ content_id: 'A', title: 'T' }] });
|
||||
expect(out).toContain('data-file="content.asset/A"');
|
||||
});
|
||||
|
||||
it('那一列还空着:整个属性去掉 —— 没有文件的行不该长得像能点开', () => {
|
||||
const out = renderView(TPL, { 'mine:content.asset': [{ title: '还没编号' }] });
|
||||
expect(out).not.toContain('data-file');
|
||||
expect(out).toContain('还没编号');
|
||||
});
|
||||
|
||||
it('主键值里的引号被转义 —— 一行数据撑不开属性', () => {
|
||||
const out = renderView(TPL, { 'mine:content.asset': [{ content_id: 'A" onclick="x', title: 'T' }] });
|
||||
// 引号成了 ",那一串整个还在 data-file 的值里面 —— 没有第二个属性冒出来
|
||||
expect(out).toContain('data-file="content.asset/A" onclick="x"');
|
||||
expect(out).not.toContain('onclick="x"');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user