402 lines
20 KiB
JavaScript
402 lines
20 KiB
JavaScript
/**
|
|
* card 电子名片模块跨平台适配测试: 微信小程序 / H5 / 鸿蒙元服务
|
|
*
|
|
* 背景:
|
|
* uni-app 产物覆盖 MP-WEIXIN / H5 / MP-HARMONY(鸿蒙元服务) 三端,
|
|
* card 模块(名片夹/详情/编辑/线索/分享)任何平台分支遗漏都会在对应端
|
|
* 表现为「按钮消失」或「整页白屏」。本测试把适配契约固化为断言。
|
|
*
|
|
* 覆盖:
|
|
* 1. 条件编译配对平衡(防止 #ifdef 漏 #endif 导致编译失败)
|
|
* 2. 平台 token 覆盖(detail/share 必须同时照顾三端)
|
|
* 3. 保存二维码按钮在 微信+鸿蒙 双端可见(saveImageToPhotosAlbum 鸿蒙可用)
|
|
* 4. share.vue 无白页: web-view 分支覆盖 微信+鸿蒙+App, H5 分支有跳转
|
|
* 5. goBack 不用裸 uni.switchTab(鸿蒙 switchTab 静默失败, catch 兜底失效,
|
|
* 必须走 $util.redirectTo —— 见 common/js/util.js#redirectTo tabBar 分支)
|
|
* 6. 分享 URL 编解码闭环(三端语义不同: 微信/鸿蒙不 decode 值, H5 预 decode)
|
|
* 7. 平台敏感 API 只在受保护的分支内调用
|
|
* 8. card_id 参数编码安全(纯 ASCII, encodeURIComponent 幂等)
|
|
*
|
|
* 跑法:
|
|
* node --test tests/card-platform.test.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const CARD_DIR = path.resolve(__dirname, '../pages_tool/card');
|
|
const read = f => fs.readFileSync(path.join(CARD_DIR, f), 'utf8');
|
|
|
|
// =============================================================
|
|
// 工具: 条件编译块解析(行注释 // / 块注释 /* */ / HTML <!-- --> 三种写法)
|
|
// =============================================================
|
|
|
|
/**
|
|
* 把文件解析成 [{ cond, start, end }] 顶层块列表。
|
|
* 兼容三种写法: 行注释 `// #ifdef X`、块注释 `/* #ifdef X *\/`、HTML `<!-- #ifdef X -->`。
|
|
* 关键细节:
|
|
* - 条件名含连字符(MP-WEIXIN/APP-PLUS), 捕获到注释终止符为止
|
|
* - 一行可内联 "#ifdef X -->text<!-- #endif -->", 开/关都按出现次数计
|
|
*/
|
|
function parseIfdefBlocks(src) {
|
|
const lines = src.split('\n');
|
|
const stack = []; // [{ cond, startLine }]
|
|
const blocks = []; // 顶层块
|
|
// 开标记: 捕获条件串(惰性到注释终止符/行尾); 关标记: 单独识别
|
|
const TOKEN_RE = /(?:\/\/\s*|\/\*\s*|<!--\s*)(#ifdef|#ifndef|#endif)\b(?:\s+(.+?))?\s*(?:-->|\*\/|$)/g;
|
|
lines.forEach((line, i) => {
|
|
TOKEN_RE.lastIndex = 0;
|
|
let m;
|
|
const tokens = [];
|
|
while ((m = TOKEN_RE.exec(line)) !== null) {
|
|
if (m[0].trim() === '') break; // 防空匹配死循环
|
|
tokens.push({ kind: m[1], cond: m[2] ? m[2].trim() : null });
|
|
}
|
|
for (const tk of tokens) {
|
|
if (tk.kind === '#ifdef' || tk.kind === '#ifndef') {
|
|
if (stack.length === 0) blocks.push({ cond: tk.cond, start: i, end: lines.length - 1 });
|
|
stack.push({ cond: tk.cond, startLine: i });
|
|
} else {
|
|
const top = stack.pop();
|
|
if (top && stack.length === 0) {
|
|
const b = blocks[blocks.length - 1];
|
|
if (b && b.start === top.startLine) b.end = i;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return blocks;
|
|
}
|
|
|
|
/** 判断 cond 串(如 "MP-WEIXIN || MP-HARMONY")在指定平台下是否命中 */
|
|
function condHits(cond, platform) {
|
|
return cond
|
|
.split('||')
|
|
.map(s => s.trim().replace(/^!/, ''))
|
|
.includes(platform);
|
|
}
|
|
|
|
/** 找出源码中某 API 出现的所有位置及其所属顶层 cond(无保护返回 null) */
|
|
function apiOccurrences(src, apiRegex) {
|
|
const lines = src.split('\n');
|
|
const blocks = parseIfdefBlocks(src);
|
|
const out = [];
|
|
lines.forEach((line, i) => {
|
|
if (!apiRegex.test(line)) return;
|
|
// 找包含该行的顶层块
|
|
const b = blocks.find(b => i >= b.start && i <= b.end);
|
|
out.push({ line: i + 1, text: line.trim(), cond: b ? b.cond : null });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
// =============================================================
|
|
// 1. 条件编译配对平衡
|
|
// =============================================================
|
|
|
|
for (const f of ['list.vue', 'detail.vue', 'edit.vue', 'leads.vue', 'share.vue']) {
|
|
test(`ifdef 配对平衡: ${f}`, () => {
|
|
const src = read(f);
|
|
const open = (src.match(/#ifdef|#ifndef/g) || []).length;
|
|
const close = (src.match(/#endif/g) || []).length;
|
|
assert.equal(open, close, `${f}: #ifdef/#ifndef=${open} 但 #endif=${close}(漏配对编译必失败)`);
|
|
});
|
|
}
|
|
|
|
// =============================================================
|
|
// 2. 平台 token 覆盖
|
|
// =============================================================
|
|
|
|
test('平台覆盖: detail.vue 同时含 MP-WEIXIN / H5 / MP-HARMONY 分支', () => {
|
|
const src = read('detail.vue');
|
|
for (const p of ['MP-WEIXIN', 'H5', 'MP-HARMONY']) {
|
|
assert.ok(src.includes(p), `detail.vue 缺少 ${p} 分支(该端分享面板会缺按钮)`);
|
|
}
|
|
});
|
|
|
|
test('平台覆盖: share.vue 的 web-view 分支含 MP-HARMONY(防鸿蒙白屏)', () => {
|
|
const src = read('share.vue');
|
|
// web-view 必须出现在 MP-WEIXIN || MP-HARMONY 保护块内
|
|
const wvOcc = apiOccurrences(src, /<web-view/);
|
|
assert.ok(wvOcc.length >= 2, 'share.vue 至少应有 2 处 web-view(小程序+App)');
|
|
const mpWv = wvOcc.find(o => o.cond && /MP-WEIXIN/.test(o.cond));
|
|
assert.ok(mpWv, 'web-view 必须位于 MP-WEIXIN 分支内');
|
|
assert.ok(/MP-HARMONY/.test(mpWv.cond),
|
|
`web-view 分支必须同时覆盖 MP-HARMONY, 实际 cond="${mpWv.cond}"(鸿蒙会白页)`);
|
|
});
|
|
|
|
test('平台覆盖: share.vue H5 分支有 location.href 跳转(防 H5 停留中转页)', () => {
|
|
const src = read('share.vue');
|
|
const occ = apiOccurrences(src, /window\.location\.href/);
|
|
const h5Occ = occ.find(o => o.cond === 'H5');
|
|
assert.ok(h5Occ, 'location.href 必须位于 H5 分支内');
|
|
});
|
|
|
|
// =============================================================
|
|
// 3. 保存二维码按钮双端可见
|
|
// =============================================================
|
|
|
|
test('保存二维码: 按钮在 MP-WEIXIN 与 MP-HARMONY 分支均可见', () => {
|
|
const src = read('detail.vue');
|
|
// saveQr 的按钮绑定点(template 内 @tap="saveQr")
|
|
const btnOcc = apiOccurrences(src, /@tap="saveQr"/);
|
|
const conds = btnOcc.map(o => o.cond).filter(Boolean);
|
|
assert.ok(conds.some(c => /MP-WEIXIN/.test(c)), '保存二维码按钮应有 MP-WEIXIN 分支');
|
|
assert.ok(conds.some(c => /MP-HARMONY/.test(c)),
|
|
`保存二维码按钮应有 MP-HARMONY 分支(鸿蒙 uni.saveImageToPhotosAlbum 可用), 实际: ${JSON.stringify(conds)}`);
|
|
});
|
|
|
|
test('保存二维码: saveImageToPhotosAlbum/downloadFile 调用不受平台分支保护误删', () => {
|
|
// 这两个 API 在鸿蒙受支持, 但不能出现在 H5 分支( H5 无此 API )
|
|
const src = read('detail.vue');
|
|
for (const api of [/uni\.saveImageToPhotosAlbum/, /uni\.downloadFile/]) {
|
|
for (const occ of apiOccurrences(src, api)) {
|
|
assert.notEqual(occ.cond, 'H5', `${api} 不应在 H5 分支内(第 ${occ.line} 行)`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// =============================================================
|
|
// 4. 分享中转页不白屏(各端都有可渲染内容)
|
|
// =============================================================
|
|
|
|
test('share.vue 三端均有渲染分支: MP-WEIXIN||MP-HARMONY / H5 / APP-PLUS', () => {
|
|
const src = read('share.vue');
|
|
const conds = parseIfdefBlocks(src).map(b => b.cond);
|
|
assert.ok(conds.some(c => c && /MP-WEIXIN/.test(c) && /MP-HARMONY/.test(c)),
|
|
`应有合并的 MP-WEIXIN||MP-HARMONY 分支, 实际: ${JSON.stringify(conds)}`);
|
|
assert.ok(conds.includes('H5'), '应有 H5 分支');
|
|
assert.ok(conds.includes('APP-PLUS'), '应有 APP-PLUS 分支');
|
|
});
|
|
|
|
// =============================================================
|
|
// 5. goBack 不用裸 switchTab(鸿蒙静默失败回归)
|
|
// =============================================================
|
|
|
|
test('goBack: share.vue 不用裸 uni.switchTab(鸿蒙静默失败, catch 不触发)', () => {
|
|
const src = read('share.vue');
|
|
// 提取 goBack 方法体
|
|
const m = src.match(/goBack\(\)\s*\{([\s\S]*?)\n\t\t\},/);
|
|
assert.ok(m, '应能提取 goBack 方法体');
|
|
const body = m[1];
|
|
// 只断言真实调用(带括号), 注释里提到 uni.switchTab 是说明文字不算
|
|
assert.ok(!/uni\.switchTab\s*\(/.test(body),
|
|
'goBack 不得直接调 uni.switchTab: 鸿蒙上 success/fail 都不触发, .catch 兜底失效');
|
|
assert.ok(/\$util\.redirectTo/.test(body),
|
|
'goBack 栈底回首页必须走 $util.redirectTo(内部有 switchTab→500ms→reLaunch 兜底)');
|
|
});
|
|
|
|
// =============================================================
|
|
// 6. 分享 URL 编解码闭环(三端语义)
|
|
// =============================================================
|
|
|
|
/**
|
|
* 模拟三端 onShareAppMessage path → 目标页 onLoad options.url 的传递:
|
|
* - 微信: path 里的 query 按 & = 切分但不 decode value(项目实测结论,
|
|
* 见 common/js/util-navigate.js#_safeEncodeForPlatform 注释)
|
|
* - 鸿蒙: 同微信语义(uni-app harmony 沿用小程序路由)
|
|
* - H5: 浏览器预 decode, onLoad 拿到已解码值
|
|
*/
|
|
function simulateShareOpen(pathStr, platform) {
|
|
const qi = pathStr.indexOf('?');
|
|
const query = qi === -1 ? '' : pathStr.slice(qi + 1);
|
|
const params = {};
|
|
for (const kv of query.split('&')) {
|
|
if (!kv) continue;
|
|
const eq = kv.indexOf('=');
|
|
const k = kv.slice(0, eq);
|
|
const raw = kv.slice(eq + 1);
|
|
// 微信/鸿蒙: value 原样(不 decode); H5: 浏览器已 decode
|
|
params[k] = platform === 'h5' ? safeDecode(raw) : raw;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
function safeDecode(s) {
|
|
try { return decodeURIComponent(s); } catch (e) { return s; }
|
|
}
|
|
|
|
test('分享闭环: 微信/鸿蒙 onLoad 拿到编码值, decode 后还原完整 URL', () => {
|
|
const shareUrl = 'https://shop.example.com/card/wap/index/detail?site_id=1&id=237';
|
|
const pathStr = '/pages_tool/card/share?url=' + encodeURIComponent(shareUrl);
|
|
for (const p of ['mp-weixin', 'mp-harmony']) {
|
|
const opts = simulateShareOpen(pathStr, p);
|
|
assert.ok(opts.url, `${p}: options.url 应存在`);
|
|
// 编码值不应含裸 & (否则 query 被切碎, url 被截断)
|
|
assert.ok(!opts.url.includes('&'), `${p}: url 参数不应含裸 &`);
|
|
// share.vue onLoad 的 decodeURIComponent 必须还原出完整 URL
|
|
assert.equal(safeDecode(opts.url), shareUrl, `${p}: decode 后应还原完整分享 URL`);
|
|
}
|
|
});
|
|
|
|
test('分享闭环: H5 onLoad 拿到已解码值, 再 decode 是幂等的(无双解码)', () => {
|
|
const shareUrl = 'https://shop.example.com/card/wap/index/detail?site_id=1&id=237';
|
|
const pathStr = '/pages_tool/card/share?url=' + encodeURIComponent(shareUrl);
|
|
const opts = simulateShareOpen(pathStr, 'h5');
|
|
assert.equal(safeDecode(opts.url), shareUrl, 'H5: 浏览器已解码, 页面内再 decode 应仍得完整 URL');
|
|
});
|
|
|
|
test('分享闭环: onShareAppMessage 的 path 确实预编码了 url(与 webview.vue 惯例一致)', () => {
|
|
const src = read('detail.vue');
|
|
assert.ok(/path:\s*'\/pages_tool\/card\/share\?url='\s*\+\s*encodeURIComponent/.test(src),
|
|
'onShareAppMessage path 必须 encodeURIComponent(url), 否则 & 会切碎 query');
|
|
});
|
|
|
|
// =============================================================
|
|
// 7. card_id 参数编码安全
|
|
// =============================================================
|
|
|
|
test('card_id 编码幂等: 纯 ASCII 编号 encodeURIComponent 不变(双编码无害)', () => {
|
|
// 服务端生成格式 c_YYYYMMDD_hex, 只含字母数字下划线
|
|
const cardId = 'c_20261012_f1018de954e6';
|
|
assert.equal(encodeURIComponent(cardId), cardId, 'card_id 编码后应不变');
|
|
assert.equal(decodeURIComponent(encodeURIComponent(cardId)), cardId, '双解码也应无害');
|
|
});
|
|
|
|
// =============================================================
|
|
// 8. member/index.vue 的 tourl 导航走 $util.redirectTo(鸿蒙安全)
|
|
// =============================================================
|
|
|
|
test('member/index.vue 的 tourl 走 $util.redirectTo(鸿蒙安全)', () => {
|
|
const memberSrc = fs.readFileSync(
|
|
path.resolve(__dirname, '../pages_tool/member/index.vue'), 'utf8');
|
|
const m = memberSrc.match(/tourl\(url\)\s*\{([\s\S]*?)\n\t\t\}/);
|
|
assert.ok(m, 'member/index.vue 应定义 tourl');
|
|
assert.ok(/\$util\.redirectTo/.test(m[1]),
|
|
'tourl 必须走 $util.redirectTo(鸿蒙 switchTab 静默失败兜底在内)');
|
|
});
|
|
|
|
// =============================================================
|
|
// 9. 端到端: 三端分享面板按钮矩阵
|
|
// =============================================================
|
|
|
|
test('端到端: 三端分享面板按钮矩阵(静态推导)', () => {
|
|
const src = read('detail.vue');
|
|
// 复制链接: 无条件(三端都有)
|
|
assert.ok(/@tap="copyLink"/.test(src), '复制链接应三端可用');
|
|
// 保存二维码: MP-WEIXIN + MP-HARMONY
|
|
// 浏览器分享: H5
|
|
// 发给好友: MP-WEIXIN(鸿蒙无转发菜单, 用复制链接代替)
|
|
const h5Btns = [];
|
|
const wxBtns = [];
|
|
const harmonyBtns = [];
|
|
// 简化推导: 逐行扫描按钮行并记录当前 cond
|
|
const blocks = parseIfdefBlocks(src);
|
|
src.split('\n').forEach((line, i) => {
|
|
const b = blocks.find(b => i >= b.start && i <= b.end);
|
|
const cond = b ? b.cond : null;
|
|
const tm = line.match(/@tap="(\w+)"/);
|
|
if (!tm || !/ss-btn/.test(line)) return;
|
|
if (!cond) { h5Btns.push(tm[1]); wxBtns.push(tm[1]); harmonyBtns.push(tm[1]); return; }
|
|
if (condHits(cond, 'MP-WEIXIN')) wxBtns.push(tm[1]);
|
|
if (condHits(cond, 'MP-HARMONY')) harmonyBtns.push(tm[1]);
|
|
if (condHits(cond, 'H5')) h5Btns.push(tm[1]);
|
|
});
|
|
assert.ok(wxBtns.includes('copyLink') && wxBtns.includes('saveQr') && wxBtns.includes('shareToFriend'),
|
|
`微信按钮矩阵: ${JSON.stringify(wxBtns)}`);
|
|
assert.ok(harmonyBtns.includes('copyLink') && harmonyBtns.includes('saveQr'),
|
|
`鸿蒙按钮矩阵(无发给好友): ${JSON.stringify(harmonyBtns)}`);
|
|
assert.ok(!harmonyBtns.includes('shareToFriend'), '鸿蒙不应出现「发给好友」(无转发菜单)');
|
|
assert.ok(h5Btns.includes('copyLink') && h5Btns.includes('h5Share'),
|
|
`H5 按钮矩阵: ${JSON.stringify(h5Btns)}`);
|
|
assert.ok(!h5Btns.includes('saveQr'), 'H5 不应出现「保存二维码」(无 saveImageToPhotosAlbum)');
|
|
});
|
|
|
|
// ============================================================
|
|
// 1.8.0 后台「选择链接」注册: PAGE_KEY_MAP ↔ pages.json 对齐
|
|
//
|
|
// 后端 lucky_link 注册 CARD_LIST/CARD_LEADS(page_key=PAGE_CARD_LIST/
|
|
// PAGE_CARD_LEADS), 前端导航 resolveLinkUrl 优先用 page_key 经
|
|
// PAGE_KEY_MAP 解析; 两边必须与 pages.json 注册路径一致, 否则链接点了 404。
|
|
// ============================================================
|
|
test('选择链接: PAGE_KEY_MAP 含 card 两个页面常量', () => {
|
|
const utilSrc = fs.readFileSync(path.resolve(__dirname, '../common/js/util.js'), 'utf8');
|
|
const m = utilSrc.match(/PAGE_CARD_LIST:\s*'([^']+)'/);
|
|
const m2 = utilSrc.match(/PAGE_CARD_LEADS:\s*'([^']+)'/);
|
|
assert.ok(m, 'PAGE_KEY_MAP 应有 PAGE_CARD_LIST');
|
|
assert.ok(m2, 'PAGE_KEY_MAP 应有 PAGE_CARD_LEADS');
|
|
assert.equal(m[1], '/pages_tool/card/list');
|
|
assert.equal(m2[1], '/pages_tool/card/leads');
|
|
});
|
|
|
|
test('选择链接: card 页面已在 pages.json 注册', () => {
|
|
const pagesSrc = fs.readFileSync(path.resolve(__dirname, '../pages.json'), 'utf8');
|
|
// pages.json 含 uni-app 合法 // 注释, 剥后解析
|
|
const stripped = pagesSrc.replace(/^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
|
|
const pages = JSON.parse(stripped);
|
|
const sub = (pages.subPackages || []).find(sp => sp.root === 'pages_tool');
|
|
assert.ok(sub, 'pages_tool 分包应存在');
|
|
const paths = (sub.pages || []).map(p => p.path);
|
|
assert.ok(paths.includes('card/list'), `card/list 未注册: ${paths.join(',')}`);
|
|
assert.ok(paths.includes('card/leads'), `card/leads 未注册: ${paths.join(',')}`);
|
|
});
|
|
|
|
test('选择链接: card 页面不在 TabBar(NO_TAB_PAGES 语义)不影响导航兜底', () => {
|
|
// card 页不是 tabBar 根页, redirectTo 走 navigateTo 分支;
|
|
// 此处仅防回归: 确认 util.js 的 TAB_BAR_KEYS 未被误加 card 路径
|
|
const utilSrc = fs.readFileSync(path.resolve(__dirname, '../common/js/util.js'), 'utf8');
|
|
const tabBlock = utilSrc.match(/TAB_BAR_KEYS\s*=\s*\{[\s\S]*?\}/);
|
|
assert.ok(tabBlock, 'TAB_BAR_KEYS 应存在');
|
|
assert.ok(!/card/.test(tabBlock[0]), 'TAB_BAR_KEYS 不应含 card 路径(card 非 tabBar 页)');
|
|
});
|
|
|
|
// ============================================================
|
|
// 2.0.0 人脉库: contacts / contact-edit 注册 + 互链 + 鸿蒙安全
|
|
// ============================================================
|
|
|
|
test('人脉库: PAGE_KEY_MAP 含 PAGE_CARD_CONTACTS', () => {
|
|
const utilSrc = fs.readFileSync(path.resolve(__dirname, '../common/js/util.js'), 'utf8');
|
|
const m = utilSrc.match(/PAGE_CARD_CONTACTS:\s*'([^']+)'/);
|
|
assert.ok(m, 'PAGE_KEY_MAP 应有 PAGE_CARD_CONTACTS');
|
|
assert.equal(m[1], '/pages_tool/card/contacts');
|
|
});
|
|
|
|
test('人脉库: contacts/contact-edit 已注册进 pages.json 且不在 TabBar', () => {
|
|
const pagesSrc = fs.readFileSync(path.resolve(__dirname, '../pages.json'), 'utf8');
|
|
const stripped = pagesSrc.replace(/^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
|
|
const pages = JSON.parse(stripped);
|
|
const sub = (pages.subPackages || []).find(sp => sp.root === 'pages_tool');
|
|
assert.ok(sub, 'pages_tool 分包应存在');
|
|
const paths = (sub.pages || []).map(p => p.path);
|
|
assert.ok(paths.includes('card/contacts'), `card/contacts 未注册: ${paths.join(',')}`);
|
|
assert.ok(paths.includes('card/contact-edit'), `card/contact-edit 未注册: ${paths.join(',')}`);
|
|
// TabBar 根页不在 pages_tool 分包, 天然不在 TabBar; 防回归: tabBar 列表不得含 card
|
|
const tabPages = (pages.tabBar && pages.tabBar.list ? pages.tabBar.list : []).map(p => p.pagePath);
|
|
assert.ok(!tabPages.some(p => /card/.test(p)), 'TabBar 不应含 card 页面');
|
|
});
|
|
|
|
test('人脉库: contacts/leads 顶部互链都走 $util.redirectTo(鸿蒙静默失败回归)', () => {
|
|
const contactsSrc = read('contacts.vue');
|
|
const leadsSrc = read('leads.vue');
|
|
// contacts → leads
|
|
assert.ok(/goLeads\(\)\s*\{[\s\S]*?\$util\.redirectTo\('\/pages_tool\/card\/leads'\)/.test(contactsSrc),
|
|
'contacts.vue 顶部「潜在客户」链接必须走 $util.redirectTo');
|
|
// leads → contacts
|
|
assert.ok(/goContacts\(\)\s*\{[\s\S]*?\$util\.redirectTo\('\/pages_tool\/card\/contacts'\)/.test(leadsSrc),
|
|
'leads.vue 顶部「我的人脉」链接必须走 $util.redirectTo');
|
|
// 两文件不得出现裸 uni.switchTab 调用
|
|
for (const [name, src] of [['contacts.vue', contactsSrc], ['leads.vue', leadsSrc]]) {
|
|
assert.ok(!/uni\.switchTab\s*\(/.test(src), `${name} 不得直接调 uni.switchTab`);
|
|
}
|
|
});
|
|
|
|
test('人脉库: contact-edit 保存成功回 contacts 走 $util.redirectTo(鸿蒙安全)', () => {
|
|
const src = read('contact-edit.vue');
|
|
// backToList 按来源分支: 员工端缺省回 /pages_tool/card/contacts,
|
|
// 商家端(from=admin)回 /pages_tool/card/admin/contacts(见 card-admin.test.js)
|
|
const m = src.match(/backToList\(\)\s*\{([\s\S]*?)\n\t\t\}/);
|
|
assert.ok(m, '应能提取 backToList 方法体');
|
|
assert.ok(/\$util\.redirectTo/.test(m[1]),
|
|
'保存成功回列表必须走 $util.redirectTo');
|
|
assert.ok(m[1].includes("'/pages_tool/card/contacts'"),
|
|
'员工端缺省应回 /pages_tool/card/contacts');
|
|
assert.ok(!/uni\.switchTab\s*\(/.test(src), 'contact-edit.vue 不得直接调 uni.switchTab');
|
|
// 姓名必填: 模板有 req-star + 保存前有空名校验
|
|
assert.ok(/req-star/.test(src), '姓名必填应有 req-star 红星');
|
|
assert.ok(/请填写姓名/.test(src), '保存前应校验姓名非空');
|
|
});
|