Files
lucky_shop/tests/card-admin.test.js

187 lines
8.5 KiB
JavaScript

/**
* 名片云 · 商家端(admin)页面测试
*
* 背景:
* 商家端 7 个页面中, 后台接口先就绪的是 contact/* 与 lead/*(以及 card/lists),
* 因此本轮先上三个页面(其余 5 页等后台接口, 见 CARD_API_REFERENCE.md 缺口):
* - admin/manage 名片管理(基础版: 搜索/详情/编辑, 详情编辑复用员工端页)
* - admin/contacts 人脉管理(列表/搜索/来源筛选/编辑/转线索/删除)
* - admin/leads 潜在客户(状态筛选/列表/更新跟进状态)
* 本测试固化商家端契约: 注册/收口/鸿蒙安全导航/复用回跳。
*
* 跑法:
* node --test tests/card-admin.test.js
*/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const ADMIN_DIR = path.join(ROOT, 'pages_tool/card/admin');
const readAdmin = f => fs.readFileSync(path.join(ADMIN_DIR, f), 'utf8');
const readCard = f => fs.readFileSync(path.join(ROOT, 'pages_tool/card', f), 'utf8');
const pagesSrc = fs.readFileSync(path.join(ROOT, 'pages.json'), 'utf8');
const utilSrc = fs.readFileSync(path.join(ROOT, 'common/js/util.js'), 'utf8');
// pages.json 含 uni-app 合法 // 注释, 剥后解析
const stripped = pagesSrc.replace(/^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
const pagesJson = JSON.parse(stripped);
const ADMIN_PAGES = {
'card/admin/manage': '名片管理',
'card/admin/contacts': '人脉管理',
'card/admin/leads': '潜在客户'
};
/** 取 pages_tool 分包已注册路径 */
function toolPaths() {
const sub = (pagesJson.subPackages || []).find(sp => sp.root === 'pages_tool');
assert.ok(sub, 'pages_tool 分包应存在');
return (sub.pages || []).map(p => p.path);
}
// =============================================================
// 1. 页面注册
// =============================================================
test('注册: 商家端 3 页已注册进 pages.json 的 pages_tool 分包', () => {
const paths = toolPaths();
for (const p of Object.keys(ADMIN_PAGES)) {
assert.ok(paths.includes(p), `${p} 未注册: ${paths.join(',')}`);
}
});
test('注册: 商家端页面标题正确且不在 TabBar', () => {
const sub = (pagesJson.subPackages || []).find(sp => sp.root === 'pages_tool');
for (const [p, title] of Object.entries(ADMIN_PAGES)) {
const page = (sub.pages || []).find(x => x.path === p);
assert.ok(page, `${p} 应存在`);
assert.equal(page.style.navigationBarTitleText, title, `${p} 标题应为「${title}」`);
// 名片云全部页面都不是 tabBar 根页
assert.ok(page.style.enablePullDownRefresh === true, `${p} 应开启下拉刷新`);
}
const tabPages = (pagesJson.tabBar && pagesJson.tabBar.list ? pagesJson.tabBar.list : []).map(p => p.pagePath);
for (const p of Object.keys(ADMIN_PAGES)) {
assert.ok(!tabPages.some(tp => tp.includes(p)), `TabBar 不应含 ${p}`);
}
});
test('注册: PAGE_KEY_MAP 含商家端 3 页且与 pages.json 一致', () => {
const map = {
PAGE_CARD_ADMIN_MANAGE: '/pages_tool/card/admin/manage',
PAGE_CARD_ADMIN_CONTACTS: '/pages_tool/card/admin/contacts',
PAGE_CARD_ADMIN_LEADS: '/pages_tool/card/admin/leads'
};
for (const [key, val] of Object.entries(map)) {
const m = utilSrc.match(new RegExp(`${key}:\\s*'([^']+)'`));
assert.ok(m, `PAGE_KEY_MAP 应有 ${key}`);
assert.equal(m[1], val);
assert.ok(toolPaths().includes(val.slice('/pages_tool/'.length)),
`${val} 未在 pages.json 注册`);
}
// 防回归: TAB_BAR_KEYS 不得被误加 admin 路径
const tabBlock = utilSrc.match(/TAB_BAR_KEYS\s*=\s*\{[\s\S]*?\}/);
assert.ok(tabBlock, 'TAB_BAR_KEYS 应存在');
assert.ok(!/admin/.test(tabBlock[0]), 'TAB_BAR_KEYS 不应含 admin 路径');
});
// =============================================================
// 2. API 收口(与 card-api 测试同一契约, 覆盖新页面)
// =============================================================
test('收口: 商家端 3 页零 /card/api/ 字面量且走 this.$cardApi', () => {
for (const f of ['manage.vue', 'contacts.vue', 'leads.vue']) {
const src = readAdmin(f);
assert.ok(!/['"]\/card\/api\//.test(src), `${f} 仍直连 /card/api/ 字面量`);
assert.ok(/this\.\$cardApi\./.test(src), `${f} 未使用 this.$cardApi`);
}
});
test('收口: 商家端页面从 card-api.js 导入共享字典', () => {
assert.ok(/import\s*\{\s*CONTACT_SOURCES\s*\}\s*from\s*'@\/common\/js\/card-api\.js'/.test(readAdmin('contacts.vue')),
'contacts.vue 应导入 CONTACT_SOURCES');
assert.ok(/import\s*\{\s*LEAD_STATUS\s*\}\s*from\s*'@\/common\/js\/card-api\.js'/.test(readAdmin('leads.vue')),
'leads.vue 应导入 LEAD_STATUS');
});
test('收口: leads.vue 的 setStatus 只接受 0-3(后端约束)', () => {
const src = readAdmin('leads.vue');
// ActionSheet 选项来自 LEAD_STATUS(0-3), 不产生越界值
assert.ok(/itemList:\s*LEAD_STATUS\.map/.test(src), '状态选项必须来自 LEAD_STATUS 字典');
assert.ok(!/status\s*=\s*[4-9]/.test(src), '不得出现 0-3 之外的 status 值');
});
// =============================================================
// 3. 鸿蒙安全导航(禁裸 uni.switchTab 静默失败)
// =============================================================
test('鸿蒙: 商家端 3 页平级互链全部走 $util.redirectTo', () => {
const cross = {
'manage.vue': [['goContacts', '/pages_tool/card/admin/contacts'], ['goLeads', '/pages_tool/card/admin/leads']],
'contacts.vue': [['goManage', '/pages_tool/card/admin/manage'], ['goLeads', '/pages_tool/card/admin/leads']],
'leads.vue': [['goManage', '/pages_tool/card/admin/manage'], ['goContacts', '/pages_tool/card/admin/contacts']]
};
for (const [f, pairs] of Object.entries(cross)) {
const src = readAdmin(f);
for (const [method, target] of pairs) {
const m = src.match(new RegExp(`${method}\\(\\)\\s*\\{([\\s\\S]*?)\\n\\t\\t\\}`));
assert.ok(m, `${f} 应定义 ${method}`);
assert.ok(/\$util\.redirectTo/.test(m[1]), `${f}#${method} 必须走 $util.redirectTo`);
assert.ok(new RegExp(`redirectTo\\('${target.replace(/\//g, '\\/')}'\\)`).test(m[1]),
`${f}#${method} 目标应为 ${target}`);
}
assert.ok(!/uni\.switchTab\s*\(/.test(src), `${f} 不得直接调 uni.switchTab`);
}
});
test('鸿蒙: manage.vue 跳详情/编辑用 navigateTo + encodeURIComponent(card_id)', () => {
const src = readAdmin('manage.vue');
// 名片路由参数一律 card_id(字符串), 双编码无害(纯 ASCII)
assert.ok(/\/pages_tool\/card\/detail\?card_id='\s*\+\s*encodeURIComponent\(item\.card_id\)/.test(src),
'goDetail 应 encodeURIComponent(card_id)');
assert.ok(/\/pages_tool\/card\/edit\?card_id='\s*\+\s*encodeURIComponent\(item\.card_id\)/.test(src),
'goEdit 应 encodeURIComponent(card_id)');
});
// =============================================================
// 4. 编辑页复用回跳(from=admin)
// =============================================================
test('复用: contact-edit 支持 from=admin 且缺省行为不变', () => {
const src = readCard('contact-edit.vue');
assert.ok(/this\.fromAdmin\s*=\s*options\.from\s*===\s*'admin'/.test(src),
'onLoad 应解析 from=admin');
const m = src.match(/backToList\(\)\s*\{([\s\S]*?)\n\t\t\}/);
assert.ok(m, '应能提取 backToList 方法体');
assert.ok(/\$util\.redirectTo/.test(m[1]), 'backToList 必须走 $util.redirectTo');
assert.ok(/this\.fromAdmin\s*\?/.test(m[1]), 'backToList 应按来源分支');
assert.ok(m[1].includes("'/pages_tool/card/admin/contacts'"),
'商家端应回 /pages_tool/card/admin/contacts');
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');
});
test('复用: admin/contacts 的编辑入口带 from=admin', () => {
const src = readAdmin('contacts.vue');
assert.ok(/contact-edit\?from=admin&id='/.test(src), '编辑入口应带 from=admin&id=');
assert.ok(/contact-edit\?from=admin'/.test(src), '添加入口应带 from=admin');
});
// =============================================================
// 5. 条件编译配对平衡(新文件同样纳入)
// =============================================================
test('ifdef 配对平衡: 商家端 3 页', () => {
for (const f of ['manage.vue', 'contacts.vue', 'leads.vue']) {
const src = readAdmin(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}`);
}
});