test(docs): 补齐隔离验收与功能实施记录

This commit is contained in:
LoveChengke committed 2026-10-17 13:04:22 +08:00
1 parent 57069a9a75
commit ead66f0b93
14 files changed
+899 -40

No files matched your search

+50
View File
@@ -1188,6 +1188,7 @@ async function main() {
// 7.9 管理员专属:教师录入(本轮新增「教师管理」页,仅管理员可见可用)
await runAdminTeacherChecks(win);
await runPlanEditorChecks(win);
// 8. 手机小屏适配(390×844,1Panel 风格:侧边栏收进抽屉 + 卡片内横向滚动)
await runMobileChecks(win);
@@ -1435,6 +1436,55 @@ async function runMobileChecks(win) {
win.hide();
}
/** 新页面用真实点击进入,仅保存临时验收班级的可逆字段。 */
async function runPlanEditorChecks(win) {
await win.loadURL(new URL('/plan-editor', TARGET_URL).href);
const ready = await waitFor(
win,
`document.body.innerText.includes('星期 × 时间点') && Boolean(document.querySelector('.editor-table tbody tr'))`,
);
record('日课表编辑器周视图与显式时间点行可用', ready);
const save = await win.webContents.executeJavaScript(
`(() => {const b=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='保存日课表');b?.click();return Boolean(b&&!b.disabled);})()`,
);
const saved = save && (await waitFor(win, `document.body.innerText.includes('整张日课表已保存')`));
record('日课表整表保存可以从页面提交', saved);
await win.webContents.executeJavaScript(
`(() => {[...document.querySelectorAll('[role=tab]')].find(e=>e.textContent.includes('时间表'))?.click();})()`,
);
record(
'时间表页四种时间点与管理员保存入口可见',
await waitFor(
win,
`document.body.innerText.includes('添加时间点') && document.body.innerText.includes('保存时间表')`,
),
);
await win.loadURL(new URL('/system-settings', TARGET_URL).href);
record(
'开发环境自启动明确显示不可用原因',
await waitFor(
win,
`document.body.innerText.includes('开发环境') && Boolean(document.querySelector('.el-switch.is-disabled'))`,
),
);
await win.loadURL(new URL('/term-weeks', TARGET_URL).href);
record(
'学年升级设置与待补跑提示可见且不执行升级',
await waitFor(
win,
`document.body.innerText.includes('每年自动学年升级') && document.body.innerText.includes('总周数')`,
),
);
await win.loadURL(new URL('/archives', TARGET_URL).href);
record(
'归档页有转出分区且无手动归档入口',
await waitFor(
win,
`document.body.innerText.includes('已转出') && ![...document.querySelectorAll('button')].some(e=>e.textContent.trim()==='新建归档')`,
),
);
}
async function finish(win) {
record('全程后台结束(窗口隐藏且未聚焦)', !win.isVisible() && !win.isFocused());
const failed = results.filter((item) => !item.ok);
+18
View File
@@ -0,0 +1,18 @@
/** 仅供隔离验收:不启动年度升级、备份、PID 或其它后台运维任务。 */
import { createServer } from 'node:http';
import { createApp } from '../packages/server/src/app.js';
import { initRealtime } from '../packages/server/src/realtime/socket.js';
if (!process.env.DATABASE_URL?.includes('v14-verification'))
throw new Error('验收服务只能使用专用临时数据库');
const localFetch = globalThis.fetch;
globalThis.fetch = (input, init) => {
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input : input.url);
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))
return Promise.reject(new Error('隔离验收禁止访问外网'));
return localFetch(input, init);
};
const server = createServer(createApp());
initRealtime(server);
server.listen(Number(process.env.PORT || 4105), '127.0.0.1', () =>
console.log(`验收服务已就绪:${process.env.PORT || 4105}`),
);
+113
View File
@@ -0,0 +1,113 @@
/** 旧回归也必须隔离:临时库、带前缀的临时教师与班级,绝不回退真实实例。 */
import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
dir = path.join(root, '.cache', 'v14-verification', `e2e-${process.pid}-${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
const env = {
...process.env,
DATABASE_PROVIDER: 'sqlite',
DATABASE_URL: `file:${path.join(dir, 'test.db').replaceAll('\\', '/')}`,
TZ: 'Asia/Shanghai',
RATE_LIMIT_ENABLED: 'false',
LOG_LEVEL: 'warn',
TERM_START_DATE: '2026-09-07',
};
const require = createRequire(path.join(root, 'packages/server/package.json')),
{ createClient } = require('@libsql/client');
const db = createClient({ url: env.DATABASE_URL });
await db.execute('PRAGMA user_version=0');
db.close();
function run(args, logName) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
cwd: root,
env,
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
});
const log = fs.createWriteStream(path.join(dir, logName));
for (const stream of [child.stdout, child.stderr])
stream.on('data', (chunk) => {
log.write(chunk);
process.stdout.write(chunk);
});
child.once('error', reject);
child.once('exit', (code) => {
log.end();
if (code === 0) resolve();
else reject(new Error(`${logName} 退出码 ${code},日志 ${dir}`));
});
});
}
const seedFile = path.join(root, 'packages/server/prisma', `.verification-seed-${process.pid}.ts`),
testFile = path.join(root, 'packages/server/scripts', `.verification-e2e-${process.pid}.mjs`);
const prefix = `temporary${process.pid}_`;
function namespace(code) {
for (const name of ['admin', 'teacher1', 'teacher2'])
code = code.replaceAll(`'${name}'`, `'${prefix}${name}'`);
return code;
}
let server;
try {
await run(
[
'packages/server/node_modules/prisma/build/index.js',
'migrate',
'deploy',
'--config',
'packages/server/prisma.config.ts',
],
'migrate.log',
);
fs.writeFileSync(
seedFile,
namespace(fs.readFileSync(path.join(root, 'packages/server/prisma/seed.ts'), 'utf8')),
{ flag: 'wx' },
);
await run(['packages/server/node_modules/tsx/dist/cli.mjs', seedFile], 'seed.log');
const probe = net.createServer();
await new Promise((r) => probe.listen(0, '127.0.0.1', r));
env.PORT = String(probe.address().port);
await new Promise((r) => probe.close(r));
env.VERIFY_BASE_URL = `http://127.0.0.1:${env.PORT}`;
server = spawn(
process.execPath,
['--import', './packages/server/node_modules/tsx/dist/loader.mjs', 'scripts/verification-server.mts'],
{ cwd: root, env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] },
);
const log = fs.createWriteStream(path.join(dir, 'server.log'));
server.stdout.pipe(log);
server.stderr.pipe(log);
server.once('exit', (code, signal) => console.log(`临时服务退出:code=${code} signal=${signal}`));
let ready = false;
const startupDeadline = Date.now() + 60_000;
while (Date.now() < startupDeadline) {
if (server.exitCode !== null) throw new Error('临时服务启动失败');
try {
ready = (await fetch(`${env.VERIFY_BASE_URL}/healthz`, { signal: AbortSignal.timeout(1500) })).ok;
} catch {
/* 等待进程启动 */
}
if (ready) break;
await new Promise((r) => setTimeout(r, 100));
}
if (!ready) throw new Error('临时服务未就绪');
fs.writeFileSync(
testFile,
namespace(fs.readFileSync(path.join(root, 'packages/server/scripts/verify-e2e.mjs'), 'utf8')),
{ flag: 'wx' },
);
await run([testFile], 'e2e.log');
} finally {
if (server && server.exitCode === null) {
server.kill();
await new Promise((r) => server.once('exit', r));
}
for (const file of [seedFile, testFile]) if (fs.existsSync(file)) fs.unlinkSync(file);
}
console.log(`隔离端到端日志:${dir}`);
+37
View File
@@ -0,0 +1,37 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const dir = path.join(root, '.cache', 'v14-verification', `run-${process.pid}-${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
const env = {
...process.env,
DATABASE_PROVIDER: 'sqlite',
DATABASE_URL: `file:${path.join(dir, 'test.db').replaceAll('\\', '/')}`,
TZ: 'Asia/Shanghai',
RATE_LIMIT_ENABLED: 'false',
LOG_LEVEL: 'warn',
};
const require = createRequire(path.join(root, 'packages/server/package.json'));
const { createClient } = require('@libsql/client');
const database = createClient({ url: env.DATABASE_URL });
await database.execute('PRAGMA user_version=0');
database.close();
function run(args) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, { cwd: root, env, stdio: 'inherit', windowsHide: true });
child.once('error', reject);
child.once('exit', (code) => (code === 0 ? resolve() : reject(new Error(`验收子进程失败:${code}`))));
});
}
await run([
'packages/server/node_modules/prisma/build/index.js',
'migrate',
'deploy',
'--config',
'packages/server/prisma.config.ts',
]);
await run(['packages/server/node_modules/tsx/dist/cli.mjs', 'packages/server/scripts/verify-v14.ts']);
console.log(`隔离验收数据库:${dir}`);