/** * 名片 DIY 组件 ·「简介 / 社交媒体 / 标签 / 来源」契约测试(2.22, 2026-10-28) * * 背景: 名片 DIY 组件相对「名片基础信息」有字段遗漏(简介 bio 后端已下发但组件 * 不渲染、社交媒体 socials[] 整族缺失、标签/来源标注也没有)。 * 本次补齐, 且「简介」像资料区一样可独立成块 + 逐向微调。 * * 覆盖: * 1. 简介区块: 开关 + 独立成块判定 + 逐向间距(prefix=bio, 复用 blockCustomSpacing) * 2. 社交/标签/来源: 字段读取与渲染 * 3. 地址细分: address 行用完整地址覆盖 + 邮编独立成行 * 4. 通用块样式工厂: blockIndependent / blockFinalStyle 的语义 * 5. 反例: 不得出现「简介写死 mock」「社交恒为空」这类假绿 * * 跑法: node --test tests/diy-card-bio-social.test.js */ 'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const DIY_DIR = path.resolve(__dirname, '../components-diy'); const read = f => fs.readFileSync(path.join(DIY_DIR, f), 'utf8'); // 先剥注释再断言 —— 注释里写「绝不能写 X」会被自判违规 const strip = s => s .replace(//g, '') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|[^:])\/\/.*$/gm, '$1'); const miniSrc = strip(read('diy-card-mini.vue')); test('简介区块: 开关 + 独立成块 + 逐向间距(bio 前缀)', () => { // 开关 assert.match(miniSrc, /showBioBlock\(\)\s*\{[\s\S]*?flagOn\(this\.c\.showBio, false\)/, '缺少「显示简介」开关(flagOn(this.c.showBio))'); // 数据源: 名片简介 assert.match(miniSrc, /bioText\(\)\s*\{[\s\S]*?this\.card && this\.card\.bio/, '简介文本未取 card.bio'); // 独立成块判定 + 逐向间距 assert.match(miniSrc, /bioFinalStyle\(\)\s*\{[\s\S]*?blockFinalStyle\(this\.c, 'bio'\)/, '简介未走通用独立成块样式(blockFinalStyle(this.c, "bio"))'); assert.match(miniSrc, /function blockFinalStyle\(c, prefix\)/, '缺少通用 blockFinalStyle 工厂'); assert.match(miniSrc, /function blockCustomSpacing\(c, prefix\)/, '缺少通用逐向间距工厂(应复用而非新写一套)'); // 逐向字段走**通用 prefix 拼接**(C 端不写字面量, 与资料区/二维码同一套) assert.match(miniSrc, /side\('MarginTop'\)|c\[prefix \+ 'MarginTop'\]/, '通用逐向工厂未按 prefix 拼字段名(bio 应复用同一套)'); // 标题与行数 assert.match(miniSrc, /bioTitleShow\(\)/, '缺少简介标题开关'); assert.match(miniSrc, /bioMaxLines\(\)/, '缺少简介行数控制'); }); test('简介渲染: 模板消费 bioText, 不是写死文案', () => { assert.match(miniSrc, /class="mini-bio"[\s\S]*?\{\{ bioText \}\}/, '简介块未渲染 bioText'); // 反例: 不得出现写死的简介正文 assert.doesNotMatch(miniSrc, /这是一段示例简介|示例简介文案/, '简介正文疑似写死'); }); test('社交媒体: 读取 card.socials 并渲染, 可独立成块', () => { assert.match(miniSrc, /socialList\(\)\s*\{[\s\S]*?this\.card && Array\.isArray\(this\.card\.socials\)/, '社交列表未取 card.socials'); assert.match(miniSrc, /showSocialBlock\(\)/, '缺少社交显示开关'); assert.match(miniSrc, /socialFinalStyle\(\)\s*\{[\s\S]*?blockFinalStyle\(this\.c, 'social'\)/, '社交块未走通用独立成块样式'); assert.match(miniSrc, /class="mini-social"[\s\S]*?v-for="\(s, i\) in socialList"/, '社交块未渲染列表'); // 反例: 不得把社交写死成空 assert.doesNotMatch(miniSrc, /socialList\(\)\s*\{\s*return \[\];/, '社交列表被写死为空'); }); test('名片标签 / 来源标注 / 过期提示', () => { assert.match(miniSrc, /cardTags\(\)\s*\{[\s\S]*?this\.card && Array\.isArray\(this\.card\.tags\)/, '标签未取 card.tags'); assert.match(miniSrc, /showTagsRow\(\)/, '缺少标签显示开关'); assert.match(miniSrc, /sourceName\(\)\s*\{[\s\S]*?this\.card && this\.card\.source_name/, '来源标注未取 card.source_name'); assert.match(miniSrc, /showSourceRow\(\)/, '缺少来源显示开关'); assert.match(miniSrc, /cardExpired\(\)\s*\{[\s\S]*?this\.card && this\.card\.expired/, '过期标记未取 card.expired'); assert.match(miniSrc, /class="mini-expired"/, '缺少过期遮罩元素'); }); test('地址细分: address 行用完整地址覆盖 + 邮编独立成行', () => { assert.match(miniSrc, /decorateRows\(rows\)/, '缺少资料行装饰'); assert.match(miniSrc, /this\.card && this\.card\.address_full/, '未取完整地址'); assert.match(miniSrc, /this\.card && this\.card\.postal_code/, '未取邮编'); assert.match(miniSrc, /field === 'address'/, '未按 address 字段覆盖'); assert.match(miniSrc, /label: '邮编'/, '邮编未独立成行'); // rowsInfo 必须走 decorateRows(否则覆盖不生效) assert.match(miniSrc, /rowsInfo\(\)\s*\{[\s\S]*?visibleRows\(this\.decorateRows\(this\.card\.rows\)/, 'rowsInfo 未走 decorateRows'); }); test('多语言: 请求带 lang 参数(后端按 _en 优先下发)', () => { for (const f of ['diy-card-show.vue', 'diy-card-self.vue', 'diy-card-list.vue']) { const src = read(f); assert.match(src, /lang: uni\.getStorageSync\('lang'\) \|\| 'zh-cn'/, f + ' 请求未带 lang 参数'); } });