- 新增 tests/diy-card-qr-image-source.test.js(24 项): mini 基件 qrSrc 按「名片级 card.qr_image > 组件级 c.qrImage > 系统码 card.qr_url」三层优先级取值(源码契约 + 真实求值双证); 组件级 qrImage 仍完整保留(未被名片级顶掉); 反例防恒真 - diy-card-qr-style.test.js 补 2.20 盒模型/色带/逐向间距字段契约, 并修正一处陈旧断言: cornerHeadStyle 的 paddingRight 已叠加 2.20 逐向微调右位移(customRight), 旧正则未同步
450 lines
26 KiB
JavaScript
450 lines
26 KiB
JavaScript
/**
|
||
* 名片 DIY 组件 · 4.0 头像开关 + 二维码多彩/微调 契约测试
|
||
*
|
||
* 背景: 参考图复刻的第二轮增强:
|
||
* - 头像可隐藏(关闭后头部只留姓名/职位, 信息密度可调)
|
||
* - 二维码四档外观: plain 原图 / card 白卡 / theme 主题色 / gradient 渐变
|
||
* (多彩只装饰外围衬底, 码本身黑白保可扫 —— 不改共享 qr 生成端点)
|
||
* - 自定义尺寸(0=按档位自动) + transform 微调位移(WXSS 不支持 calc, 用 translate)
|
||
* - 扫码提示文案可关
|
||
*
|
||
* 覆盖:
|
||
* 1. common.js 纯函数(动态 import 真值断言, 非正则)
|
||
* 2. mini.vue 基件契约(先剥注释再断言)
|
||
* 3. show/self 6 字段透传
|
||
* 4. 两个 design.html 面板: 控件绑定 + 预览 mock 消费(属性变更预览同步)
|
||
* 5. 反例: 虚构外观/虚构字段必须查不到(防恒真)
|
||
*
|
||
* 跑法: node --test tests/diy-card-qr-style.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 PANEL_DIR = path.resolve(__dirname, '../../backend/src/addon/card/component/view');
|
||
const read = f => fs.readFileSync(path.join(DIY_DIR, f), 'utf8');
|
||
const strip = s => s
|
||
.replace(/<!--[\s\S]*?-->/g, '')
|
||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
||
|
||
const showSrc = strip(read('diy-card-show.vue'));
|
||
const selfSrc = strip(read('diy-card-self.vue'));
|
||
const miniSrc = strip(read('diy-card-mini.vue'));
|
||
// 面板目录属 backend 仓库, 可能不在本工作区: 缺失时跳过面板断言(组件契约照常跑)
|
||
const hasPanels = fs.existsSync(path.join(PANEL_DIR, 'diy-card-self/design.html'));
|
||
const showPanel = hasPanels ? strip(fs.readFileSync(path.join(PANEL_DIR, 'diy-card-show/design.html'), 'utf8')) : '';
|
||
const selfPanel = hasPanels ? strip(fs.readFileSync(path.join(PANEL_DIR, 'diy-card-self/design.html'), 'utf8')) : '';
|
||
const BOX_STYLE_FIELDS = [
|
||
'bandShow', 'bandBgMode', 'bandBg',
|
||
'wrapMarginTop', 'wrapMarginRight', 'wrapMarginBottom', 'wrapMarginLeft',
|
||
'headBgMode', 'headBg', 'headRadiusMode', 'headRadius', 'headSpacingCustom',
|
||
'headMarginTop', 'headMarginRight', 'headMarginBottom', 'headMarginLeft',
|
||
'headPaddingTop', 'headPaddingRight', 'headPaddingBottom', 'headPaddingLeft',
|
||
];
|
||
|
||
/**
|
||
* 面板控件的字段绑定有三种形态(2026-10-21 面板改版: 下拉框 → 可视化选项组,
|
||
* 数字框 → 滑块):
|
||
* ① v-model="nc.X" 输入框 / 开关
|
||
* ② nc.X = 'v' 可视化选项组(芯片)的赋值绑定
|
||
* ③ field: 'X' 滑块控件 :data="{field:'X'}"
|
||
* 断言的是「这个字段确实被某个控件绑定了」, 不限定控件形态 —— 契约是字段名, 不是控件类型。
|
||
*/
|
||
const bound = (src, field) => new RegExp(
|
||
'v-model(\\.[a-z]+)?="nc\\.' + field + '"'
|
||
+ '|\\bnc\\.' + field + "\\s*=\\s*['\"]"
|
||
+ '|field\\s*:\\s*[\'"]' + field + '[\'"]'
|
||
).test(src);
|
||
|
||
// =============================================================
|
||
// 1. common.js 纯函数(真值断言)
|
||
// =============================================================
|
||
|
||
test('common: QR_STYLES 四档外观枚举', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
assert.deepEqual(m.QR_STYLES, ['plain', 'card', 'theme', 'gradient']);
|
||
});
|
||
|
||
test('common: normalizeQrStyle 合法直通, 未知/空回落 card(默认白卡)', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
for (const s of ['plain', 'card', 'theme', 'gradient']) {
|
||
assert.equal(m.normalizeQrStyle(s), s);
|
||
}
|
||
// 未配置(存量装修页无此字段) → card, 与 diy_view.php 默认值同口径
|
||
assert.equal(m.normalizeQrStyle(''), 'card');
|
||
assert.equal(m.normalizeQrStyle(undefined), 'card');
|
||
assert.equal(m.normalizeQrStyle(null), 'card');
|
||
assert.equal(m.normalizeQrStyle('rainbow'), 'card');
|
||
// 面板只下发小写档位; 大写属未知值, 回落 card(保守, 不炸)
|
||
assert.equal(m.normalizeQrStyle('CARD'), 'card');
|
||
// 中文档位名别名(面板下拉若直接存中文档名, 归一化兜住, 否则 C 端永远回落 card)
|
||
assert.equal(m.normalizeQrStyle('原图'), 'plain');
|
||
assert.equal(m.normalizeQrStyle('白卡描边'), 'card');
|
||
assert.equal(m.normalizeQrStyle('主题色衬底'), 'theme');
|
||
assert.equal(m.normalizeQrStyle('渐变圆角'), 'gradient');
|
||
});
|
||
|
||
test('common: normalizeQrPosition 位置档中文别名兼容', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
assert.equal(m.normalizeQrPosition('资料行下方'), 'rows');
|
||
assert.equal(m.normalizeQrPosition('头部右侧'), 'head');
|
||
assert.equal(m.normalizeQrPosition('右上角浮层'), 'corner');
|
||
assert.equal(m.normalizeQrPosition('主按钮上方'), 'bottom');
|
||
// 英文码直通 / 未知回落 rows
|
||
assert.equal(m.normalizeQrPosition('bottom'), 'bottom');
|
||
assert.equal(m.normalizeQrPosition('nope'), 'rows');
|
||
});
|
||
|
||
test('common: darkenColor 压暗生成可见渐变终止色', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
// 纯色压暗 0.18: #119dff → #0e81d1
|
||
assert.equal(m.darkenColor('#119dff', 0.18), '#0e81d1');
|
||
// 比例越大约暗
|
||
assert.notEqual(m.darkenColor('#119dff', 0.4), m.darkenColor('#119dff', 0.18));
|
||
// 非法输入原样返回(不影响既有渲染)
|
||
assert.equal(m.darkenColor('#12', 0.18), '#12');
|
||
assert.equal(m.darkenColor('rgb(1,2,3)', 0.18), 'rgb(1,2,3)');
|
||
});
|
||
|
||
test('common: rowIconFor 资料行图标字段映射(含方案1邮箱预留 icon-youxiang)', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
// 有字形的字段返回 iconfont 类
|
||
assert.equal(m.rowIconFor('mobile'), 'icon-dianhua');
|
||
assert.equal(m.rowIconFor('phone'), 'icon-dianhua');
|
||
assert.equal(m.rowIconFor('wechat'), 'icon-weixin');
|
||
assert.equal(m.rowIconFor('address'), 'icon-dizhi');
|
||
// 邮箱: 方案1 预留 icon-youxiang(需字库补充信封字形后方能显示), 多键名兜底
|
||
assert.equal(m.rowIconFor('email'), 'icon-youxiang');
|
||
assert.equal(m.rowIconFor('mail'), 'icon-youxiang');
|
||
assert.equal(m.rowIconFor('邮箱'), 'icon-youxiang');
|
||
// 网站(2026-10-22): 复用字库现成链条字形 icon-fuzhilianjie, 多键名兜底
|
||
assert.equal(m.rowIconFor('website'), 'icon-fuzhilianjie');
|
||
assert.equal(m.rowIconFor('link'), 'icon-fuzhilianjie');
|
||
assert.equal(m.rowIconFor('网站'), 'icon-fuzhilianjie');
|
||
assert.equal(m.rowIconFor('custom'), '');
|
||
assert.equal(m.rowIconFor(''), '');
|
||
});
|
||
|
||
test('common: iconfont.css 邮箱信封 SVG 覆盖(字库无字形期的降级实现)', () => {
|
||
const iconSrc = fs.readFileSync(path.resolve(__dirname, '../common/css/iconfont.css'), 'utf8');
|
||
// 覆盖规则存在且 content 置空(避免渲染豆腐块), 尺寸/对齐与 row-icon 槽位匹配
|
||
assert.match(iconSrc, /\.icon-youxiang:before \{[\s\S]*?content: "";[\s\S]*?width: 30rpx;[\s\S]*?height: 30rpx;/);
|
||
// 信封 SVG data-uri(描边风格, #999999 与资料行标签默认色一致)
|
||
assert.match(iconSrc, /\.icon-youxiang:before \{[\s\S]*?data:image\/svg\+xml,[\s\S]*?stroke='%23999999'[\s\S]*?%3E%3Crect x='2' y='4' width='20' height='16'[\s\S]*?center\/contain no-repeat/);
|
||
});
|
||
|
||
test('common: resolveQrSize 自定义>档位自动, 单位 px→rpx(×2)', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
// 自定义尺寸(面板 px) × 2 = rpx
|
||
assert.equal(m.resolveQrSize('rows', 120), 240);
|
||
assert.equal(m.resolveQrSize('corner', 60), 120);
|
||
// 0 / 未配置 / 非法 → 按档位自动
|
||
assert.equal(m.resolveQrSize('rows', 0), m.QR_SIZES.rows);
|
||
assert.equal(m.resolveQrSize('head', 0), m.QR_SIZES.head);
|
||
assert.equal(m.resolveQrSize('corner', 0), m.QR_SIZES.corner);
|
||
assert.equal(m.resolveQrSize('bottom', 0), m.QR_SIZES.bottom);
|
||
assert.equal(m.resolveQrSize('rows', undefined), m.QR_SIZES.rows);
|
||
assert.equal(m.resolveQrSize('rows', -30), m.QR_SIZES.rows);
|
||
assert.equal(m.resolveQrSize('rows', 'abc'), m.QR_SIZES.rows);
|
||
// 未知档位 → rows 兜底
|
||
assert.equal(m.resolveQrSize('sideways', 0), m.QR_SIZES.rows);
|
||
// 字符串数字也要认(面板可能给字符串)
|
||
assert.equal(m.resolveQrSize('rows', '120'), 240);
|
||
});
|
||
|
||
test('common: qrTransform 位移(任一轴非零才输出; WXSS 支持 translate)', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
assert.equal(m.qrTransform(0, 0), '');
|
||
assert.equal(m.qrTransform(undefined, undefined), '');
|
||
assert.equal(m.qrTransform(null, null), '');
|
||
assert.equal(m.qrTransform('', ''), '');
|
||
// px × 2 = rpx
|
||
assert.equal(m.qrTransform(10, -20), 'translate(20rpx, -40rpx)');
|
||
assert.equal(m.qrTransform(5, 0), 'translate(10rpx, 0rpx)');
|
||
assert.equal(m.qrTransform(0, 5), 'translate(0rpx, 10rpx)');
|
||
assert.equal(m.qrTransform(-50, 50), 'translate(-100rpx, 100rpx)');
|
||
// 字符串数字(面板 number 输入可能给字符串)
|
||
assert.equal(m.qrTransform('10', '20'), 'translate(20rpx, 40rpx)');
|
||
// 非数字 → 0
|
||
assert.equal(m.qrTransform('abc', 'xyz'), '');
|
||
});
|
||
|
||
// =============================================================
|
||
// 2. mini.vue 基件契约
|
||
// =============================================================
|
||
|
||
test('mini: 顶部色带默认隐藏,背景支持同步名片设计或独立颜色', () => {
|
||
assert.match(miniSrc, /class="mini-card-wrap"/);
|
||
assert.match(miniSrc, /<view v-if="bandShow" class="mini-band" :style="bandStyle"><\/view>/);
|
||
assert.match(miniSrc, /bandShow\(\)\s*\{\s*return flagOn\(this\.c\.bandShow, false\)/);
|
||
assert.match(miniSrc, /bandStyle\(\)\s*\{[\s\S]*?this\.c\.bandBgMode === 'custom'[\s\S]*?themeGradient\(this\.card\)/);
|
||
assert.match(miniSrc, /class="mini-card fade-up" :style="wrapStyle"/);
|
||
// 保留原色带造型,显隐和背景只改内联消费,不改几何结构。
|
||
assert.match(miniSrc, /\.mini-card-wrap \{\s*position: relative;\s*margin-top: 40rpx;/);
|
||
assert.match(miniSrc, /\.mini-band \{\s*position: absolute;[\s\S]*?top: -40rpx;[\s\S]*?bottom: 88rpx;[\s\S]*?border-radius: 0 0 28rpx 28rpx;/);
|
||
assert.ok(!miniSrc.includes('mini-block'));
|
||
});
|
||
|
||
test('mini: 卡片四向外边距与头部盒模型配置完整消费', () => {
|
||
for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
|
||
assert.match(miniSrc, new RegExp('margin' + side + ': px2rpx\\(num\\(c\\.wrapMargin' + side + ', 0\\)\\)'));
|
||
assert.match(miniSrc, new RegExp('c\\.headMargin' + side));
|
||
assert.match(miniSrc, new RegExp('c\\.headPadding' + side));
|
||
}
|
||
assert.match(miniSrc, /headBoxStyle\(\)\s*\{[\s\S]*?c\.headBgMode === 'custom'[\s\S]*?this\.headTheme \? themeGradient\(this\.card\) : 'transparent'/);
|
||
assert.match(miniSrc, /headThemeStyle\(\)\s*\{\s*return this\.headBoxStyle/);
|
||
assert.match(miniSrc, /plainHeadFinalStyle\(\)\s*\{[\s\S]*?this\.cornerHeadStyle[\s\S]*?return \[this\.headBoxStyle, corner\]/);
|
||
});
|
||
|
||
test('mini: C端盒模型计算值——历史默认兼容且自定义值逐项生效', async () => {
|
||
const m = await import('../components-diy/diy-card-common.js');
|
||
const script = miniSrc.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||
.replace(/import\s*\{[\s\S]*?\}\s*from\s*'\.\/diy-card-common\.js';/, '')
|
||
.replace('export default {', 'module.exports = {');
|
||
const names = Object.keys(m);
|
||
const moduleBox = { exports: {} };
|
||
new Function('module', ...names, script)(moduleBox, ...names.map(k => m[k]));
|
||
const computed = moduleBox.exports.computed;
|
||
const util = { objToStyle: o => o };
|
||
const card = { theme_color: '#119DFF', gradient_end: '#0E81D1', gradient_angle: 135 };
|
||
|
||
assert.equal(computed.bandShow.call({ c: {} }), false, '历史配置色带必须默认隐藏');
|
||
assert.equal(computed.bandStyle.call({ c: {}, card, $util: util }).background.includes('linear-gradient'), true);
|
||
assert.equal(computed.bandStyle.call({ c: { bandBgMode: 'custom', bandBg: '#123456' }, card, $util: util }).background, '#123456');
|
||
|
||
const wrap = computed.wrapStyle.call({
|
||
c: { wrapMarginTop: 11, wrapMarginRight: 12, wrapMarginBottom: 13, wrapMarginLeft: 14, padding: 22 },
|
||
card, $util: util,
|
||
});
|
||
assert.deepEqual([wrap.marginTop, wrap.marginRight, wrap.marginBottom, wrap.marginLeft], ['22rpx', '24rpx', '26rpx', '28rpx']);
|
||
assert.equal(wrap.padding, '44rpx');
|
||
|
||
const autoHead = computed.headBoxStyle.call({ c: { padding: 16 }, headTheme: true, card, $util: util });
|
||
assert.deepEqual(
|
||
[autoHead.marginTop, autoHead.marginRight, autoHead.marginBottom, autoHead.marginLeft],
|
||
['-32rpx', '-32rpx', '0rpx', '-32rpx']
|
||
);
|
||
assert.deepEqual(
|
||
[autoHead.paddingTop, autoHead.paddingRight, autoHead.paddingBottom, autoHead.paddingLeft],
|
||
['32rpx', '32rpx', '28rpx', '32rpx']
|
||
);
|
||
assert.equal(autoHead.borderRadius, '0 0 16rpx 16rpx');
|
||
const customCfg = {
|
||
headBgMode: 'custom', headBg: '#ABCDEF', headRadiusMode: 'custom', headRadius: 18, headSpacingCustom: 1,
|
||
headMarginTop: 1, headMarginRight: 2, headMarginBottom: 3, headMarginLeft: 4,
|
||
headPaddingTop: 5, headPaddingRight: 6, headPaddingBottom: 7, headPaddingLeft: 8,
|
||
};
|
||
for (const headTheme of [true, false]) {
|
||
const style = computed.headBoxStyle.call({ c: customCfg, headTheme, card, $util: util });
|
||
assert.equal(style.background, '#ABCDEF');
|
||
assert.equal(style.borderRadius, '36rpx');
|
||
assert.deepEqual([style.marginTop, style.marginRight, style.marginBottom, style.marginLeft], ['2rpx', '4rpx', '6rpx', '8rpx']);
|
||
assert.deepEqual([style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft], ['10rpx', '12rpx', '14rpx', '16rpx']);
|
||
}
|
||
});
|
||
|
||
test('mini: 企业徽标固定红底(参考图口径, 与头像底色解耦)', () => {
|
||
assert.match(miniSrc, /badgeStyle\(\)\s*\{\s*return this\.\$util\.objToStyle\(\{\s*background: '#FA5151'\s*\}\);/);
|
||
});
|
||
|
||
test('mini: 头像开关(默认显示, 关掉只留姓名/职位)', () => {
|
||
assert.match(miniSrc, /showAvatarBlock\(\)\s*\{\s*return flagOn\(this\.c\.showAvatar,\s*true\)/);
|
||
// 模板头像块受开关控制(无 v-else 占位 —— 关掉即消失, 头部信息自然左移)
|
||
assert.match(miniSrc, /<view v-if="showAvatarBlock" class="mini-avatar" :style="avatarStyle">/);
|
||
// 反例形态: 不允许出现常显头像(绕过开关)
|
||
assert.ok(!miniSrc.includes('<view class="mini-avatar"'));
|
||
});
|
||
|
||
test('mini: qrStyle 计算属性归一化', () => {
|
||
assert.match(miniSrc, /qrStyle\(\)\s*\{\s*return normalizeQrStyle\(this\.c\.qrStyle\)/);
|
||
});
|
||
|
||
test('mini: qrBlockStyle 四档外观分支(多彩只装饰外围, 码黑白保扫)', () => {
|
||
// card 白卡描边(分支内逐行收紧, 防跨分支 lazy 匹配造成假过)
|
||
assert.match(miniSrc, /if \(this\.qrStyle === 'card'\) \{\s+style\.background = '#ffffff';\s+style\.border = '2rpx solid #EEF0F3';\s+style\.borderRadius = '12rpx';\s+style\.padding = '10rpx';/);
|
||
// theme 主题色衬底
|
||
assert.match(miniSrc, /else if \(this\.qrStyle === 'theme'\) \{\s+style\.background = theme;\s+style\.borderRadius = '12rpx';\s+style\.padding = '10rpx';/);
|
||
// gradient 渐变 + 6rpx 紧凑内边距; 单主题色用 darkenColor 兜底终止色(可见明暗)
|
||
assert.match(miniSrc, /else if \(this\.qrStyle === 'gradient'\) \{\s+const end = \(this\.card && this\.card\.gradient_end\) \|\| darkenColor\(theme, 0\.18\);\s+style\.background = 'linear-gradient\(135deg, ' \+ theme \+ ' 0%, ' \+ end \+ ' 100%\)';\s+style\.borderRadius = '16rpx';\s+style\.padding = '6rpx';/);
|
||
// plain 不进任何分支(无衬底, 即原图) —— 三个 if 之外不再设 background
|
||
assert.ok(!miniSrc.includes("qrStyle === 'plain' ? style.background"));
|
||
// 2026-10-23 用户定稿: 外观衬底只包二维码图(qrImgStyle), 「扫码查看完整名片」tip 在衬底外无蓝底
|
||
assert.match(miniSrc, /class="qr-img" :style="qrImgStyle"/);
|
||
const tipBinds = miniSrc.match(/class="qr-tip" :style="labelStyle"/g) || [];
|
||
assert.equal(tipBinds.length, 5, '五档 tip 仍独立(不进衬底; 含 theme 头部块), 实际 ' + tipBinds.length);
|
||
});
|
||
|
||
test('mini: 二维码尺寸取 resolveQrSize(自定义>0 优先, 否则档位自动)', () => {
|
||
// qrBlockStyle(容器) + qrImgStyle(图, 外观衬底) + cornerClearStyle(垂直让位) + cornerHeadStyle(水平让位)
|
||
// (自定义大尺寸时浮层占高与占宽都要动态跟随)
|
||
const hits = miniSrc.match(/resolveQrSize\(this\.qrPos, this\.c\.qrSize\)/g) || [];
|
||
assert.equal(hits.length, 4, 'qrBlockStyle + qrImgStyle + cornerClearStyle + cornerHeadStyle 四处, 实际 ' + hits.length);
|
||
});
|
||
|
||
test('mini: 微调位移 transform(不用 calc, WXSS 兼容)', () => {
|
||
assert.match(miniSrc, /const transform = qrTransform\(this\.c\.qrOffsetX, this\.c\.qrOffsetY\);\s*if \(transform\) style\.transform = transform;/);
|
||
// 位移零值不输出 transform(不污染默认布局)
|
||
assert.match(miniSrc, /if \(transform\) style\.transform = transform;/);
|
||
});
|
||
|
||
test('mini: 扫码提示开关 + 四个挂点统一消费', () => {
|
||
assert.match(miniSrc, /showQrTip\(\)\s*\{\s*return flagOn\(this\.c\.qrTipShow,\s*true\)/);
|
||
// 二维码 tip 挂点: corner/rows/bottom 各 1 + head 档 2(head 双形态分支互斥渲染, 2.17)
|
||
const tips = miniSrc.match(/<text v-if="showQrTip" class="qr-tip"/g) || [];
|
||
assert.equal(tips.length, 5, 'tip 挂点(4 档 + head 双分支各一), 实际 ' + tips.length);
|
||
});
|
||
|
||
test('mini: cornerClearStyle 按实际尺寸反算让位(防遮挡, 纯数值不用 calc)', () => {
|
||
assert.match(miniSrc, /cornerClearStyle\(\)\s*\{[\s\S]*?if \(this\.qrPos !== 'corner' \|\| !this\.showQrBase\) return \{\};/);
|
||
// 让位量 = 浮层 top 20 + 块高[尺寸+衬底+tip] - 头部 104 + 间隙 10; 下限 24
|
||
assert.match(miniSrc, /Math\.max\(24, total - headHeight \+ 10\)/);
|
||
// 衬底与 tip 参与计算(关 tip 时让位自动收窄)
|
||
assert.match(miniSrc, /const tip = this\.showQrTip \? 42 : 0;/);
|
||
});
|
||
|
||
test('mini: cornerHeadStyle 头部水平让位按浮层实际宽度动态反算', () => {
|
||
// 审计修复: 静态 180rpx 在自定义 qrSize>120 时不足 → 头部让位动态化
|
||
assert.match(miniSrc, /cornerHeadStyle\(\)\s*\{[\s\S]*?if \(this\.qrPos !== 'corner' \|\| !this\.showQrBase\) return \{\};/);
|
||
// 浮层宽[尺寸+衬底] + 右偏移 20 + 呼吸 20 + 2.20 逐向微调右位移(0=关)
|
||
assert.match(miniSrc, /const customRight = flagOn\(this\.c\.qrSpacingCustom, false\) \? px2rpx\(num\(this\.c\.qrMarginRight, 0\)\) : 0;/);
|
||
assert.match(miniSrc, /paddingRight: \(size \+ pad \+ 40 \+ customRight\) \+ 'rpx'/);
|
||
// 模板头部绑定了动态让位(浮层不存在时输出空对象不占位)
|
||
assert.match(miniSrc, /<view class="mini-head" :style="cornerHeadStyle">/);
|
||
});
|
||
|
||
test('mini: 二维码块样式绑定——corner/head 走 qrBlockStyle, rows/bottom 走 qrBlockFinalStyle(2.19.10 分卡合并)', () => {
|
||
const direct = miniSrc.match(/:style="qrBlockStyle"/g) || [];
|
||
assert.equal(direct.length, 3, 'qrBlockStyle 直绑(corner 1 + head 双分支 2), 实际 ' + direct.length);
|
||
const final = miniSrc.match(/:style="qrBlockFinalStyle"/g) || [];
|
||
assert.equal(final.length, 2, 'qrBlockFinalStyle(rows/bottom 各 1), 实际 ' + final.length);
|
||
});
|
||
|
||
// =============================================================
|
||
// 3. self miniConfig 透传 / show 自绘版式二维码契约
|
||
// =============================================================
|
||
|
||
test('self: miniConfig 透传头像+二维码新六字段', () => {
|
||
assert.match(selfSrc, /showAvatar:\s*v\.showAvatar/);
|
||
assert.match(selfSrc, /qrStyle:\s*v\.qrStyle/);
|
||
assert.match(selfSrc, /qrSize:\s*v\.qrSize/);
|
||
assert.match(selfSrc, /qrOffsetX:\s*v\.qrOffsetX/);
|
||
assert.match(selfSrc, /qrOffsetY:\s*v\.qrOffsetY/);
|
||
assert.match(selfSrc, /qrTipShow:\s*v\.qrTipShow/);
|
||
});
|
||
|
||
test('self: miniConfig 透传色带、卡片外边距与头部盒模型字段', () => {
|
||
for (const f of BOX_STYLE_FIELDS) {
|
||
assert.match(selfSrc, new RegExp(f + ':\\s*v\\.' + f + ','), 'miniConfig 缺透传 ' + f);
|
||
}
|
||
});
|
||
|
||
test('self: 「我的名片」角标与未登录提示条均已移除(2026-10-23 用户定稿)', () => {
|
||
// 角标按 2026-10-23 参考图删除, 反例防回归
|
||
assert.ok(!selfSrc.includes('self-badge') && !selfSrc.includes('slot="badge"'));
|
||
// 未登录提示条(「登录后即可展示我的名片·去看看名片」)按用户要求整条移除, 反例防回归
|
||
assert.ok(!selfSrc.includes('self-login-bar') && !selfSrc.includes('loginBarTip'));
|
||
// 空态分支接管提示条删除后的条件(非引导模式 + 无卡 + 非加载)
|
||
assert.match(selfSrc, /v-else-if="!loading && !card" class="self-empty"/);
|
||
});
|
||
|
||
test('show: miniConfig 全字段透传(方案A定稿: C端=diy-card-mini 基件=面板预览逐字段一致)', () => {
|
||
// 2026-10-24 用户定稿: Show 弃自绘版式恢复基件 —— 面板 53 字段全部经 miniConfig 生效(与 self 同构);
|
||
// 行为由 mini 侧同名测试覆盖, 此处锁透传链防 merge 冲突解决再丢(10-24 两次 merge 曾丢失整段透传)
|
||
const fields = [
|
||
// 视觉开关/设计资产
|
||
'showQuick', 'showDetailBtn', 'detailBtnText', 'useCover', 'showGradient', 'showLogo', 'showShadow',
|
||
'showRows', 'maxRows', 'showRowIcons', 'rowTap',
|
||
// 字号 7 字段
|
||
'nameSize', 'cardTitleSize', 'subSize', 'labelSize', 'valueSize', 'actSize', 'btnSize',
|
||
// 二维码六字段 + 自定义图
|
||
'showQr', 'qrTip', 'qrPosition', 'showAvatar', 'qrStyle', 'qrSize', 'qrOffsetX', 'qrOffsetY', 'qrTipShow', 'qrImage',
|
||
// 2.17 双形态 + 2.18 位置/成块 + 2.19 图层管理全套
|
||
'headStyle', 'headNameColor', 'headSubColor', 'actsLayout', 'actGridColorMode',
|
||
'showRowsTitle', 'rowsTitle', 'showCopyBtn', 'actsPosition', 'actsBlockBg', 'actsBlockRadius', 'actsBlockPad',
|
||
'actIcons', 'actVis', 'actDeleted', 'actLabels', 'actOrder', 'customActs',
|
||
...BOX_STYLE_FIELDS,
|
||
];
|
||
for (const f of fields) {
|
||
assert.match(showSrc, new RegExp(f + ':\\s*v\\.' + f + ','), 'miniConfig 缺透传 ' + f);
|
||
}
|
||
// 二维码/发名片点击链路: 基件 onQrTap/onSend $emit('detail'/'send'), show 接收方挂 @detail
|
||
assert.match(showSrc, /@detail="goDetail"/);
|
||
});
|
||
|
||
// =============================================================
|
||
// 4. design.html 面板: 控件绑定 + 预览 mock 消费
|
||
// =============================================================
|
||
|
||
for (const [name, panel] of hasPanels ? [['show', showPanel], ['self', selfPanel]] : []) {
|
||
test(name + ' 面板: 头像开关 + 二维码新六字段有 v-model 绑定', () => {
|
||
assert.match(panel, /v-model="nc\.showAvatar"/);
|
||
// qrStyle/qrSize 为档位 chips/slide 绑定形态(2026-10-24 merge 后口径), 其余字段 v-model
|
||
assert.match(panel, /(?:v-model|@click)="[^"]*nc\.qrStyle/);
|
||
assert.match(panel, /field:\s*'qrSize'/);
|
||
assert.match(panel, /v-model\.number="nc\.qrOffsetX"/);
|
||
assert.match(panel, /v-model\.number="nc\.qrOffsetY"/);
|
||
assert.match(panel, /v-model="nc\.qrTipShow"/);
|
||
// 二维码外观四选项(plain/card/theme/gradient)
|
||
|
||
for (const v of ['plain', 'card', 'theme', 'gradient']) {
|
||
assert.ok(
|
||
panel.includes("nc.qrStyle = '" + v + "'") || panel.includes('<option value="' + v + '">'),
|
||
'缺选项 ' + v
|
||
);
|
||
}
|
||
});
|
||
|
||
test(name + ' 面板: 色带/卡片/头部盒模型字段均有控件且预览真实消费', () => {
|
||
const preview = panel.split('<template slot="edit-content">')[0];
|
||
for (const f of BOX_STYLE_FIELDS) {
|
||
assert.ok(bound(panel, f), '面板缺控件绑定 ' + f);
|
||
assert.ok(preview.includes('nc.' + f), '预览缺消费 ' + f);
|
||
}
|
||
assert.match(panel, /nc\.bandBgMode = 'theme'/);
|
||
assert.match(panel, /nc\.bandBgMode = 'custom'/);
|
||
assert.match(panel, /nc\.headBgMode = 'auto'/);
|
||
assert.match(panel, /nc\.headBgMode = 'custom'/);
|
||
});
|
||
|
||
test(name + ' 面板: 预览 mock 消费新属性(属性变更预览同步)', () => {
|
||
// 头像开关
|
||
assert.ok(panel.includes('nc.showAvatar !== false'), '预览不消费 showAvatar');
|
||
// 二维码外观四档
|
||
assert.ok(panel.includes("nc.qrStyle === 'plain'"), '预览不消费 qrStyle');
|
||
// 自定义尺寸: 只锁「自定义值优先于档位自动」这层语义, 不锁死具体像素值
|
||
// (档位默认值是可调的设计参数, 锁死会让改一次尺寸就得改一次测试)
|
||
assert.ok(panel.includes('nc.qrSize > 0 ? nc.qrSize'), '预览不消费 qrSize');
|
||
// 微调位移
|
||
assert.ok(panel.includes('nc.qrOffsetX || 0'), '预览不消费 qrOffsetX');
|
||
assert.ok(panel.includes('nc.qrOffsetY || 0'), '预览不消费 qrOffsetY');
|
||
// 提示开关
|
||
assert.ok(panel.includes('nc.qrTipShow !== false'), '预览不消费 qrTipShow');
|
||
});
|
||
}
|
||
|
||
// =============================================================
|
||
// 反例(防恒真)
|
||
// =============================================================
|
||
|
||
test('反例: 不存在第五档二维码外观', () => {
|
||
assert.ok(!miniSrc.includes("qrStyle === 'rainbow'"));
|
||
assert.ok(!miniSrc.includes("qrStyle === 'neon'"));
|
||
// 面板同理
|
||
assert.ok(!showPanel.includes("nc.qrStyle == 'rainbow'"));
|
||
assert.ok(!selfPanel.includes("nc.qrStyle == 'rainbow'"));
|
||
});
|
||
|
||
test('反例: 虚构字段绝不出现在面板/基件', () => {
|
||
assert.ok(!showPanel.includes('nc.qrMagic'));
|
||
assert.ok(!selfPanel.includes('nc.qrMagic'));
|
||
assert.ok(!miniSrc.includes('c.qrMagic'));
|
||
assert.ok(!showSrc.includes('v.qrMagic'));
|
||
assert.ok(!selfSrc.includes('v.qrMagic'));
|
||
});
|