Files
lucky_shop/tests/diy-card-qr-style.test.js
ZF sun 1de3146c3a feat(card-diy): 2.19.10 C 端消费 detail 对齐 + 区块独立样式字段
- mini.vue: 8 detail 字段消费(三列行/复制范围外观/QR 多行/URL 行/hero 徽章)
  + rowsBlockStyle/qrBlockCustomStyle computed(与操作区 actsBlockStyle 同款模式)
  + rowsFinalStyle/qrBlockFinalStyle 合并独立块样式
- show.vue/self.vue: miniConfig 透传 14 字段(8 detail + 6 独立样式)
- 测试 ×3: 2.19.10 契约锁(配置层/透传/面板/预览消费/反例: layoutCards 已移除)

前端 7 套 123 项全绿
2026-10-24 17:03:57 +08:00

367 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 名片 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')) : '';
/**
* 面板控件的字段绑定有三种形态(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: 蓝色顶带版式(2026-10-23 与精选名片 show 同款: 自适应白卡高度 + 弧线距底 88rpx)', () => {
// 模板: 卡片包进 wrap, 顶带在最前
assert.match(miniSrc, /class="mini-card-wrap"/);
assert.match(miniSrc, /class="mini-band" :style="bandStyle"/);
assert.match(miniSrc, /class="mini-card fade-up" :style="wrapStyle"/);
// 计算属性: 顶带底色走主题渐变(与 show bandStyle 同口径)
assert.match(miniSrc, /bandStyle\(\)\s*\{\s*return this\.\$util\.objToStyle\(\{\s*background: themeGradient\(this\.card\)\s*\}\);/);
// CSS: wrap 撑露沿 + band 拉伸(top:-40rpx / bottom:88rpx) + 圆角(顶直底弧)
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;/);
// mini-block 方案不得回归
assert.ok(!miniSrc.includes('mini-block'));
});
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
assert.match(miniSrc, /paddingRight: \(size \+ pad \+ 40\) \+ '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: 「我的名片」角标与未登录提示条均已移除(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',
];
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 + ' 面板: 预览 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'));
});