feat: 为高频变更 JS 文件补充 smoke test 并建立 js-test-gate Rule
新增测试: - api.test.js: 28 个用例覆盖 VpsFmt 纯工具函数(OAuth 归一化、倒计时文案、费用文案、徽章样式) - app.test.js: 15 个用例覆盖 NAVS/VIEW_MAP/VALID_VIEWS 常量和 _viewFromHash 路由解析 - modals.test.js: 3 个用例验证 9 个模态框组件定义完整性 规则与钩子: - 新建 .qoder/rules/js-test-gate.md:编辑 static/js/ 下 JS 文件后运行 vitest - 更新 post-edit-check.sh:JS 编辑后 node --check + vitest(排除 vendored 文件) 部署闸门 check_js_tests(update.sh)已有 vitest run 步骤,现在测试覆盖从 1/7 文件提升到 4/7
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Post-edit hook: 自动验证编辑后的文件
|
||||
# - Python 文件 → 运行 pytest
|
||||
# - JavaScript 文件 → 运行 node --check
|
||||
# - JavaScript 文件 → node --check 语法检查 + vitest 单元测试
|
||||
|
||||
input=$(cat)
|
||||
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
|
||||
@@ -17,7 +17,15 @@ case "$rel_path" in
|
||||
;;
|
||||
static/js/*.js)
|
||||
echo "🔍 检测到 JavaScript 文件变更: $rel_path" >&2
|
||||
node --check "$file_path"
|
||||
node --check "$file_path" || exit 1
|
||||
# 语法通过后运行 vitest(排除 vendored 文件)
|
||||
case "$rel_path" in
|
||||
*/vue.global.prod.js|*/tailwind.js|*/chart.umd.js) ;;
|
||||
*)
|
||||
cd "$QODER_PROJECT_DIR" || exit 0
|
||||
npx vitest run
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# vps-manager JavaScript 测试验证规则
|
||||
|
||||
## 触发条件
|
||||
|
||||
每次编辑 `vps-manager/static/js/` 下的 `.js` 文件后自动运行(排除第三方 vendored 文件如 `vue.global.prod.js`、`tailwind.js`、`chart.umd.js`)。
|
||||
|
||||
## 执行命令
|
||||
|
||||
```bash
|
||||
cd vps-manager && npx vitest run
|
||||
```
|
||||
|
||||
## 规则
|
||||
|
||||
- 全部通过才视为改动完成,失败则先修复再提交
|
||||
- 测试文件位于 `static/js/__tests__/` 目录,vitest 配置在 `vitest.config.js`
|
||||
- 当前覆盖:`store.test.js`(状态管理)、`api.test.js`(工具函数)、`app.test.js`(导航路由)、`modals.test.js`(组件定义)
|
||||
- 部署闸门 `deploy/update.sh` 的 `check_js_tests` 步骤会在 cc1 自动更新时运行 `npx vitest run`,失败则回滚并告警
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* api.js smoke test
|
||||
*
|
||||
* 验证 VpsFmt 纯工具函数:OAuth 归一化、倒计时文案、费用文案、徽章样式等。
|
||||
* api.js 是 IIFE,会立即创建 window.VpsApi / window.VpsFmt,
|
||||
* 用 vm.runInNewContext 在沙箱内加载,注入最小 mock。
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
|
||||
let ctx;
|
||||
|
||||
beforeAll(() => {
|
||||
const _store = {};
|
||||
const localStorage = {
|
||||
getItem: (k) => (k in _store ? _store[k] : null),
|
||||
setItem: (k, v) => { _store[k] = String(v); },
|
||||
removeItem: (k) => { delete _store[k]; },
|
||||
clear: () => { for (const k of Object.keys(_store)) delete _store[k]; },
|
||||
get length() { return Object.keys(_store).length; },
|
||||
key: (i) => Object.keys(_store)[i] ?? null,
|
||||
};
|
||||
|
||||
ctx = vm.createContext({
|
||||
APP_CONFIG: { appName: 'Test', version: '0.0.0', commit: 'test' },
|
||||
window: {},
|
||||
localStorage,
|
||||
fetch: async () => ({ ok: true, status: 200, json: async () => ({}) }),
|
||||
console,
|
||||
});
|
||||
ctx.window = ctx;
|
||||
|
||||
const code = readFileSync(
|
||||
resolve(import.meta.dirname, '..', 'api.js'),
|
||||
'utf-8',
|
||||
);
|
||||
vm.runInContext(code, ctx);
|
||||
});
|
||||
|
||||
/* ---------- VpsApi 基础 ---------- */
|
||||
|
||||
describe('VpsApi 初始化', () => {
|
||||
it('暴露 get/post/put/del 方法', () => {
|
||||
expect(typeof ctx.VpsApi.get).toBe('function');
|
||||
expect(typeof ctx.VpsApi.post).toBe('function');
|
||||
expect(typeof ctx.VpsApi.put).toBe('function');
|
||||
expect(typeof ctx.VpsApi.del).toBe('function');
|
||||
});
|
||||
|
||||
it('CFG 来自 window.APP_CONFIG', () => {
|
||||
expect(ctx.VpsApi.CFG.appName).toBe('Test');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- OAuth 归一化 ---------- */
|
||||
|
||||
describe('VpsFmt OAuth 函数', () => {
|
||||
it('oauthLabel 预设 key 返回中文标签', () => {
|
||||
expect(ctx.VpsFmt.oauthLabel('alipay')).toBe('支付宝');
|
||||
expect(ctx.VpsFmt.oauthLabel('github')).toBe('GitHub');
|
||||
expect(ctx.VpsFmt.oauthLabel('wechat')).toBe('微信');
|
||||
});
|
||||
|
||||
it('oauthLabel 未知 key 原样返回', () => {
|
||||
expect(ctx.VpsFmt.oauthLabel('custom_provider')).toBe('custom_provider');
|
||||
expect(ctx.VpsFmt.oauthLabel('')).toBe('');
|
||||
expect(ctx.VpsFmt.oauthLabel(null)).toBe('');
|
||||
});
|
||||
|
||||
it('oauthDetail 预设返回「标签(key)」格式', () => {
|
||||
expect(ctx.VpsFmt.oauthDetail('alipay')).toBe('支付宝(alipay)');
|
||||
expect(ctx.VpsFmt.oauthDetail('github')).toBe('GitHub(github)');
|
||||
});
|
||||
|
||||
it('oauthDetail 未知 key 只返回原文', () => {
|
||||
expect(ctx.VpsFmt.oauthDetail('my_custom')).toBe('my_custom');
|
||||
});
|
||||
|
||||
it('normalizeOauth 小写 key 直通', () => {
|
||||
expect(ctx.VpsFmt.normalizeOauth('github')).toBe('github');
|
||||
expect(ctx.VpsFmt.normalizeOauth(' GitHub ')).toBe('github');
|
||||
});
|
||||
|
||||
it('normalizeOauth 中文标签归一为 key', () => {
|
||||
expect(ctx.VpsFmt.normalizeOauth('支付宝')).toBe('alipay');
|
||||
expect(ctx.VpsFmt.normalizeOauth('微信')).toBe('wechat');
|
||||
});
|
||||
|
||||
it('normalizeOauth 空值返回空字符串', () => {
|
||||
expect(ctx.VpsFmt.normalizeOauth('')).toBe('');
|
||||
expect(ctx.VpsFmt.normalizeOauth(null)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 倒计时文案 ---------- */
|
||||
|
||||
describe('VpsFmt.countdownText', () => {
|
||||
it('正数返回「N 天后」', () => {
|
||||
expect(ctx.VpsFmt.countdownText(30)).toBe('30 天后');
|
||||
expect(ctx.VpsFmt.countdownText(1)).toBe('1 天后');
|
||||
});
|
||||
|
||||
it('负数返回「已过期 N 天」', () => {
|
||||
expect(ctx.VpsFmt.countdownText(-5)).toBe('已过期 5 天');
|
||||
});
|
||||
|
||||
it('零返回「今天到期」', () => {
|
||||
expect(ctx.VpsFmt.countdownText(0)).toBe('今天到期');
|
||||
});
|
||||
|
||||
it('null/undefined 返回默认占位', () => {
|
||||
expect(ctx.VpsFmt.countdownText(null)).toBe('未设到期日');
|
||||
expect(ctx.VpsFmt.countdownText(undefined)).toBe('未设到期日');
|
||||
});
|
||||
|
||||
it('opts.empty 自定义占位', () => {
|
||||
expect(ctx.VpsFmt.countdownText(null, { empty: '—' })).toBe('—');
|
||||
});
|
||||
|
||||
it('opts.unit="" 只返回「N 天」', () => {
|
||||
expect(ctx.VpsFmt.countdownText(7, { unit: '' })).toBe('7 天');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 费用文案 ---------- */
|
||||
|
||||
describe('VpsFmt.costText', () => {
|
||||
it('零返回「未填」', () => {
|
||||
expect(ctx.VpsFmt.costText(0, 'USD')).toBe('未填');
|
||||
});
|
||||
|
||||
it('有值有币种拼接', () => {
|
||||
expect(ctx.VpsFmt.costText(9.99, 'USD')).toBe('9.99 USD');
|
||||
});
|
||||
|
||||
it('有值无币种只返回数字字符串', () => {
|
||||
expect(ctx.VpsFmt.costText(100, '')).toBe('100');
|
||||
expect(ctx.VpsFmt.costText(50, null)).toBe('50');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 到期日样式 ---------- */
|
||||
|
||||
describe('VpsFmt 到期日样式函数', () => {
|
||||
it('expiryText 按天数区间返回不同样式类', () => {
|
||||
expect(ctx.VpsFmt.expiryText(-1)).toContain('red');
|
||||
expect(ctx.VpsFmt.expiryText(3)).toContain('red');
|
||||
expect(ctx.VpsFmt.expiryText(15)).toContain('amber');
|
||||
expect(ctx.VpsFmt.expiryText(60)).toContain('slate');
|
||||
expect(ctx.VpsFmt.expiryText(null)).toContain('slate');
|
||||
});
|
||||
|
||||
it('expiryBadge 按天数区间返回不同背景色', () => {
|
||||
expect(ctx.VpsFmt.expiryBadge(3)).toContain('red');
|
||||
expect(ctx.VpsFmt.expiryBadge(15)).toContain('amber');
|
||||
expect(ctx.VpsFmt.expiryBadge(60)).toContain('slate');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 标签映射表 ---------- */
|
||||
|
||||
describe('VpsFmt 标签映射表', () => {
|
||||
it('TYPE_LABELS 包含核心资产类型', () => {
|
||||
expect(ctx.VpsFmt.TYPE_LABELS.vps).toBe('VPS');
|
||||
expect(ctx.VpsFmt.TYPE_LABELS.domain).toBe('域名');
|
||||
expect(ctx.VpsFmt.TYPE_LABELS.ai_agent).toBe('AI账号');
|
||||
});
|
||||
|
||||
it('STATUS_LABELS 包含全部状态', () => {
|
||||
expect(ctx.VpsFmt.STATUS_LABELS.active).toBe('使用中');
|
||||
expect(ctx.VpsFmt.STATUS_LABELS.expired).toBe('已过期');
|
||||
});
|
||||
|
||||
it('LOGIN_TYPE_LABELS 包含登录方式', () => {
|
||||
expect(ctx.VpsFmt.LOGIN_TYPE_LABELS.password).toBe('密码登录');
|
||||
expect(ctx.VpsFmt.LOGIN_TYPE_LABELS.oauth).toBe('授权登录');
|
||||
});
|
||||
|
||||
it('OAUTH_PRESETS 是非空数组', () => {
|
||||
expect(Array.isArray(ctx.VpsFmt.OAUTH_PRESETS)).toBe(true);
|
||||
expect(ctx.VpsFmt.OAUTH_PRESETS.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 徽章函数兜底 ---------- */
|
||||
|
||||
describe('VpsFmt 徽章函数兜底', () => {
|
||||
it('typeBadge 未知类型返回默认样式', () => {
|
||||
expect(ctx.VpsFmt.typeBadge('nonexistent')).toContain('slate');
|
||||
});
|
||||
|
||||
it('statusBadge 未知状态返回默认样式', () => {
|
||||
expect(ctx.VpsFmt.statusBadge('unknown_status')).toContain('slate');
|
||||
});
|
||||
|
||||
it('loginTypeBadge 未知类型返回默认样式', () => {
|
||||
expect(ctx.VpsFmt.loginTypeBadge('xxx')).toContain('slate');
|
||||
});
|
||||
|
||||
it('checkBadge 各状态返回对应颜色', () => {
|
||||
expect(ctx.VpsFmt.checkBadge('pass')).toContain('emerald');
|
||||
expect(ctx.VpsFmt.checkBadge('warn')).toContain('amber');
|
||||
expect(ctx.VpsFmt.checkBadge('fail')).toContain('red');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* app.js smoke test
|
||||
*
|
||||
* 验证导航常量(NAVS / VIEW_MAP / VALID_VIEWS)和 Hash 路由解析函数。
|
||||
* app.js 末尾会 Vue.createApp + mount,通过 mock Vue 使 mount 为空操作跳过。
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
|
||||
let ctx;
|
||||
|
||||
beforeAll(() => {
|
||||
const _store = {};
|
||||
const localStorage = {
|
||||
getItem: (k) => (k in _store ? _store[k] : null),
|
||||
setItem: (k, v) => { _store[k] = String(v); },
|
||||
removeItem: (k) => { delete _store[k]; },
|
||||
clear: () => { for (const k of Object.keys(_store)) delete _store[k]; },
|
||||
get length() { return Object.keys(_store).length; },
|
||||
key: (i) => Object.keys(_store)[i] ?? null,
|
||||
};
|
||||
|
||||
/* Vue mock:createApp 返回链式调用对象,mount 为空跳过实际挂载 */
|
||||
const chainable = { component() { return chainable; }, mount() {} };
|
||||
const Vue = {
|
||||
reactive: (o) => o,
|
||||
computed: (fn) => ({ value: typeof fn === 'function' ? fn() : fn }),
|
||||
createApp: () => chainable,
|
||||
};
|
||||
|
||||
const store = {
|
||||
view: 'dashboard',
|
||||
loading: false,
|
||||
error: '',
|
||||
dark: false,
|
||||
serverMetrics: {},
|
||||
search: '',
|
||||
monitorTab: 'ssl',
|
||||
assetModal: { show: false },
|
||||
confirmModal: { show: false },
|
||||
resultModal: { show: false },
|
||||
credentialModal: { show: false },
|
||||
accountModal: { show: false },
|
||||
providerModal: { show: false },
|
||||
accountsModal: { show: false },
|
||||
};
|
||||
|
||||
const Api = { CFG: { appName: 'Test', version: '0.0.0', commit: 'test' } };
|
||||
const Fmt = {
|
||||
TYPE_LABELS: {}, STATUS_LABELS: {}, CATEGORY_LABELS: {},
|
||||
SERVICES_LABELS: {}, CYCLE_LABELS: {}, LOGIN_TYPE_LABELS: {},
|
||||
OAUTH_PRESETS: [],
|
||||
normalizeOauth: (s) => (s || '').trim().toLowerCase(),
|
||||
oauthLabel: (k) => k || '',
|
||||
oauthDetail: (k) => k || '',
|
||||
typeBadge: () => '',
|
||||
statusBadge: () => '',
|
||||
loginTypeBadge: () => '',
|
||||
expiryText: () => '',
|
||||
expiryBadge: () => '',
|
||||
countdownText: () => '',
|
||||
costText: () => '',
|
||||
checkBadge: () => '',
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
const mockView = { template: '<div></div>', setup() { return {}; } };
|
||||
|
||||
ctx = vm.createContext({
|
||||
Vue,
|
||||
localStorage,
|
||||
console,
|
||||
location: { hash: '#dashboard' },
|
||||
addEventListener: noop,
|
||||
removeEventListener: noop,
|
||||
window: {},
|
||||
document: {
|
||||
documentElement: { classList: { toggle: noop, add: noop, remove: noop } },
|
||||
addEventListener: noop,
|
||||
removeEventListener: noop,
|
||||
createElement: () => ({ select: noop, value: '' }),
|
||||
body: { appendChild: noop, removeChild: noop },
|
||||
},
|
||||
navigator: { clipboard: null },
|
||||
setTimeout: (fn) => { fn(); return 0; },
|
||||
clearTimeout: noop,
|
||||
setInterval: () => 0,
|
||||
clearInterval: noop,
|
||||
confirm: () => true,
|
||||
alert: noop,
|
||||
fetch: async () => ({ ok: true, status: 200, json: async () => ({}) }),
|
||||
store,
|
||||
Api,
|
||||
Fmt,
|
||||
toast: { show: false, text: '' },
|
||||
loadAssets: noop,
|
||||
loadAll: noop,
|
||||
applyDark: noop,
|
||||
openAssetCreate: noop,
|
||||
openProviderCreate: noop,
|
||||
openAICreate: noop,
|
||||
openCredentialCreate: noop,
|
||||
showResult: noop,
|
||||
closeResult: noop,
|
||||
closeConfirm: noop,
|
||||
closeActions: noop,
|
||||
closeDns: noop,
|
||||
actionModal: { show: false },
|
||||
dnsModal: { show: false },
|
||||
DashboardView: mockView,
|
||||
AssetsView: mockView,
|
||||
MonitorView: mockView,
|
||||
VaultView: mockView,
|
||||
ProvidersView: mockView,
|
||||
ServersView: mockView,
|
||||
SettingsView: mockView,
|
||||
AssetModal: mockView,
|
||||
ProviderModal: mockView,
|
||||
AccountModal: mockView,
|
||||
AccountsViewModal: mockView,
|
||||
CredentialModal: mockView,
|
||||
ActionModal: mockView,
|
||||
DnsModal: mockView,
|
||||
ConfirmModal: mockView,
|
||||
ResultModal: mockView,
|
||||
});
|
||||
ctx.window = ctx;
|
||||
|
||||
let code = readFileSync(
|
||||
resolve(import.meta.dirname, '..', 'app.js'),
|
||||
'utf-8',
|
||||
);
|
||||
code = code.replace(/^const\s+(?!\{)(\w+)\s*=/gm, 'globalThis.$1 =');
|
||||
code = code.replace(/^function\s+(\w+)/gm, 'globalThis.$1 = function $1');
|
||||
vm.runInContext(code, ctx);
|
||||
});
|
||||
|
||||
/* ---------- NAVS 导航项 ---------- */
|
||||
|
||||
describe('NAVS 导航配置', () => {
|
||||
it('包含 6 个导航项', () => {
|
||||
expect(ctx.NAVS).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('每项均有 key / label / icon', () => {
|
||||
for (const n of ctx.NAVS) {
|
||||
expect(n.key).toBeTruthy();
|
||||
expect(n.label).toBeTruthy();
|
||||
expect(n.icon).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('包含核心导航键', () => {
|
||||
const keys = ctx.NAVS.map(n => n.key);
|
||||
expect(keys).toContain('dashboard');
|
||||
expect(keys).toContain('assets');
|
||||
expect(keys).toContain('monitor');
|
||||
expect(keys).toContain('vault');
|
||||
expect(keys).toContain('providers');
|
||||
expect(keys).toContain('settings');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- VIEW_MAP 视图映射 ---------- */
|
||||
|
||||
describe('VIEW_MAP 视图映射', () => {
|
||||
it('包含全部导航视图', () => {
|
||||
expect(ctx.VIEW_MAP.dashboard).toBe('dashboard-view');
|
||||
expect(ctx.VIEW_MAP.assets).toBe('assets-view');
|
||||
expect(ctx.VIEW_MAP.monitor).toBe('monitor-view');
|
||||
expect(ctx.VIEW_MAP.vault).toBe('vault-view');
|
||||
expect(ctx.VIEW_MAP.providers).toBe('providers-view');
|
||||
expect(ctx.VIEW_MAP.settings).toBe('settings-view');
|
||||
});
|
||||
|
||||
it('包含隐藏路由 servers', () => {
|
||||
expect(ctx.VIEW_MAP.servers).toBe('servers-view');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- VALID_VIEWS ---------- */
|
||||
|
||||
describe('VALID_VIEWS', () => {
|
||||
it('集合大小与 VIEW_MAP 键数一致', () => {
|
||||
expect(ctx.VALID_VIEWS.size).toBe(Object.keys(ctx.VIEW_MAP).length);
|
||||
});
|
||||
|
||||
it('包含全部 VIEW_MAP 键', () => {
|
||||
for (const k of Object.keys(ctx.VIEW_MAP)) {
|
||||
expect(ctx.VALID_VIEWS.has(k)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- _viewFromHash 路由解析 ---------- */
|
||||
|
||||
describe('_viewFromHash 路由解析', () => {
|
||||
it('空 hash 默认 dashboard', () => {
|
||||
ctx.window.location.hash = '';
|
||||
expect(ctx._viewFromHash()).toBe('dashboard');
|
||||
});
|
||||
|
||||
it('#assets 解析为 assets', () => {
|
||||
ctx.window.location.hash = '#assets';
|
||||
expect(ctx._viewFromHash()).toBe('assets');
|
||||
});
|
||||
|
||||
it('#/vault 解析为 vault(兼容斜杠)', () => {
|
||||
ctx.window.location.hash = '#/vault';
|
||||
expect(ctx._viewFromHash()).toBe('vault');
|
||||
});
|
||||
|
||||
it('无效视图回退 dashboard', () => {
|
||||
ctx.window.location.hash = '#nonexistent';
|
||||
expect(ctx._viewFromHash()).toBe('dashboard');
|
||||
});
|
||||
|
||||
it('忽略查询串', () => {
|
||||
ctx.window.location.hash = '#monitor?tab=ssl';
|
||||
expect(ctx._viewFromHash()).toBe('monitor');
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- AppRoot 组件结构 ---------- */
|
||||
|
||||
describe('AppRoot 组件', () => {
|
||||
it('已定义且包含 template 和 setup', () => {
|
||||
expect(ctx.AppRoot).toBeDefined();
|
||||
expect(typeof ctx.AppRoot.template).toBe('string');
|
||||
expect(typeof ctx.AppRoot.setup).toBe('function');
|
||||
});
|
||||
|
||||
it('template 包含底部导航栏(移动端适配)', () => {
|
||||
expect(ctx.AppRoot.template).toContain('md:hidden');
|
||||
});
|
||||
|
||||
it('template 包含桌面端侧边栏', () => {
|
||||
expect(ctx.AppRoot.template).toContain('hidden md:flex');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* modals.js smoke test
|
||||
*
|
||||
* 验证 9 个 Vue 模态框组件均已正确定义(含 template + setup)。
|
||||
* modals.js 只有 const XModal = { template, setup } 声明,
|
||||
* setup() 内的外部引用(store / Fmt / Vue)只在 Vue 挂载时执行,
|
||||
* 因此 VM 沙箱无需注入完整 mock 即可验证组件结构。
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
|
||||
let ctx;
|
||||
|
||||
const EXPECTED_MODALS = [
|
||||
'AssetModal',
|
||||
'ProviderModal',
|
||||
'AccountModal',
|
||||
'AccountsViewModal',
|
||||
'CredentialModal',
|
||||
'ActionModal',
|
||||
'DnsModal',
|
||||
'ConfirmModal',
|
||||
'ResultModal',
|
||||
];
|
||||
|
||||
beforeAll(() => {
|
||||
ctx = vm.createContext({ console });
|
||||
ctx.window = ctx;
|
||||
|
||||
let code = readFileSync(
|
||||
resolve(import.meta.dirname, '..', 'modals.js'),
|
||||
'utf-8',
|
||||
);
|
||||
// 同 store.test.js 约定:顶格 const → globalThis,使测试可通过 ctx.X 访问
|
||||
code = code.replace(/^const\s+(?!\{)(\w+)\s*=/gm, 'globalThis.$1 =');
|
||||
vm.runInContext(code, ctx);
|
||||
});
|
||||
|
||||
describe('modals.js 组件定义完整性', () => {
|
||||
it('全部 9 个模态框组件均已定义', () => {
|
||||
for (const name of EXPECTED_MODALS) {
|
||||
expect(ctx[name], `${name} 未定义`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('每个组件均包含 template 字符串和 setup 函数', () => {
|
||||
for (const name of EXPECTED_MODALS) {
|
||||
const comp = ctx[name];
|
||||
expect(typeof comp.template, `${name}.template 非字符串`).toBe('string');
|
||||
expect(comp.template.length, `${name}.template 为空`).toBeGreaterThan(10);
|
||||
expect(typeof comp.setup, `${name}.setup 非函数`).toBe('function');
|
||||
}
|
||||
});
|
||||
|
||||
it('template 包含 v-if 条件渲染(模态框显隐控制)', () => {
|
||||
for (const name of EXPECTED_MODALS) {
|
||||
expect(ctx[name].template, `${name}.template 缺少 v-if`).toContain('v-if');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user