@@ -227,6 +227,11 @@ test('mini.vue + show/self: save(存通讯录)动作全链路(2.17.1)', () => {
}
assert . match ( vue , /Array\.isArray\(c\.rows\)[\s\S]*?map\[r\.field\]/ , file + ' 字段从 rows(白名单)提取' ) ;
assert . doesNotMatch ( vue , /doAct\(act\)\s*\{\s*if \(!act \|\| !act\.value\) return;/ , file + ' doAct 不再拦 value 为空的 save' ) ;
// 2.19.3 扩展类型: sms/map/link 分支 + map 空值回落名片地址
assert . match ( vue , /act\.type === 'map' && !act\.value && this\.card && this\.card\.address/ , file + ' map 空值回落名片地址' ) ;
assert . match ( vue , /act\.type === 'sms'\)\s*\{[\s\S]*?window\.location\.href = 'sms:' \+ act\.value;[\s\S]*?号码已复制/ , file + ' sms 分支(H5 sms: 链接/非 H5 复制)' ) ;
assert . match ( vue , /act\.type === 'map'\)\s*\{[\s\S]*?地址已复制,可粘贴到地图导航/ , file + ' map 分支(复制地址)' ) ;
assert . match ( vue , /act\.type === 'link'\)\s*\{[\s\S]*?indexOf\('http:\/\/'\) === 0 [\s\S]*?\$util\.redirectTo\(act\.value, \{\}\)/ , file + ' link 分支(外链 H5 跳转/小程序复制, 站内 redirectTo)' ) ;
}
} ) ;
@@ -307,7 +312,8 @@ test('2.18: 操作区位置/独立样式/每按钮图标/自定义二维码 全
assert . match ( html , /js-act-color" data-scope="default" :data-key="a\.k" data-field="bg"/ , comp + ' 每按钮底色输入(同上)' ) ;
// 自定义二维码(媒体库)
assert . match ( html , /v-model="nc\.qrImage"/ , comp + ' 二维码自定义图输入' ) ;
assert . match ( html , /openAlbum\(function \(obj\)[\s\S]*?nc\.qrImage = obj\[0\]\.pic_path/ , comp + ' 二维码选图走 openAlbum 媒体库 ' ) ;
assert . match ( html , /js-qr-media" title="从媒体库选择二维码图片"/ , comp + ' 二维码选图按钮走 js-qr-media 委托(2.19.3, 内联 openAlbum 不可靠) ' ) ;
assert . doesNotMatch ( html , /@click="openAlbum\(/ , comp + ' 零内联 openAlbum 调用' ) ;
// 预览消费
assert . match ( html , /nc\.actsPosition === 'above'/ , comp + ' 预览上方档分支' ) ;
assert . match ( html , /nc\.actsPosition !== 'above'/ , comp + ' 预览下方档互斥守卫' ) ;
@@ -365,7 +371,7 @@ test('反例自测: 新形态标记不得出现在未改造组件中', () => {
* 2.19 快捷操作: 显示控制 + 自定义操作 + 素材图标库 + 图层式管理
* ------------------------------------------------------------------ */
const V19 _FIELDS = { actVis : 'object' , actLabels : 'object' , customActs : 'array' , actOrder : 'array' } ;
const V19 _FIELDS = { actVis : 'object' , actDeleted : 'object' , actLabels : 'object' , customActs : 'array' , actOrder : 'array' } ;
const V19 _ACT _LABELS = { tel : '打电话' , wx : '微信号' , mail : '发邮件' , web : '网站' , save : '存通讯录' } ;
test ( 'diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认文案/空数组/历史顺序=零变化)' , ( ) => {
@@ -381,6 +387,8 @@ test('diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认
assert . deepEqual ( v . customActs , [ ] , comp + '.customActs 默认空数组' ) ;
// actOrder(2.19.1): 历史渲染顺序
assert . deepEqual ( v . actOrder , [ 'tel' , 'wx' , 'mail' , 'web' , 'save' ] , comp + '.actOrder 默认=历史顺序' ) ;
// actDeleted(2.19.2): 全 0=无删除(历史零变化)
assert . deepEqual ( v . actDeleted , { tel : 0 , wx : 0 , mail : 0 , web : 0 , save : 0 } , comp + '.actDeleted 默认全 0' ) ;
}
} ) ;
@@ -434,13 +442,16 @@ test('common.js: normalizeActVis / normalizeActLabels / normalizeCustomActs / no
js . match ( /export const ACT_ORDER_DEFAULT = [^;]+;/ ) [ 0 ] . replace ( 'export const' , 'const' ) ,
] . join ( '\n' ) ;
const grab = ( name ) => js . match ( new RegExp ( 'export function ' + name + '[\\s\\S]*?\\n}' ) ) [ 0 ] . replace ( 'export function ' , 'function ' ) ;
const body = consts + '\n' + [ 'normalizeActVis' , 'normalizeActLabels' , 'normalizeCustomActs' , 'normalizeActOrder' ] . map ( grab ) . join ( '\n' ) + '\n'
+ 'return [normalizeActVis, normalizeActLabels, normalizeCustomActs, normalizeActOrder];' ;
const [ visFn , labelFn , customFn , orderFn ] = new Function ( body ) ( ) ;
const body = consts + '\n' + [ 'normalizeActVis' , 'normalizeActDeleted' , 'normalizeActLabels' , 'normalizeCustomActs' , 'normalizeActOrder' ] . map ( grab ) . join ( '\n' ) + '\n'
+ 'return [normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder];' ;
const [ visFn , delFn , labelFn , customFn , orderFn ] = new Function ( body ) ( ) ;
// actVis: 缺省全显(历史零变化); '0' → 0; 未知键忽略
assert . deepEqual ( visFn ( undefined ) , { tel : 1 , wx : 1 , mail : 1 , web : 1 , save : 1 } , 'actVis 缺省全显' ) ;
assert . deepEqual ( visFn ( { tel : '0' , wx : 0 } ) , { tel : 0 , wx : 0 , mail : 1 , web : 1 , save : 1 } , 'actVis 0=隐藏/缺键=显' ) ;
// actDeleted(2.19.2): 缺省全 0(历史零变化); '1' → 1
assert . deepEqual ( delFn ( undefined ) , { tel : 0 , wx : 0 , mail : 0 , web : 0 , save : 0 } , 'actDeleted 缺省全 0' ) ;
assert . deepEqual ( delFn ( { tel : '1' , save : 1 } ) , { tel : 1 , wx : 0 , mail : 0 , web : 0 , save : 1 } , 'actDeleted 1=已删/缺键=0' ) ;
// actLabels: 缺省=内置文案; 空串回落默认
assert . deepEqual ( labelFn ( undefined ) , V19 _ACT _LABELS , 'actLabels 缺省=内置默认' ) ;
assert . deepEqual ( labelFn ( { tel : '' , wx : '加微信' } ) . wx , '加微信' , 'actLabels 改名生效' ) ;
@@ -458,6 +469,8 @@ test('common.js: normalizeActVis / normalizeActLabels / normalizeCustomActs / no
assert . equal ( list [ 0 ] . label , '加群' , 'label 保留' ) ;
assert . equal ( list [ 1 ] . label , '自定义' , 'label 缺省' ) ;
assert . equal ( list [ 1 ] . type , 'tel' , '合法 type 保留' ) ;
// 2.19.3 扩展类型白名单
assert . deepEqual ( customFn ( [ { type : 'sms' , value : '139' } , { type : 'map' , value : '某某大厦' } , { type : 'link' , value : '/pages/index/index' } ] ) . map ( x => x . type ) , [ 'sms' , 'map' , 'link' ] , 'sms/map/link 新类型通过白名单' ) ;
// actOrder(2.19.1): 缺省=历史顺序; 去重+白名单+缺失尾补
assert . deepEqual ( orderFn ( undefined ) , [ 'tel' , 'wx' , 'mail' , 'web' , 'save' ] , 'actOrder 缺省=历史顺序' ) ;
assert . deepEqual ( orderFn ( [ 'save' , 'tel' , 'bogus' , 'save' , 'mail' ] ) , [ 'save' , 'tel' , 'mail' , 'wx' , 'web' ] , '重排去重+非法剔除+缺失尾补' ) ;
@@ -494,8 +507,11 @@ test('mini.vue: acts 合成(显隐过滤 + 次序 + 文案覆盖 + 自定义动
assert . match ( src , /acts\(\)\s*\{[\s\S]*?normalizeCustomActs\(this\.c\.customActs\)/ , 'acts 消费 normalizeCustomActs' ) ;
// 2.19.1 次序: 按 normalizeActOrder 输出默认动作 + order 外扩展类型尾接兜底
assert . match ( src , /acts\(\)\s*\{[\s\S]*?normalizeActOrder\(this\.c\.actOrder\)/ , 'acts 消费 normalizeActOrder' ) ;
assert . match ( src , /acts\(\)\s*\{[\s\S]*?const byType = \{\};[\s\S]*?order\.forEach\(t => \{[\s\S]*?if \(byType\[t\] && vis\[t\] !== 0\) out\.push\(merge\(byType\[t\]\)\); / , '按 order 次序取默认动作(vis=0 过滤) ' ) ;
assert . match ( src , /acts\(\)\s*\{[\s\S]*?if \(order\.indexOf\(t\) >= 0 \|\| vis\[t\] === 0\) return;/ , 'order 外扩展类型尾接且隐藏不渲染' ) ;
assert . match ( src , /acts\(\)\s*\{[\s\S]*?const byType = \{\};[\s\S]*?order\.forEach\(t => \{/ , 'acts 按 order 次序取默认动作' ) ;
assert . match ( src , /acts\(\)\s*\{[\s\S]*?if \(order\.indexOf\(t\) >= 0 \|\| vis\[t\] === 0 \|\| del\[t\] === 1 \) return;/ , 'order 外扩展类型尾接且隐藏/已删 不渲染' ) ;
// 2.19.2 删除: 按 order 取默认动作时 deleted=1 的不渲染
assert . match ( src , /acts\(\)\s*\{[\s\S]*?normalizeActDeleted\(this\.c\.actDeleted\)/ , 'acts 消费 normalizeActDeleted' ) ;
assert . match ( src , /acts\(\)\s*\{[\s\S]*?if \(byType\[t\] && vis\[t\] !== 0 && del\[t\] !== 1\) out\.push\(merge\(byType\[t\]\)\);/ , 'order 取默认动作时 vis=0/deleted=1 均过滤' ) ;
assert . match ( src , /labels\[t\] \|\| a\.label/ , 'actLabels 覆盖文案(空回落服务端)' ) ;
assert . match ( src , /cfgImg: String\(cfg\.img \|\| ''\)\.trim\(\)/ , '默认动作合并 cfgImg' ) ;
assert . match ( src , /cfgKey: String\(cfg\.iconKey \|\| ''\)\.trim\(\)/ , '默认动作合并 cfgKey(iconKey)' ) ;
@@ -505,12 +521,12 @@ test('mini.vue: acts 合成(显隐过滤 + 次序 + 文案覆盖 + 自定义动
assert . match ( src , /actLibStyle\(act\)\s*\{[\s\S]*?libIconBg\(this\.actLibOf\(act\), this\.actIconColor\(act\)\)/ , 'actLibStyle 消费 libIconBg + 四级色口径' ) ;
assert . equal ( ( src . match ( /v-else-if="actLibOf\(act\)" class="act-grid-svg" :style="actLibStyle\(act\)"/g ) || [ ] ) . length , 2 , '上/下两份宫格副本均插入素材库分支' ) ;
assert . match ( src , /actSvgOf\(act\)\s*\{[\s\S]*?!this\.actImgOf\(act\) && !this\.actLibOf\(act\) && !!WAP_LINE_ICONS/ , '线稿档被 img/素材库优先占位后不再进入' ) ;
assert . match ( src , /import \{[\s\S]*?normalizeActVis, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg/ , 'mini.vue 引入 2.19+2.19.1 依赖' ) ;
assert . match ( src , /import \{[\s\S]*?normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg/ , 'mini.vue 引入 2.19+2.19.1+2.19.2 依赖' ) ;
// 透传
// show.vue 自绘, 不持有 v 对象; actVis/actLabels/actOrder/customActs 透传仅校验 self.vue(走 v 架构)
for ( const file of [ 'diy-card-self.vue' ] ) {
const vue = readFileSync ( resolve ( FE , file ) , 'utf8' ) ;
for ( const f of [ 'actVis' , 'actLabels' , 'actOrder' , 'customActs' ] ) {
for ( const f of [ 'actVis' , 'actDeleted' , 'actLabels' , 'actOrder' , 'customActs' ] ) {
assert . match ( vue , new RegExp ( f + ': v\\.' + f + ',' ) , file + ' 透传 ' + f ) ;
}
}
@@ -522,13 +538,22 @@ test('design.html × 2: 图层式管理面板 + 素材图标库 + 预览显隐/
// 图层管理器: 眼睛开关 / 图标缩略 / 行内改名 / 素材库+媒体库按钮 / 清除
assert . match ( html , /diy-card-layer__eye layui-icon" :class="nc\.actVis && nc\.actVis\[a\.k\] === 0 \? 'layui-icon-eye-invisible' : 'layui-icon-eye'/ , comp + ' 默认动作眼睛开关' ) ;
assert . match ( html , /nc\.actVis\[a\.k\] = nc\.actVis\[a\.k\] === 0 \? 1 : 0;/ , comp + ' 眼睛切换写回 actVis' ) ;
assert . match ( html , /v-model="nc\.actLabels\[a\.k\]" maxlength="6"/ , comp + ' 行内改名 v-model actLabels' ) ;
// 存量组件防御(2026-10-24): defaults($diy_view_utils)缺新字段时 refreshComponent 补不了,
// v-model="nc.actLabels[a.k]" 裸读双层直取会在 render 阶段炸 "reading 'tel'" ——
// 管理器容器 v-if 守卫 + 改名输入 :value/@input 防御式写回 + @click 禁以 if 开头(keyword 编译警告)
assert . match ( html , /v-if="nc\.showQuick && nc\.actLabels && nc\.actIcons"/ , comp + ' 管理器容器 v-if 守卫(未补全不渲染)' ) ;
assert . doesNotMatch ( html , /v-model="nc\.[a-zA-Z]+\[/ , comp + ' 零 v-model 双层裸读(render 阶段防炸)' ) ;
assert . match ( html , /:value="nc\.actLabels \? \(nc\.actLabels\[a\.k\] \|\| ''\) : ''" @input="\$set\(nc, 'actLabels', nc\.actLabels \|\| \{\}\); \$set\(nc\.actLabels, a\.k, \$event\.target\.value\)"/ , comp + ' 行内改名防御式写回($set 兼容存量行)' ) ;
assert . doesNotMatch ( html , /@click="if \(/ , comp + ' 内联事件不以 if 开头(消 JS keyword 编译警告)' ) ;
assert . match ( html , /v-model="nc\.actLabels\[a\.k\]" maxlength="6"|maxlength="6" title="快捷操作文字"/ , comp + ' 改名输入在位' ) ;
assert . match ( html , /js-act-lib" :data-key="a\.k" data-scope="default"/ , comp + ' 素材图标按钮(默认动作)' ) ;
assert . doesNotMatch ( html , /js-act-media/ , comp + ' 媒体库按钮已移除(2.19.1)' ) ;
assert . match ( html , /\$set\(nc\.actIcons\[a\.k\], 'iconKey', ''\)/ , comp + ' 清除图标走 $set(iconKey 兼容存量行)' ) ;
// 自定义动作行: 类型 chips / 取值 / 排序 / 删除 / 添加
assert . match ( html , /diy-card-layer--custom" v-for="\(c, ci\) in nc\.customActs"/ , comp + ' 自定义动作图层行' ) ;
assert . match ( html , /:class="\{ active: c\.type === t\.v \}"/ , comp + ' 自定义动作类型 chips' ) ;
assert . match ( html , /\{v:'copy',lb:'复制'\},\{v:'sms',lb:'短信'\},\{v:'map',lb:'导航'\},\{v:'link',lb:'页面'\}\]/ , comp + ' 类型 chips 含 2.19.3 扩展(短信/导航/页面, web 文案改网址)' ) ;
assert . match ( html , /js-act-syslink" data-scope="custom" :data-idx="ci" v-if="c\.type === 'link'"/ , comp + ' link 类型出现「系统链接」选择按钮' ) ;
assert . match ( html , /v-model="c\.value" maxlength="200"/ , comp + ' 自定义动作取值输入' ) ;
assert . match ( html , /js-act-lib" data-scope="custom" :data-idx="ci"/ , comp + ' 素材图标按钮(自定义动作)' ) ;
assert . match ( html , /nc\.customActs\.splice\(ci - 1, 0, nc\.customActs\.splice\(ci, 1\)\[0\]\)/ , comp + ' 上移排序' ) ;
@@ -536,13 +561,18 @@ test('design.html × 2: 图层式管理面板 + 素材图标库 + 预览显隐/
assert . match ( html , /nc\.customActs\.splice\(ci, 1\)/ , comp + ' 删除自定义动作' ) ;
// 2.19.1 默认动作次序: 面板行按 actOrder 渲染 + ↑↓ 委托调整
assert . equal ( ( html . match ( /<template v-for="k in \(nc\.actOrder && nc\.actOrder\.length \? nc\.actOrder : \['tel','wx','mail','web','save'\]\)"><div class="diy-card-layer"/g ) || [ ] ) . length , 1 , comp + ' 面板默认行按 actOrder 次序渲染(嵌套 v-for + v-if 匹配)' ) ;
// 2.19.2 删除: 行过滤 + 删除按钮 + 恢复 chips + 预览过滤
assert . match ( html , /v-if="a\.k === k && !\(nc\.actDeleted && nc\.actDeleted\[a\.k\] === 1\)"/ , comp + ' 面板行过滤已删除动作' ) ;
assert . match ( html , /diy-card-layer__delact" title="删除此默认操作\(可在下方恢复\)" @click="\$set\(nc, 'actDeleted', nc\.actDeleted \|\| \{ tel: 0, wx: 0, mail: 0, web: 0, save: 0 \}\); \$set\(nc\.actDeleted, a\.k, 1\);"/ , comp + ' 删除按钮防御式写回 actDeleted' ) ;
assert . match ( html , /diy-card-layer__restore" v-if="nc\.actDeleted && \(nc\.actDeleted\.tel === 1/ , comp + ' 恢复 chips 行(有删除项才显示)' ) ;
assert . match ( html , /v-if="nc\.actDeleted\[t\.k\] === 1" :key="t\.k" @click="\$set\(nc\.actDeleted, t\.k, 0\);"/ , comp + ' 恢复 chip 点击写回 0' ) ;
assert . equal ( ( html . match ( /v-if="a\.k === k && !\(nc\.actVis && nc\.actVis\[a\.k\] === 0\) && !\(nc\.actDeleted && nc\.actDeleted\[a\.k\] === 1\)"/g ) || [ ] ) . length , 2 , comp + ' 预览显隐+删除双重过滤(上/下两副本)' ) ;
assert . equal ( ( html . match ( /js-act-move" title="上移" :data-key="a\.k" data-dir="up"/g ) || [ ] ) . length , 1 , comp + ' 默认行上移按钮(委托)' ) ;
assert . equal ( ( html . match ( /js-act-move" title="下移" :data-key="a\.k" data-dir="down"/g ) || [ ] ) . length , 1 , comp + ' 默认行下移按钮(委托)' ) ;
assert . match ( html , /nc\.customActs\.push\(\{ label: '自定义', type: 'copy'[\s\S]*?svg: ''[\s\S]*?vis: 1 \}\)/ , comp + ' 添加自定义操作(全键补齐含 svg)' ) ;
assert . match ( html , /c\.vis = c\.vis === 0 \? 1 : 0;/ , comp + ' 自定义动作眼睛开关' ) ;
// 预览消费: 次序 + 显隐过滤 + 文案覆盖 + 自定义动作单元格
assert . equal ( ( html . match ( /<template v-for="k in \(nc\.actOrder && nc\.actOrder\.length \? nc\.actOrder : \['tel','wx','mail','web','save'\]\)"><template v-for="a in \[\{k:'tel',ch:'打',lb:'打电话'/g ) || [ ] ) . length , 2 , comp + ' 预览默认 cell 按 actOrder 次序渲染(上/下两副本)' ) ;
assert . equal ( ( html . match ( /v-if="a\.k === k && !\(nc\.actVis && nc\.actVis\[a\.k\] === 0\)"/g ) || [ ] ) . length , 2 , comp + ' 预览显隐过滤(嵌套 key 匹配)' ) ;
assert . match ( html , /\{\{ \(nc\.actLabels && nc\.actLabels\[a\.k\]\) \|\| a\.lb \}\}/ , comp + ' 预览文案覆盖' ) ;
// svg 快照口径: 预览只读纯数据字段(委托 JS 选图/改色时预计算 data URI 写入),
// 模板内严禁调用全局函数(模板作用域函数解析不可靠, 实测 typeof 恒 false)
@@ -557,6 +587,15 @@ test('design.html × 2: 图层式管理面板 + 素材图标库 + 预览显隐/
assert . match ( html , /data-field="color"/ , comp + ' 前景色 data-field' ) ;
assert . match ( html , /data-field="bg"/ , comp + ' 底色 data-field' ) ;
assert . match ( html , /data-scope="custom" :data-idx="ci" data-field=/ , comp + ' 自定义行颜色输入带作用域' ) ;
// 2.19.7 面板颜色输入占位必须与预览同链(空值=跟随配色档, 显示诚实):
// 默认行 color: wx#07C160/save#FF9500 固定 → theme#2F6BFF → a.fg; bg: theme#E6EDFF(rgba 叠白近似) → a.bg
const previewColorChain = html . match ( /color: \(nc\.actIcons && nc\.actIcons\[a\.k\] && nc\.actIcons\[a\.k\]\.color\) \|\| \(a\.k === 'wx' \? '#07C160'[^"]*\)/ ) ;
assert . ok ( previewColorChain , comp + ' 预览 color 回落链存在' ) ;
assert . match ( html , /nc\.actIcons\[a\.k\]\.color \? nc\.actIcons\[a\.k\]\.color : \(a\.k === 'wx' \? '#07C160' : a\.k === 'save' \? '#FF9500' : \(nc\.actGridColorMode === 'theme' \? '#2F6BFF' : a\.fg\)\)"/ , comp + ' 默认行前景输入占位与预览同链(wx/save 固定色 + theme/preset 档)' ) ;
assert . match ( html , /nc\.actIcons\[a\.k\]\.bg \? nc\.actIcons\[a\.k\]\.bg : \(nc\.actGridColorMode === 'theme' \? '#E6EDFF' : a\.bg\)"/ , comp + ' 默认行底色输入占位与预览同链(theme 叠白近似/preset 浅底)' ) ;
assert . doesNotMatch ( html , /actIcons\[a\.k\]\.color \? nc\.actIcons\[a\.k\]\.color : '#2F6BFF'/ , comp + ' 默认行前景占位不再恒 #2F6BFF(对多彩档是谎报)' ) ;
assert . match ( html , /:value="c\.color \|\| '#2F6BFF'"/ , comp + ' 自定义行前景占位(与自定义预览回落一致)' ) ;
assert . match ( html , /:value="c\.bg \|\| '#E6EDFF'"/ , comp + ' 自定义行底色占位(rgba 叠白近似)' ) ;
// 结构铁律: 自定义动作 cell 必须在默认动作 template v-for 之外(否则每默认动作重复渲染一次);
// 2.19.1 起默认 cell 为双层嵌套 template(外层 actOrder 次序), 收尾双 </template>
assert . equal ( ( html . match ( /<\/template><\/template><div class="diy-card-mini__act-cell" v-for="\(c, ci\) in nc\.customActs"/g ) || [ ] ) . length , 2 , comp + ' 自定义动作 cell 在双层 template v-for 之外(上/下两副本)' ) ;
@@ -564,16 +603,19 @@ test('design.html × 2: 图层式管理面板 + 素材图标库 + 预览显隐/
assert . doesNotMatch ( html , /<\/div><div class="diy-card-mini__act-cell" v-for="\(c, ci\) in nc\.customActs"/ , comp + ' 自定义动作 cell 不再嵌套于 template v-for 内' ) ;
// template 标签必须配平(未闭合 template 会把后续编辑器内容吞进惰性节点, vue 全局不初始化)
assert . equal ( ( html . match ( /<template/g ) || [ ] ) . length , ( html . match ( /<\/template>/g ) || [ ] ) . length , comp + ' template 标签配平' ) ;
// 资源引用
assert . match ( html , /js src="\{\$resource_path\}\/\.\.\/public\/js\/card-icon-lib\.js"/ , comp + ' 引入素材图标库 JS' ) ;
assert . match ( html , /js src="\{\$resource_path\}\/\.\.\/public\/js\ /card-act-icon\.js"/ , comp + ' 引入图标选择委托 JS ') ;
// 资源引用(2026-10-24 起带 ?v={$version} 破生产浏览器/CDN 缓存 —— 同名 CSS/JS
// 旧缓存曾致生产编辑器面板图层行与预览宫格样式全丢、竖排裸文字)
for ( const r of [ 'css/card-select\\.css' , 'css/card-panel\\.css' , 'css/card-preview\\.css' , 'js/card-select\\.js' , 'js/card-icon-lib\\.js' , 'js /card-act-icon\\ .js'] ) {
assert . match ( html , new RegExp ( 'src="\\{\\$resource_path\\}/\\.\\./public/' + r + '\\?v=\\{\\$version\\}"' ) , comp + ' 资源引用带版本参数: ' + r ) ;
}
assert . doesNotMatch ( html , /public\/(css|js)\/card-[a-z-]+\.(css|js)"/ , comp + ' 零无版本资源引用' ) ;
}
} ) ;
test ( 'card-act-icon.js: 类名委托修 2.18 选图标 Bug + $set 写回 + 素材库唯一图标来源 + 次序调整(2.19+2.19.1)' , ( ) => {
const src = readFileSync ( resolve ( BE , 'component/view/public/js/card-act-icon.js' ) , 'utf8' ) ;
assert . match ( src , /on\('click', '\.js-act-lib'/ , '素材图标库走类名委托' ) ;
assert . doesNotMatch ( src , /js-act-media|openAlbum\(function / , '媒体库委托已整体 移除(2.19.1 图标唯一来源=素材图标库)' ) ;
assert . doesNotMatch ( src , /js-act-media/ , '快捷操作 媒体库委托已移除(2.19.1 图标唯一来源=素材图标库; QR 选图的 js-qr-media/openAlbum 为 2.19.3 合法新增 )' ) ;
// 2.19.1 默认动作次序调整
assert . match ( src , /on\('click', '\.js-act-move'/ , '次序调整走类名委托' ) ;
assert . match ( src , /var ACT_ORDER_DEF = \['tel', 'wx', 'mail', 'web', 'save'\];/ , 'actOrder 白名单默认序' ) ;
@@ -592,9 +634,32 @@ test('card-act-icon.js: 类名委托修 2.18 选图标 Bug + $set 写回 + 素
assert . match ( src , /iconKey: '', svg: '', color: ''/ , '缺行防御补齐含 svg 键' ) ;
assert . match ( src , /window\.CARD_ICON_LIB \|\| \{\}/ , '弹层渲染消费同源图标库' ) ;
assert . match ( src , /cal-item" data-key=/ , '弹层图标网格' ) ;
// 2.19.3 系统链接对话框委托(link 类型)
assert . match ( src , /on\('click', '\.js-act-syslink'/ , '系统链接走类名委托' ) ;
assert . match ( src , /ns\.select_link\(\{\}, function \(data\)/ , '调全局 ns.select_link 打开系统链接对话框' ) ;
assert . match ( src , /data\.wap_url \|\| data\.url/ , '回执取 wap_url 写入 value' ) ;
assert . match ( src , /window\.vue\.\$set\(target\.item, 'value', url\)/ , '系统链接结果 $set 写回 value' ) ;
// 2.19.3 自定义二维码选图委托(模板内联 openAlbum 不可靠, 2.18 同坑收尾)
assert . match ( src , /on\('click', '\.js-qr-media'/ , '二维码选图走类名委托' ) ;
assert . match ( src , /window\.vue\.\$set\(comp, 'qrImage', obj\[0\]\.pic_path\)/ , '媒体库回执 $set 写回 qrImage' ) ;
// 反例: 面板不再内联调 openAlbum 选快捷操作图标(2.18 Bug 根因规避)
for ( const comp of [ 'show' , 'self' ] ) {
const html = readFileSync ( resolve ( BE , 'component/view/diy-card-' + comp + '/design.html' ) , 'utf8' ) ;
assert . doesNotMatch ( html , /openAlbum\(function \(obj\) \{ if \(obj && obj\.length\) \{ if \(!nc\.actIcons\)/ , comp + ' 面板不再内联调 openAlbum 写 actIcons' ) ;
}
} ) ;
test ( 'card-preview.css: 默认线稿图标 mask+currentColor —— 前景色实时跟随(2.19.6)' , ( ) => {
const css = readFileSync ( resolve ( BE , 'component/view/public/css/card-preview.css' ) , 'utf8' ) ;
// 5 个默认线稿类必须走 mask-image(颜色与图分离) + background-color: currentColor
for ( const k of [ 'tel' , 'wx' , 'mail' , 'web' , 'save' ] ) {
const m = css . match ( new RegExp ( '\\.diy-card-mini__act-svg--' + k + '\\s*\\{[^}]*\\}' ) ) ;
assert . ok ( m , '线稿类存在: ' + k ) ;
const block = m [ 0 ] ;
assert . match ( block , /mask-image:\s*url\("data:image\/svg\+xml/ , k + ' 走 mask-image(非固定色 background-image)' ) ;
assert . match ( block , /-webkit-mask-image:/ , k + ' 带 -webkit 前缀' ) ;
assert . match ( block , /background-color:\s*currentColor/ , k + ' 背景消费 currentColor(跟随内联 color)' ) ;
assert . doesNotMatch ( block , /background-image:/ , k + ' 不再用固定色 background-image(颜色编码在图内改色无效)' ) ;
}
} ) ;