chore: remove backend/test-time-fix-regression.js

This commit is contained in:
2026-04-29 02:08:55 +08:00
parent 54f19459ff
commit b886e37218
-607
View File
@@ -1,607 +0,0 @@
/**
* =============================================
* 时间显示修复 - 完整回归测试脚本
* =============================================
* 测试目标: 验证 createdAt 排序修复和 parseDate 本地时区解析
* 测试环境: 后端 API (localhost:3001)
* 测试日期: 2026-04-26
* =============================================
*/
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const BASE_URL = 'http://localhost:3001';
const USER_ID = 1;
// 测试报告
const report = {
total: 0,
passed: 0,
failed: 0,
warnings: 0,
results: [],
startTime: new Date(),
endTime: null
};
function log(msg, level = 'INFO') {
const icon = { INFO: '📝', PASS: '✅', FAIL: '❌', WARN: '⚠️', HEADER: '📋', SEP: '─' };
const prefix = icon[level] || '📝';
console.log(`${prefix} ${msg}`);
}
function recordTest(name, status, detail = '') {
report.total++;
if (status === 'PASS') {
report.passed++;
log(`${name} - PASS${detail ? ' | ' + detail : ''}`, 'PASS');
} else if (status === 'WARN') {
report.warnings++;
log(`${name} - WARN${detail ? ' | ' + detail : ''}`, 'WARN');
} else {
report.failed++;
log(`${name} - FAIL${detail ? ' | ' + detail : ''}`, 'FAIL');
}
report.results.push({ name, status, detail });
}
// API 请求封装
function apiRequest(method, path, body = null) {
return new Promise((resolve, reject) => {
const url = new URL(path, BASE_URL);
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method,
headers: { 'Content-Type': 'application/json' }
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
resolve({ statusCode: res.statusCode, body: JSON.parse(data) });
} catch (e) {
resolve({ statusCode: res.statusCode, body: data });
}
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
// 创建记录辅助函数
async function createRecord(type, category, amount, description, date = null) {
const payload = {
userId: USER_ID,
accountId: 3,
type,
amount,
category,
description,
date: date || new Date().toISOString().split('T')[0]
};
const result = await apiRequest('POST', '/api/records', payload);
if (result.body.success) {
return result.body.data;
} else {
throw new Error(`创建记录失败: ${result.body.message}`);
}
}
// 获取记录辅助函数
async function getRecords(params = {}) {
const query = new URLSearchParams({ userId: USER_ID, ...params });
const result = await apiRequest('GET', `/api/records?${query}`);
if (result.body.success) {
return result.body.data;
}
return [];
}
// 删除记录辅助函数
async function deleteRecord(id) {
await apiRequest('DELETE', `/api/records/${id}`);
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// =============================================
// 主测试流程
// =============================================
async function runTests() {
console.log('\n' + '='.repeat(60));
console.log('🧪 时间显示修复 - 完整回归测试');
console.log('='.repeat(60));
console.log(`测试开始时间: ${new Date().toLocaleString('zh-CN')}`);
console.log(`后端地址: ${BASE_URL}`);
console.log(`测试用户: userId=${USER_ID}`);
console.log('='.repeat(60) + '\n');
// ==========================================
// 测试1: 验证后端 parseDate 函数
// ==========================================
log('═══════════════════════════════════════════════════════', 'HEADER');
log('【测试1】验证后端 parseDate 函数对纯日期字符串的解析', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
// 创建一条记录,使用纯日期字符串
const now = new Date();
const dateStr = '2026-04-26';
const record1 = await createRecord('expense', '餐饮', 10, 'parseDate测试', dateStr);
const parsedDate = new Date(record1.date);
const expectedHour = 0; // 应该解析为当天 00:00 北京时间
const actualHour = parsedDate.getHours();
log(`输入日期字符串: "${dateStr}"`, 'INFO');
log(`解析结果: ${parsedDate.toLocaleString('zh-CN')}`, 'INFO');
log(`解析结果 ISO: ${parsedDate.toISOString()}`, 'INFO');
log(`本地时区小时: ${actualHour}:00`, 'INFO');
if (actualHour === expectedHour) {
recordTest('parseDate 解析纯日期为本地时区 00:00', 'PASS',
`解析为 ${parsedDate.toLocaleString('zh-CN')} (北京时间 00:00,非 UTC 00:00)`);
} else {
recordTest('parseDate 解析纯日期为本地时区 00:00', 'FAIL',
`期望 00:00,实际 ${actualHour}:00`);
}
// 验证 createdAt 为实际创建时间
const createdAt = new Date(record1.createdAt);
const createdAtDiff = Math.abs(createdAt.getTime() - now.getTime());
log(`记录创建时间 (createdAt): ${createdAt.toLocaleString('zh-CN')}`, 'INFO');
log(`创建时间差: ${createdAtDiff}ms`, 'INFO');
if (createdAtDiff < 5000) {
recordTest('createdAt 反映实际创建时间', 'PASS',
`与当前时间差 ${createdAtDiff}ms < 5s`);
} else {
recordTest('createdAt 反映实际创建时间', 'FAIL',
`与当前时间差 ${createdAtDiff}ms > 5s`);
}
await deleteRecord(record1.id);
// ==========================================
// 测试2: 验证后端 API 返回的 createdAt 字段
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试2】验证后端 API 返回的 createdAt 字段是否正确', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
const beforeCreate = new Date();
await sleep(100);
const record2 = await createRecord('expense', '交通', 20, 'createdAt验证');
const afterCreate = new Date();
const apiCreatedAt = new Date(record2.createdAt);
log(`创建前时间: ${beforeCreate.toLocaleString('zh-CN')}`, 'INFO');
log(`API返回 createdAt: ${apiCreatedAt.toLocaleString('zh-CN')}`, 'INFO');
log(`创建后时间: ${afterCreate.toLocaleString('zh-CN')}`, 'INFO');
const isWithinRange = apiCreatedAt >= beforeCreate && apiCreatedAt <= afterCreate;
if (isWithinRange) {
recordTest('API 返回的 createdAt 在创建时间范围内', 'PASS',
`${apiCreatedAt.toLocaleString('zh-CN')} 在 [${beforeCreate.toLocaleString('zh-CN')}, ${afterCreate.toLocaleString('zh-CN')}] 内`);
} else {
recordTest('API 返回的 createdAt 在创建时间范围内', 'FAIL',
`createdAt 不在预期时间范围内`);
}
// 验证 createdAt 包含时分秒
const hasTimeComponent = record2.createdAt.includes(':');
if (hasTimeComponent) {
recordTest('createdAt 包含时分秒信息', 'PASS', `值: ${record2.createdAt}`);
} else {
recordTest('createdAt 包含时分秒信息', 'FAIL', `值: ${record2.createdAt}`);
}
await deleteRecord(record2.id);
// ==========================================
// 测试3: 验证后端排序逻辑 (orderBy: { createdAt: 'desc' })
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试3】验证后端排序逻辑 (orderBy: createdAt desc)', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
// 清空现有记录,确保测试环境干净
const existingRecords = await getRecords();
for (const r of existingRecords) {
await deleteRecord(r.id);
}
log(`已清理 ${existingRecords.length} 条现有记录`, 'INFO');
// 创建3条同一天但不同时间的记录
const records = [];
for (let i = 0; i < 3; i++) {
const r = await createRecord('expense', '餐饮', 10 + i, `同天记录${i + 1}`);
records.push(r);
log(`创建记录 ${i + 1}: ID=${r.id}, createdAt=${new Date(r.createdAt).toLocaleTimeString('zh-CN')}`, 'INFO');
await sleep(500); // 确保 createdAt 有差异
}
// 获取排序后的记录
const sortedRecords = await getRecords();
log(`\n后端返回的排序顺序:`, 'INFO');
sortedRecords.forEach((r, i) => {
log(` [${i + 1}] ${r.category} ¥${r.amount} ${r.description} | createdAt=${new Date(r.createdAt).toLocaleTimeString('zh-CN')}`, 'INFO');
});
// 验证最新的记录在最前面
const firstRecord = sortedRecords[0];
const lastRecord = sortedRecords[sortedRecords.length - 1];
if (firstRecord && firstRecord.description === '同天记录3') {
recordTest('最新创建的记录排在第一位', 'PASS', `ID=${firstRecord.id}`);
} else {
recordTest('最新创建的记录排在第一位', 'FAIL',
`期望"同天记录3",实际"${firstRecord?.description}"`);
}
// 验证排序是严格降序
let isStrictDescending = true;
for (let i = 0; i < sortedRecords.length - 1; i++) {
const t1 = new Date(sortedRecords[i].createdAt).getTime();
const t2 = new Date(sortedRecords[i + 1].createdAt).getTime();
if (t1 < t2) {
isStrictDescending = false;
break;
}
}
if (isStrictDescending) {
recordTest('后端排序为严格降序', 'PASS', `${sortedRecords.length} 条记录排序正确`);
} else {
recordTest('后端排序为严格降序', 'FAIL', '排序出现异常');
}
// ==========================================
// 测试4: 验证前端 Dashboard 排序逻辑
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试4】验证前端 Dashboard 使用 createdAt 排序', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
// 读取前端代码验证排序逻辑
const dashboardPath = path.join(__dirname, '..', 'frontend', 'src', 'pages', 'Dashboard', 'index.tsx');
if (fs.existsSync(dashboardPath)) {
const dashboardCode = fs.readFileSync(dashboardPath, 'utf-8');
// 检查是否使用 createdAt 排序
const hasCreatedAtSort = dashboardCode.includes('.sort((a, b) => new Date(b.createdAt)');
const hasDateSort = dashboardCode.includes('.sort((a, b) => new Date(b.date)') && !hasCreatedAtSort;
if (hasCreatedAtSort) {
recordTest('Dashboard 使用 createdAt 排序', 'PASS', '代码使用 new Date(b.createdAt) 排序');
} else if (hasDateSort) {
recordTest('Dashboard 使用 createdAt 排序', 'FAIL', '代码仍使用 new Date(b.date) 排序');
} else {
recordTest('Dashboard 使用 createdAt 排序', 'WARN', '未能识别排序逻辑,请手动确认');
}
// 检查 formatRecordTime 函数
const hasFormatRecordTime = dashboardCode.includes('const formatRecordTime');
const usesCreatedAtForTime = dashboardCode.includes('new Date(record.createdAt)');
if (hasFormatRecordTime && usesCreatedAtForTime) {
recordTest('Dashboard 时间格式化使用 createdAt', 'PASS', 'formatRecordTime 使用 record.createdAt');
} else {
recordTest('Dashboard 时间格式化使用 createdAt', 'FAIL', '时间格式化未使用 createdAt');
}
// 检查时间显示逻辑
const showsHHMMForToday = dashboardCode.includes("createdAt.getHours()") && dashboardCode.includes("createdAt.getMinutes()");
if (showsHHMMForToday) {
recordTest('Dashboard 今天显示 HH:MM 格式', 'PASS', '使用 getHours() 和 getMinutes() 格式化');
} else {
recordTest('Dashboard 今天显示 HH:MM 格式', 'FAIL', '未找到 HH:MM 格式化逻辑');
}
const showsYesterday = dashboardCode.includes("'昨天'") || dashboardCode.includes('"昨天"');
if (showsYesterday) {
recordTest('Dashboard 昨天显示"昨天"文本', 'PASS', '包含"昨天"显示逻辑');
} else {
recordTest('Dashboard 昨天显示"昨天"文本', 'WARN', '未找到"昨天"显示逻辑');
}
} else {
recordTest('Dashboard 文件存在性', 'FAIL', `文件不存在: ${dashboardPath}`);
}
// ==========================================
// 测试5: 验证前端 Record 页面排序逻辑
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试5】验证前端 Record 页面使用 createdAt 排序', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
const recordPath = path.join(__dirname, '..', 'frontend', 'src', 'pages', 'Record', 'index.tsx');
if (fs.existsSync(recordPath)) {
const recordCode = fs.readFileSync(recordPath, 'utf-8');
// 检查排序逻辑
const hasCreatedAtSort = recordCode.includes('.sort((a, b) => new Date(b.createdAt)');
const hasDateSort = recordCode.includes('.sort((a, b) => new Date(b.date)') && !hasCreatedAtSort;
if (hasCreatedAtSort) {
recordTest('Record 页面使用 createdAt 排序', 'PASS', '代码使用 new Date(b.createdAt) 排序');
} else if (hasDateSort) {
recordTest('Record 页面使用 createdAt 排序', 'FAIL', '代码仍使用 new Date(b.date) 排序');
} else {
recordTest('Record 页面使用 createdAt 排序', 'WARN', '未能识别排序逻辑');
}
// 检查 formatTime 函数
const hasFormatTime = recordCode.includes('const formatTime');
const usesCreatedAtForTime = recordCode.includes('new Date(record.createdAt)');
if (hasFormatTime && usesCreatedAtForTime) {
recordTest('Record 页面时间格式化使用 createdAt', 'PASS', 'formatTime 使用 record.createdAt');
} else {
recordTest('Record 页面时间格式化使用 createdAt', 'FAIL', '时间格式化未使用 createdAt');
}
// 检查今天显示 HH:MM
const showsHHMM = recordCode.includes("hour: '2-digit'") && recordCode.includes("minute: '2-digit'");
if (showsHHMM) {
recordTest('Record 页面今天显示 HH:MM 格式', 'PASS', '使用 toLocaleTimeString 格式化');
} else {
recordTest('Record 页面今天显示 HH:MM 格式', 'FAIL', '未找到 HH:MM 格式化逻辑');
}
// 检查昨天显示
const showsYesterday = recordCode.includes("'昨天'") || recordCode.includes('"昨天"');
if (showsYesterday) {
recordTest('Record 页面昨天显示"昨天"文本', 'PASS', '包含"昨天"显示逻辑');
} else {
recordTest('Record 页面昨天显示"昨天"文本', 'WARN', '未找到"昨天"显示逻辑');
}
// 检查过滤后再排序
const filterThenSort = recordCode.includes('.filter(') && recordCode.includes('.sort(');
if (filterThenSort) {
recordTest('Record 页面先过滤再排序', 'PASS', '排序在过滤后执行');
} else {
recordTest('Record 页面先过滤再排序', 'WARN', '过滤和排序顺序需确认');
}
} else {
recordTest('Record 文件存在性', 'FAIL', `文件不存在: ${recordPath}`);
}
// ==========================================
// 测试6: 验证同一天多条记录的排序
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试6】验证同一天添加多条记录的排序稳定性', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
// 当前记录应该已经是按 createdAt 降序(来自测试3
// 再添加2条记录,验证最新始终在最前
const extra1 = await createRecord('expense', '购物', 50, '额外记录1');
log(`创建额外记录1: ID=${extra1.id}`, 'INFO');
await sleep(500);
const extra2 = await createRecord('income', '兼职', 100, '额外记录2');
log(`创建额外记录2: ID=${extra2.id}`, 'INFO');
await sleep(500);
const recordsAfterExtra = await getRecords();
log(`\n添加额外记录后的排序:`, 'INFO');
recordsAfterExtra.forEach((r, i) => {
log(` [${i + 1}] ${r.type === 'income' ? '+' : '-'}¥${r.amount} ${r.category} ${r.description} | ${new Date(r.createdAt).toLocaleTimeString('zh-CN')}`, 'INFO');
});
const latestRecord = recordsAfterExtra[0];
if (latestRecord && latestRecord.id === extra2.id) {
recordTest('最新添加的记录始终在最前面', 'PASS', `最新记录: "${latestRecord.description}"`);
} else {
recordTest('最新添加的记录始终在最前面', 'FAIL',
`期望"额外记录2"在最前,实际"${latestRecord?.description}"`);
}
// 验证支出筛选后的排序
const expenseRecords = await getRecords({ type: 'expense' });
let expenseSorted = true;
for (let i = 0; i < expenseRecords.length - 1; i++) {
const t1 = new Date(expenseRecords[i].createdAt).getTime();
const t2 = new Date(expenseRecords[i + 1].createdAt).getTime();
if (t1 < t2) {
expenseSorted = false;
break;
}
}
if (expenseSorted) {
recordTest('支出筛选后仍保持 createdAt 降序', 'PASS', `${expenseRecords.length} 条支出记录排序正确`);
} else {
recordTest('支出筛选后仍保持 createdAt 降序', 'FAIL', '支出排序异常');
}
// 验证收入筛选后的排序
const incomeRecords = await getRecords({ type: 'income' });
let incomeSorted = true;
for (let i = 0; i < incomeRecords.length - 1; i++) {
const t1 = new Date(incomeRecords[i].createdAt).getTime();
const t2 = new Date(incomeRecords[i + 1].createdAt).getTime();
if (t1 < t2) {
incomeSorted = false;
break;
}
}
if (incomeSorted) {
recordTest('收入筛选后仍保持 createdAt 降序', 'PASS', `${incomeRecords.length} 条收入记录排序正确`);
} else {
recordTest('收入筛选后仍保持 createdAt 降序', 'FAIL', '收入排序异常');
}
// ==========================================
// 测试7: 验证前端时间显示逻辑 (模拟)
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试7】验证前端时间显示格式 (模拟前端逻辑)', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
// 模拟前端 formatRecordTime 逻辑 (Dashboard)
function simulateDashboardFormatTime(record) {
const createdAt = new Date(record.createdAt);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const recordDay = new Date(createdAt.getFullYear(), createdAt.getMonth(), createdAt.getDate()).getTime();
const diffDays = Math.floor((today - recordDay) / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return `${createdAt.getHours().toString().padStart(2, '0')}:${createdAt.getMinutes().toString().padStart(2, '0')}`;
} else if (diffDays === 1) {
return '昨天';
} else if (diffDays <= 7) {
return `${diffDays}天前`;
} else {
return `${createdAt.getMonth() + 1}${createdAt.getDate()}`;
}
}
// 模拟前端 formatTime 逻辑 (Record)
function simulateRecordFormatTime(record) {
const createdAt = new Date(record.createdAt);
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toDateString();
const recordDay = new Date(createdAt.getFullYear(), createdAt.getMonth(), createdAt.getDate()).toDateString();
if (todayStr === recordDay) {
return createdAt.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate()).toDateString();
if (yesterdayStr === recordDay) {
return '昨天';
}
return createdAt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
// 使用现有记录测试时间显示
const allRecords = await getRecords();
if (allRecords.length > 0) {
const todayRecord = allRecords.find(r => {
const created = new Date(r.createdAt);
const today = new Date();
return created.getFullYear() === today.getFullYear() &&
created.getMonth() === today.getMonth() &&
created.getDate() === today.getDate();
});
if (todayRecord) {
const dashTime = simulateDashboardFormatTime(todayRecord);
const recordTime = simulateRecordFormatTime(todayRecord);
log(`今天创建的记录: "${todayRecord.description}"`, 'INFO');
log(` Dashboard 显示: "${dashTime}"`, 'INFO');
log(` Record 显示: "${recordTime}"`, 'INFO');
// 验证不是 08:00
if (dashTime !== '08:00') {
recordTest('Dashboard 今天记录不显示 08:00', 'PASS', `显示: "${dashTime}"`);
} else {
recordTest('Dashboard 今天记录不显示 08:00', 'FAIL', `仍显示 "08:00"`);
}
if (recordTime !== '08:00') {
recordTest('Record 页面今天记录不显示 08:00', 'PASS', `显示: "${recordTime}"`);
} else {
recordTest('Record 页面今天记录不显示 08:00', 'FAIL', `仍显示 "08:00"`);
}
// 验证格式为 HH:MM
const dashTimeRegex = /^\d{2}:\d{2}$/;
if (dashTimeRegex.test(dashTime)) {
recordTest('Dashboard 时间格式为 HH:MM', 'PASS', `格式正确: "${dashTime}"`);
} else {
recordTest('Dashboard 时间格式为 HH:MM', 'FAIL', `格式不正确: "${dashTime}"`);
}
} else {
recordTest('今天记录时间显示验证', 'WARN', '当前没有今天的记录,跳过此测试');
}
} else {
recordTest('时间显示验证', 'WARN', '无记录可验证');
}
// ==========================================
// 测试8: 清理测试数据
// ==========================================
log('\n═══════════════════════════════════════════════════════', 'HEADER');
log('【测试8】清理测试数据', 'HEADER');
log('═══════════════════════════════════════════════════════', 'HEADER');
const finalRecords = await getRecords();
for (const r of finalRecords) {
await deleteRecord(r.id);
}
log(`已清理 ${finalRecords.length} 条测试记录`, 'INFO');
recordTest('测试数据清理完成', 'PASS', `清理 ${finalRecords.length} 条记录`);
// ==========================================
// 生成测试报告
// ==========================================
report.endTime = new Date();
const duration = report.endTime - report.startTime;
console.log('\n' + '='.repeat(60));
console.log('📊 测试报告');
console.log('='.repeat(60));
console.log(`测试总数: ${report.total}`);
console.log(`通过: ${report.passed}`);
console.log(`失败: ${report.failed}`);
console.log(`警告: ${report.warnings}`);
console.log(`通过率: ${((report.passed / report.total) * 100).toFixed(1)}%`);
console.log(`耗时: ${duration}ms`);
console.log('='.repeat(60));
console.log('\n📋 测试结果明细:');
console.log('-'.repeat(60));
report.results.forEach((r, i) => {
const icon = r.status === 'PASS' ? '✅' : r.status === 'WARN' ? '⚠️' : '❌';
console.log(` ${icon} [${i + 1}] ${r.name}${r.detail ? ' | ' + r.detail : ''}`);
});
console.log('\n' + '='.repeat(60));
if (report.failed === 0) {
console.log('🎉 全部测试通过!时间显示修复验证成功!');
} else {
console.log(`${report.failed} 项测试失败,请检查修复!`);
}
console.log('='.repeat(60) + '\n');
return report;
}
// 执行测试
runTests().catch(err => {
console.error('测试执行异常:', err);
process.exit(1);
});