- cnFetch 统一网络层,业务化错误文案+原位重试,屏蔽内部文件名 - 证据弹窗:结论/来源平台·企业·日期/原文/命中依据+翻页 - 图谱默认 Java 岗位子图;详情面板四区块(结论/能力清单/近期变化/证据操作) - 演化页本期结论块(需求指数口径)+近8季默认视图 - 匹配页输入区 60/40 + 岗位信息卡 + 结论分档文案 + 缺口折叠 - 学习路径空状态引导+阶段语义化命名;六步导览浮动条 - tests/test_frontend_logic.js 21 用例
75 lines
4.0 KiB
JavaScript
75 lines
4.0 KiB
JavaScript
/* 关键前端逻辑单测(node 直跑):错误翻译 / 结论分档 / 阶段命名 / 本期结论分组 */
|
||
"use strict";
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const src = fs.readFileSync(path.join(__dirname, "..", "webui", "app.js"), "utf-8");
|
||
|
||
function extractFn(name) {
|
||
const m = src.match(new RegExp(`function ${name}\\([\\s\\S]*?\\n}`));
|
||
if (!m) throw new Error(`未找到函数 ${name}`);
|
||
return m[0];
|
||
}
|
||
|
||
function loadFn(name) {
|
||
// 在独立函数作用域内定义并返回目标函数(规避 eval 严格模式作用域问题)
|
||
return new Function(`${extractFn(name)}; return ${name};`)();
|
||
}
|
||
const friendlyErr = loadFn("friendlyErr");
|
||
const matchAdvice = loadFn("matchAdvice");
|
||
|
||
let pass = 0, fail = 0;
|
||
function eq(actual, expected, label) {
|
||
const ok = actual === expected;
|
||
if (ok) pass++; else { fail++; console.error(`FAIL ${label}: 期望 ${JSON.stringify(expected)} 实际 ${JSON.stringify(actual)}`); }
|
||
}
|
||
function includes(hay, needle, label) {
|
||
const ok = String(hay).includes(needle);
|
||
if (ok) pass++; else { fail++; console.error(`FAIL ${label}: ${JSON.stringify(hay)} 不含 ${needle}`); }
|
||
}
|
||
|
||
/* friendlyErr:禁止出现 Failed to fetch / 内部文件名 / 接口路径 */
|
||
eq(friendlyErr(new TypeError("Failed to fetch")), "暂时无法连接服务,请检查后端是否已启动", "fetch 失败翻译");
|
||
eq(friendlyErr(new Error("NetworkError when attempting to fetch resource.")), "暂时无法连接服务,请检查后端是否已启动", "NetworkError 翻译");
|
||
eq(friendlyErr(new Error("记录不存在")), "记录不存在", "服务端中文 detail 原样保留");
|
||
eq(friendlyErr(new Error("HTTP 500")), "服务响应异常(500)", "HTTP 状态码中文化");
|
||
eq(friendlyErr(new Error("batch_2019.jsonl#92 读取失败")), "操作失败,请稍后重试", "内部文件名不外显");
|
||
eq(friendlyErr(new Error("")), "操作失败,请稍后重试", "空消息兜底");
|
||
|
||
/* matchAdvice:三档结论 */
|
||
includes(matchAdvice(85), "强烈推荐", ">=80 档");
|
||
includes(matchAdvice(72), "补齐核心缺口", "60-80 档");
|
||
includes(matchAdvice(40), "先系统学习", "<60 档");
|
||
includes(matchAdvice(80), "强烈推荐", "边界 80");
|
||
includes(matchAdvice(60), "补齐核心缺口", "边界 60");
|
||
includes(matchAdvice(59.9), "先系统学习", "边界 59.9");
|
||
|
||
/* 阶段语义化命名(与 renderPath 内逻辑一致) */
|
||
const STAGE_NAMES = ["基础补齐", "岗位核心", "项目验证"];
|
||
const stageName = (i) => STAGE_NAMES[i] || "综合提升";
|
||
eq(stageName(0), "基础补齐", "阶段1命名");
|
||
eq(stageName(1), "岗位核心", "阶段2命名");
|
||
eq(stageName(2), "项目验证", "阶段3命名");
|
||
eq(stageName(3), "综合提升", "阶段4顺延");
|
||
|
||
/* 本期结论分组逻辑(与 renderEvoSummary 一致) */
|
||
const evoEvents = [
|
||
{ window: "2026Q2", change_type: "新增", skill: "Kubernetes", prev_weight: 0, curr_weight: 2.52 },
|
||
{ window: "2026Q2", change_type: "增强", skill: "微服务", prev_weight: 1.3, curr_weight: 2.1 },
|
||
{ window: "2026Q3", change_type: "减弱", skill: "JSP", prev_weight: 0.8, curr_weight: 0.2 },
|
||
{ window: "2026Q3", change_type: "退出", skill: "Struts", prev_weight: 0.1, curr_weight: 0 },
|
||
];
|
||
const wins = [...new Set(evoEvents.map((e) => e.window))].sort().slice(-2);
|
||
eq(wins.join(","), "2026Q2,2026Q3", "最近两个季度窗口");
|
||
const verb = { "新增": "成为新要求", "增强": "需求显著增强", "减弱": "需求明显减弱", "退出": "退出能力画像" };
|
||
eq(verb[evoEvents[1].change_type], "需求显著增强", "增强动词");
|
||
const line = `${evoEvents[1].skill}(需求指数 ${evoEvents[1].prev_weight.toFixed(2)}→${evoEvents[1].curr_weight.toFixed(2)})`;
|
||
eq(line, "微服务(需求指数 1.30→2.10)", "需求指数文案");
|
||
|
||
/* 热度走势默认近 8 季切片 */
|
||
const heat = Array.from({ length: 30 }, (_, i) => [`20${10 + i}Q1`, i]);
|
||
eq(heat.slice(-8).length, 8, "近8季切片");
|
||
eq(heat.slice(-Infinity).length, 30, "全部范围");
|
||
|
||
console.log(`\n${pass} passed, ${fail} failed`);
|
||
process.exit(fail ? 1 : 0);
|