Files
CareerNexus/tests/test_frontend_logic.js
T
RoyirinRosmontis 0b198e9976 答辩级 UI 改造:指挥台驾驶舱/证据地图/演化生命线/成长路线图/候选审核台
- 驾驶舱改'人才能力变化指挥台':变化事件流主图+可信度卡+行动卡+播报卡
- 图谱子图同心分层布局、技能变化角标、系统发现折叠入右栏、主操作收敛
- 演化页双 Tab(岗位报告/全岗位扫描)+ 能力生命线 Swimlane + 更替流
- 学习路径成长路线图:概要条/示范路线/预计投入/标记完成联动
- 新增候选审核台抽屉 + GET /api/stats/credibility
- 简历裸姓名/专硕提取修复(桌面真实简历验收通过)+ BGE-M3 加载提示
2026-08-19 12:14:39 +08:00

98 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 关键前端逻辑单测(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, "全部范围");
/* candidateTrust:候选可信等级四指标(JD≥15 / 企业≥5 / 增长趋势 / 差异度≥0.40) */
const candidateTrust = loadFn("candidateTrust");
const stageEffortText = loadFn("stageEffortText");
const existJobs = [{ title: "Java开发工程师", required_skills: ["Java", "MySQL"], preferred_skills: ["Redis"] }];
const strong = candidateTrust({
source_count: 31, company_count: 12,
support_windows: ["2025Q1", "2025Q2", "2025Q3", "2025Q4", "2026Q1", "2026Q2"],
required_skills: ["RAG", "向量检索"], preferred_skills: [],
}, existJobs);
eq(strong.dots.length, 4, "四指标点");
eq(strong.level, "高", "四指标全达标为高");
const weak = candidateTrust({
source_count: 3, company_count: 1, support_windows: ["2026Q2"],
required_skills: ["Java", "MySQL"], preferred_skills: ["Redis"],
}, existJobs);
eq(weak.level, "低", "弱证据且与既有岗位重合为低");
eq(weak.nearest.title, "Java开发工程师", "识别出最相近既有岗位");
eq(weak.distinctness, 0, "技能完全重合时差异度为 0");
/* stageEffortText:阶段预计投入(每周约 8 小时) */
eq(stageEffortText(2), "2 周 / 约 16 小时", "阶段投入文案");
eq(stageEffortText(1), "1 周 / 约 8 小时", "单周投入文案");
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);