225 lines
6.7 KiB
JavaScript
225 lines
6.7 KiB
JavaScript
const fs = require("fs");
|
||
const path = require("path");
|
||
const vm = require("vm");
|
||
|
||
const staticDir = path.resolve(__dirname, "../app/static");
|
||
|
||
function domNode(id, extras = {}) {
|
||
let value = "";
|
||
return {
|
||
id,
|
||
textContent: "",
|
||
innerHTML: "",
|
||
hidden: false,
|
||
disabled: false,
|
||
dataset: {},
|
||
classList: { toggle() {} },
|
||
get value() {
|
||
return value;
|
||
},
|
||
set value(nextValue) {
|
||
value = String(nextValue ?? "");
|
||
},
|
||
setAttribute(name, nextValue) {
|
||
this[name] = nextValue;
|
||
},
|
||
addEventListener() {},
|
||
focus() {},
|
||
...extras,
|
||
};
|
||
}
|
||
|
||
const elements = new Map();
|
||
|
||
function element(id) {
|
||
if (!elements.has(id)) {
|
||
elements.set(id, domNode(id));
|
||
}
|
||
return elements.get(id);
|
||
}
|
||
|
||
const documentStub = {
|
||
querySelector(selector) {
|
||
return element(selector.replace(/^#/, ""));
|
||
},
|
||
querySelectorAll() {
|
||
return [];
|
||
},
|
||
addEventListener() {},
|
||
createElement(tag) {
|
||
return element(`created-${tag}-${elements.size}`);
|
||
},
|
||
body: { appendChild() {} },
|
||
execCommand() {
|
||
return true;
|
||
},
|
||
};
|
||
|
||
const context = {
|
||
console,
|
||
document: documentStub,
|
||
navigator: {},
|
||
fetch: async () => ({ ok: true, json: async () => ({ classnotes: { mtime: 0 } }) }),
|
||
Date,
|
||
JSON,
|
||
Map,
|
||
Number,
|
||
String,
|
||
};
|
||
context.window = { location: { href: "" } };
|
||
context.confirm = () => true;
|
||
context.alert = () => {};
|
||
|
||
function assertEqual(name, actual, expected) {
|
||
if (actual !== expected) {
|
||
throw new Error(`${name}: got ${actual}, expected ${expected}`);
|
||
}
|
||
}
|
||
|
||
function value(expression) {
|
||
return vm.runInContext(expression, context);
|
||
}
|
||
|
||
vm.createContext(context);
|
||
vm.runInContext(fs.readFileSync(path.join(staticDir, "app.js"), "utf8"), context);
|
||
|
||
vm.runInContext(
|
||
`
|
||
currentRecords = [
|
||
{ date: "2026.06.12", weekday: "星期五", time: "10:00-11:00", student: "甲", duration: "1小时0分", duration_hours: 1, teacher: "王老师", subject: "英语", _recordIndex: 0, _recordKey: "a" },
|
||
{ date: "2026.06.12", weekday: "星期五", time: "08:00-09:00", student: "乙", duration: "1小时0分", duration_hours: 1, teacher: "王老师", subject: "英语", _recordIndex: 1, _recordKey: "b" },
|
||
{ date: "2026.06.13", weekday: "星期六", time: "09:00-10:00", student: "丙", duration: "1小时0分", duration_hours: 1, teacher: "王老师", subject: "英语", _recordIndex: 2, _recordKey: "c" },
|
||
{ date: "2026.06.12", weekday: "星期五", time: "07:00-08:00", student: "丁", duration: "1小时0分", duration_hours: 1, teacher: "李老师", subject: "数学", _recordIndex: 3, _recordKey: "d" },
|
||
];
|
||
renderGroupedRecords(currentRecords);
|
||
`,
|
||
context,
|
||
);
|
||
|
||
assertEqual("初始日期按钮文案", value("dateSortBtn.textContent"), "日期 升序排列");
|
||
assertEqual("初始时间按钮文案", value("timeSortBtn.textContent"), "时间 升序排列");
|
||
assertEqual("默认组内排序", value("currentRecordOrder.join('')"), "bacd");
|
||
|
||
vm.runInContext("toggleRecordSort('date')", context);
|
||
assertEqual("日期降序按钮文案", value("dateSortBtn.textContent"), "日期 降序排列");
|
||
assertEqual("日期降序组内排序", value("currentRecordOrder.join('')"), "cbad");
|
||
|
||
vm.runInContext("toggleRecordSort('date'); toggleRecordSort('time')", context);
|
||
assertEqual("时间降序按钮文案", value("timeSortBtn.textContent"), "时间 降序排列");
|
||
assertEqual("日期升序时间降序组内排序", value("currentRecordOrder.join('')"), "abcd");
|
||
|
||
vm.runInContext(
|
||
`
|
||
correctedRecords.set("a", { ...currentRecords[0], date: "2026.06.11", weekday: "星期四", time: "07:00-08:00" });
|
||
renderGroupedRecords(currentRecords);
|
||
`,
|
||
context,
|
||
);
|
||
assertEqual("纠错后排序仍使用显示记录", value("currentRecordOrder.join('')"), "abcd");
|
||
vm.runInContext("updateCorrectionToolbar('测试提交按钮')", context);
|
||
assertEqual("提交审核按钮文案", value("submitCorrectionsBtn.textContent"), "提交审核 1 条");
|
||
|
||
const adminElements = new Map();
|
||
function adminElement(id) {
|
||
if (!adminElements.has(id)) {
|
||
adminElements.set(id, domNode(id, {
|
||
scrollIntoView() {},
|
||
}));
|
||
}
|
||
return adminElements.get(id);
|
||
}
|
||
|
||
const adminFetchUrls = [];
|
||
const adminContext = {
|
||
console,
|
||
navigator: {},
|
||
Date,
|
||
JSON,
|
||
Map,
|
||
Number,
|
||
String,
|
||
URLSearchParams,
|
||
window: { location: { href: "" } },
|
||
confirm: () => true,
|
||
alert: () => {},
|
||
document: {
|
||
querySelector(selector) {
|
||
return adminElement(selector.replace(/^#/, ""));
|
||
},
|
||
querySelectorAll() {
|
||
return [];
|
||
},
|
||
addEventListener() {},
|
||
createElement(tag) {
|
||
return adminElement(`created-${tag}-${adminElements.size}`);
|
||
},
|
||
body: { appendChild() {} },
|
||
execCommand() {
|
||
return true;
|
||
},
|
||
},
|
||
fetch: async (url) => {
|
||
adminFetchUrls.push(String(url));
|
||
if (String(url).startsWith("/api/students")) {
|
||
return {
|
||
ok: true,
|
||
json: async () => ({
|
||
summary: { total: 1, debt: 0, warning: 0, normal: 1 },
|
||
count: 1,
|
||
students: [
|
||
{
|
||
student_id: "XS001",
|
||
student: "甲",
|
||
primary_entry_year: 2020,
|
||
account_status: "正常",
|
||
remaining: 12,
|
||
remaining_duration: "12小时",
|
||
payments: [{ date: "2026-06-01", hours: 12, duration: "12小时" }],
|
||
note: "备注",
|
||
},
|
||
],
|
||
}),
|
||
};
|
||
}
|
||
return {
|
||
ok: true,
|
||
json: async () => ({
|
||
ok: true,
|
||
students_count: 1,
|
||
active_teachers_count: 0,
|
||
records_count: 0,
|
||
course_summaries_count: 0,
|
||
current_month_hours: 0,
|
||
current_month_duration: "0小时",
|
||
overview: {},
|
||
students: { summary: {}, low_remaining: [] },
|
||
teachers: {},
|
||
course_summaries: {},
|
||
tasks: {},
|
||
teaching: {},
|
||
period: {},
|
||
}),
|
||
};
|
||
},
|
||
};
|
||
|
||
vm.createContext(adminContext);
|
||
vm.runInContext(fs.readFileSync(path.join(staticDir, "admin.js"), "utf8"), adminContext);
|
||
vm.runInContext("loadStudents()", adminContext);
|
||
setTimeout(() => {
|
||
if (!adminFetchUrls.some((url) => url.startsWith("/api/students"))) {
|
||
throw new Error(`学生档案列表未调用 /api/students:${adminFetchUrls.join(",")}`);
|
||
}
|
||
const rowsHtml = adminElements.get("studentRows").innerHTML;
|
||
if (!rowsHtml.includes("XS001") || !rowsHtml.includes("2020")) {
|
||
throw new Error("学生档案列表未渲染学生ID和入学年份");
|
||
}
|
||
vm.runInContext('openEditStudentEditor("XS001")', adminContext);
|
||
const payload = vm.runInContext("buildStudentPayload()", adminContext);
|
||
if (Object.prototype.hasOwnProperty.call(payload, "remaining")) {
|
||
throw new Error("学生档案保存 payload 不应包含 remaining");
|
||
}
|
||
assertEqual("学生档案剩余课时只读展示", adminElements.get("editRemaining").value, "12小时");
|
||
console.log("smoke test passed");
|
||
}, 0);
|