test(fe): 阶段0止血——pre-commit守卫+postSafe+jest.config+CI脚本+otcformat精度修复
This commit is contained in:
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# pre-commit — 提交前自动守卫
|
||||
# =============================================================================
|
||||
# 做两件事(任一失败即阻断提交):
|
||||
# 1. guard_arch.js — 扫描新增/修改行,禁止在 Vue 组件里内联金额计算
|
||||
# 2. jest — 跑前端单测,确保不引入回归
|
||||
#
|
||||
# 安装方式(开发者只需执行一次):
|
||||
# cd <repo-root>
|
||||
# cp YLErpWeb/fe-tests/hooks/pre-commit .git/hooks/pre-commit
|
||||
# chmod +x .git/hooks/pre-commit
|
||||
#
|
||||
# 跳过方式(紧急情况,不推荐):
|
||||
# git commit --no-verify
|
||||
#
|
||||
# CI 也应调用本脚本(或等价的 npm test + node guard_arch.js)。
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# 定位仓库根目录
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
FE_TESTS_DIR="$REPO_ROOT/YLErpWeb/fe-tests"
|
||||
|
||||
# 自动加载 nvm(非交互 shell 中 nvm 不会自动加载)
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
if [ -s "$NVM_DIR/nvm.sh" ] && ! command -v node &>/dev/null; then
|
||||
source "$NVM_DIR/nvm.sh" 2>/dev/null || true
|
||||
# 如果有 .nvmrc 就用它,否则用默认版本
|
||||
if [ -f "$REPO_ROOT/.nvmrc" ]; then
|
||||
nvm use --silent 2>/dev/null || true
|
||||
else
|
||||
# 尝试用已安装的最新版本
|
||||
nvm use --silent --lts 2>/dev/null || nvm use --silent node 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}[pre-commit] 开始提交前守卫检查...${NC}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 1. guard_arch.js — 架构闸门(零依赖,纯 Node)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 只在存在 node 时运行
|
||||
if command -v node &>/dev/null; then
|
||||
echo -e "${YELLOW}[pre-commit] ① 架构闸门 (guard_arch.js)...${NC}"
|
||||
if node "$FE_TESTS_DIR/guard_arch.js"; then
|
||||
echo -e "${GREEN}[pre-commit] ✅ 架构闸门通过${NC}"
|
||||
else
|
||||
echo -e "${RED}[pre-commit] ❌ 架构闸门未通过,提交被阻断${NC}"
|
||||
echo -e "${YELLOW} 请把金额/精度计算抽到 *Calc 模块(参考 swapCalc.js)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}[pre-commit] ⚠️ 未找到 node,跳过架构闸门(建议安装 nvm + node 20)${NC}"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2. jest — 前端单测(需要先 npm install)
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f "$FE_TESTS_DIR/node_modules/.bin/jest" ]; then
|
||||
echo -e "${YELLOW}[pre-commit] ② 前端单测 (jest)...${NC}"
|
||||
# 只在有 JS 源文件改动时才跑测试
|
||||
JS_CHANGED=$(git diff --cached --name-only --diff-filter=ACM -- "*.js" | grep "wwwroot/Scripts/" || true)
|
||||
if [ -z "$JS_CHANGED" ]; then
|
||||
echo -e "${GREEN}[pre-commit] ⏭️ 无前端源文件改动,跳过单测${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} 改动的前端文件:${NC}"
|
||||
echo "$JS_CHANGED" | sed 's/^/ /'
|
||||
if (cd "$FE_TESTS_DIR" && npx jest --no-coverage --silent 2>&1); then
|
||||
echo -e "${GREEN}[pre-commit] ✅ 前端单测通过${NC}"
|
||||
else
|
||||
echo -e "${RED}[pre-commit] ❌ 前端单测未通过,提交被阻断${NC}"
|
||||
echo -e "${YELLOW} 修复测试后重新提交,或用 git commit --no-verify 跳过(不推荐)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}[pre-commit] ⚠️ 未安装 jest(node_modules 缺失),跳过单测${NC}"
|
||||
echo -e "${YELLOW} 建议:cd YLErpWeb/fe-tests && npm install${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[pre-commit] ✅ 所有守卫检查通过,继续提交${NC}"
|
||||
exit 0
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* jest.config.js — 前端测试配置
|
||||
* ============================================================================
|
||||
* 运行方式:cd YLErpWeb/fe-tests && npx jest
|
||||
*
|
||||
* 设计要点:
|
||||
* - testEnvironment: jsdom — 模拟浏览器环境(jQuery / Vue 组件测试需要)
|
||||
* - testMatch: 只跑 *.test.js,不跑 _shim_run.js / guard_arch.js
|
||||
* - collectCoverage: 默认不开(影响速度),用 --coverage 手动开启
|
||||
* - coverageThreshold: 新代码覆盖率门槛,逐步提高
|
||||
* - moduleDirectories: 让测试文件能 require('jquery') 等npm包
|
||||
* ============================================================================
|
||||
*/
|
||||
module.exports = {
|
||||
// 保持 node 环境:部分测试文件自行 require('jsdom') 搭建 DOM,
|
||||
// 若改为 jsdom 环境会与之冲突(TextEncoder 未定义等问题)。
|
||||
testEnvironment: 'node',
|
||||
|
||||
// 只匹配 *.test.js
|
||||
testMatch: ['**/*.test.js'],
|
||||
|
||||
// 模块查找路径(node_modules + wwwroot/Scripts 便于 require 源文件)
|
||||
moduleDirectories: ['node_modules', '../wwwroot/Scripts'],
|
||||
|
||||
// 覆盖率配置(--coverage 时生效)
|
||||
collectCoverageFrom: [
|
||||
'../wwwroot/Scripts/app/swaptrade/swapCalc.js',
|
||||
'../wwwroot/Scripts/fast/fastVue.base.js',
|
||||
// 逐步加入更多文件
|
||||
],
|
||||
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// 覆盖率门槛(初始宽松,逐步收紧)
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 40,
|
||||
functions: 50,
|
||||
lines: 50,
|
||||
statements: 50,
|
||||
},
|
||||
},
|
||||
|
||||
// 不转换 node_modules(本项目 JS 是原生 ES5,不需要 babel)
|
||||
transform: {},
|
||||
|
||||
// verbose 输出
|
||||
verbose: false,
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* main.postSafe.test.js — main.postSafe() 错误守卫封装测试
|
||||
* ============================================================================
|
||||
* 目的:验证 postSafe 正确将 jQuery Deferred 的 .done/.fail 映射到 Promise 的 resolve/reject,
|
||||
* 防止「业务错误走 reject、.done 不触发」陷阱复发。
|
||||
*
|
||||
* 覆盖场景:
|
||||
* 1. 成功请求 → Promise resolve
|
||||
* 2. 业务错误(resp.success === false)→ Promise reject
|
||||
* 3. 网络异常 → Promise reject
|
||||
* 4. async/await 语法兼容性
|
||||
* 5. 与 main.post 行为对比(陷阱复现)
|
||||
*
|
||||
* 运行:cd YLErpWeb/fe-tests && npx jest main.postSafe
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
// 搭建最小 DOM 环境
|
||||
const { JSDOM } = require('jsdom');
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.navigator = dom.window.navigator;
|
||||
|
||||
// Mock main 对象(与 main.js 中的逻辑等价)
|
||||
var main = {};
|
||||
|
||||
// main.post: 返回 jQuery Deferred promise,测试中通过 _lastDeferred 控制结果
|
||||
main.post = function (url, data, options) {
|
||||
var doneCb = [], failCb = [];
|
||||
var promise = {
|
||||
done: function (cb) { doneCb.push(cb); return promise; },
|
||||
fail: function (cb) { failCb.push(cb); return promise; },
|
||||
};
|
||||
main._lastDeferred = {
|
||||
resolve: function (resp) { doneCb.forEach(function (cb) { cb(resp); }); },
|
||||
reject: function (resp) { failCb.forEach(function (cb) { cb(resp); }); },
|
||||
promise: function () { return promise; },
|
||||
};
|
||||
return main._lastDeferred.promise();
|
||||
};
|
||||
|
||||
// main.postSafe: 与 main.js 中完全相同的实现
|
||||
main.postSafe = function (url, data, options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
main.post(url, data, options)
|
||||
.done(function (resp) { resolve(resp); })
|
||||
.fail(function (resp) { reject(resp); });
|
||||
});
|
||||
};
|
||||
|
||||
describe('main.postSafe — Promise 封装守卫', function () {
|
||||
|
||||
test('成功请求 → Promise resolve(resp)', function () {
|
||||
var p = main.postSafe('/api/test', { foo: 1 });
|
||||
// postSafe 内部已调用 main.post,_lastDeferred 已就绪
|
||||
main._lastDeferred.resolve({ success: true, data: 'ok' });
|
||||
return p.then(function (resp) {
|
||||
expect(resp.success).toBe(true);
|
||||
expect(resp.data).toBe('ok');
|
||||
});
|
||||
});
|
||||
|
||||
test('业务错误(resp.success===false)→ Promise reject(resp)', function () {
|
||||
var p = main.postSafe('/api/test', { foo: 1 });
|
||||
main._lastDeferred.reject({ success: false, msg: '算不出来' });
|
||||
return p.catch(function (resp) {
|
||||
expect(resp.success).toBe(false);
|
||||
expect(resp.msg).toBe('算不出来');
|
||||
});
|
||||
});
|
||||
|
||||
test('网络异常 → Promise reject', function () {
|
||||
var p = main.postSafe('/api/test', { foo: 1 });
|
||||
main._lastDeferred.reject({ errcode: 500, msg: '请求失败' });
|
||||
return p.catch(function (resp) {
|
||||
expect(resp.errcode).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
test('async/await 语法兼容 — 成功路径', async function () {
|
||||
var p = main.postSafe('/api/test', { foo: 1 });
|
||||
main._lastDeferred.resolve({ success: true, value: 42 });
|
||||
var resp = await p;
|
||||
expect(resp.success).toBe(true);
|
||||
expect(resp.value).toBe(42);
|
||||
});
|
||||
|
||||
test('async/await 语法兼容 — 失败路径', async function () {
|
||||
var p = main.postSafe('/api/test', { foo: 1 });
|
||||
main._lastDeferred.reject({ success: false, msg: '计算失败' });
|
||||
|
||||
var caught = null;
|
||||
try {
|
||||
await p;
|
||||
} catch (resp) {
|
||||
caught = resp;
|
||||
}
|
||||
expect(caught).not.toBeNull();
|
||||
expect(caught.success).toBe(false);
|
||||
expect(caught.msg).toBe('计算失败');
|
||||
});
|
||||
});
|
||||
|
||||
describe('main.postSafe — 与 main.post 行为对比(陷阱复现)', function () {
|
||||
|
||||
test('main.post 的 .done 在业务错误时不触发(陷阱复现)', function () {
|
||||
var doneCalled = false;
|
||||
var failCalled = false;
|
||||
|
||||
main.post('/api/test', {}).done(function () {
|
||||
doneCalled = true;
|
||||
}).fail(function () {
|
||||
failCalled = true;
|
||||
});
|
||||
|
||||
// 业务错误 → reject
|
||||
main._lastDeferred.reject({ success: false });
|
||||
|
||||
expect(doneCalled).toBe(false); // .done 没触发!这就是陷阱
|
||||
expect(failCalled).toBe(true); // .fail 触发了
|
||||
});
|
||||
|
||||
test('main.postSafe 的 .then 在业务错误时不触发(安全)', function () {
|
||||
var thenCalled = false;
|
||||
var caught = null;
|
||||
|
||||
var p = main.postSafe('/api/test', {}).then(function () {
|
||||
thenCalled = true;
|
||||
}).catch(function (resp) {
|
||||
caught = resp;
|
||||
});
|
||||
|
||||
// 业务错误 → reject
|
||||
main._lastDeferred.reject({ success: false, msg: 'biz error' });
|
||||
|
||||
// Promise 是微任务,需要等一轮
|
||||
return p.then(function () {
|
||||
expect(thenCalled).toBe(false); // .then 没触发
|
||||
expect(caught).not.toBeNull(); // .catch 触发了
|
||||
expect(caught.msg).toBe('biz error');
|
||||
});
|
||||
});
|
||||
});
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# run-ci-checks.sh — CI 守卫入口脚本
|
||||
# =============================================================================
|
||||
# Jenkins / GitLab CI / GitHub Actions 等任一 CI 系统调用本脚本即可。
|
||||
#
|
||||
# 做三件事(任一失败即 exit 1,阻断 CI):
|
||||
# 1. guard_arch.js — 架构闸门:禁止新增内联金额计算
|
||||
# 2. jest — 前端单测:防止回归
|
||||
# 3. rebuild-bundles — bundle 校验:确保产物与源文件一致
|
||||
#
|
||||
# 用法:
|
||||
# bash YLErpWeb/fe-tests/run-ci-checks.sh
|
||||
#
|
||||
# 前置条件:
|
||||
# - Node.js 18+ 已安装(CI 环境通常自带)
|
||||
# - cd YLErpWeb/fe-tests && npm install 已执行
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
FE_TESTS_DIR="$REPO_ROOT/YLErpWeb/fe-tests"
|
||||
YLERPWEB_DIR="$REPO_ROOT/YLErpWeb"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
FAILED=0
|
||||
|
||||
run_check() {
|
||||
local name="$1"
|
||||
local cmd="$2"
|
||||
echo -e "\n${YELLOW}========================================${NC}"
|
||||
echo -e "${YELLOW}CI Check: ${name}${NC}"
|
||||
echo -e "${YELLOW}========================================${NC}"
|
||||
if eval "$cmd"; then
|
||||
echo -e "${GREEN}✅ ${name} PASSED${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ ${name} FAILED${NC}"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Check 1: 架构闸门
|
||||
# -----------------------------------------------------------------------------
|
||||
if command -v node &>/dev/null; then
|
||||
run_check "guard_arch.js (架构闸门)" \
|
||||
"node '$FE_TESTS_DIR/guard_arch.js'"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ node 未安装,跳过架构闸门${NC}"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Check 2: 前端单测
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f "$FE_TESTS_DIR/node_modules/.bin/jest" ]; then
|
||||
run_check "jest (前端单测)" \
|
||||
"cd '$FE_TESTS_DIR' && npx jest --no-coverage --silent"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ jest 未安装,尝试自动安装...${NC}"
|
||||
if command -v npm &>/dev/null; then
|
||||
(cd "$FE_TESTS_DIR" && npm install --silent)
|
||||
run_check "jest (前端单测)" \
|
||||
"cd '$FE_TESTS_DIR' && npx jest --no-coverage --silent"
|
||||
else
|
||||
echo -e "${RED}❌ npm 不可用,无法运行前端单测${NC}"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Check 3: Bundle 校验(可选,需要 python3)
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f "$YLERPWEB_DIR/rebuild-bundles.py" ] && command -v python3 &>/dev/null; then
|
||||
run_check "bundle 校验 (rebuild-bundles.py --verify)" \
|
||||
"cd '$YLERPWEB_DIR' && python3 rebuild-bundles.py --verify"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ python3 或 rebuild-bundles.py 不可用,跳过 bundle 校验${NC}"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 汇总
|
||||
# -----------------------------------------------------------------------------
|
||||
echo -e "\n${YELLOW}========================================${NC}"
|
||||
echo -e "${YELLOW}CI Checks Summary${NC}"
|
||||
echo -e "${YELLOW}========================================${NC}"
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ All checks passed${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}❌ ${FAILED} check(s) failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user