chore: remove backend/test-sort-regression.js
This commit is contained in:
@@ -1,391 +0,0 @@
|
|||||||
/**
|
|
||||||
* 回归测试脚本:账单倒序排序功能完整验证
|
|
||||||
*
|
|
||||||
* 测试场景:
|
|
||||||
* 1. 添加5条支出记录(餐饮、交通、购物、娱乐、医疗),每条间隔1秒
|
|
||||||
* 2. 添加5条收入记录(工资、奖金、投资、兼职、理财),每条间隔1秒
|
|
||||||
* 3. 验证首页最近5条记录按createdAt倒序
|
|
||||||
* 4. 验证记账页面所有记录按createdAt倒序
|
|
||||||
* 5. 验证筛选后仍保持倒序
|
|
||||||
* 6. 同一秒内添加2条记录,验证排序稳定性
|
|
||||||
* 7. 删除中间记录后验证剩余记录排序
|
|
||||||
* 8. 跨天记录验证排序
|
|
||||||
*
|
|
||||||
* 测试环境:
|
|
||||||
* - 后端: http://localhost:3001
|
|
||||||
* - 用户ID: 1
|
|
||||||
* - 账户ID: 3 (招商银行)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import http from 'http';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:3001';
|
|
||||||
const USER_ID = 1;
|
|
||||||
const ACCOUNT_ID = 3;
|
|
||||||
const SCREENSHOT_DIR = 'd:\\Users\\kaifa\\Trae_cn260425\\test-screenshots\\sort-fix-regression';
|
|
||||||
|
|
||||||
// 测试结果收集
|
|
||||||
const testResults = [];
|
|
||||||
let testCounter = 0;
|
|
||||||
|
|
||||||
function recordTest(name, status, details = '') {
|
|
||||||
testCounter++;
|
|
||||||
testResults.push({
|
|
||||||
id: testCounter,
|
|
||||||
name,
|
|
||||||
status,
|
|
||||||
details,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⚠️';
|
|
||||||
console.log(` ${icon} [TC-${testCounter}] ${name}: ${status}${details ? ' - ' + details : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function apiRequest(method, urlPath, body = null) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const url = new URL(urlPath, 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({ status: res.statusCode, body: JSON.parse(data) });
|
|
||||||
} catch (e) {
|
|
||||||
resolve({ status: res.statusCode, body: data });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
if (body) req.write(JSON.stringify(body));
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
function logSeparator(title) {
|
|
||||||
console.log(`\n${'='.repeat(70)}`);
|
|
||||||
if (title) console.log(` ${title}`);
|
|
||||||
console.log('='.repeat(70));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createRecord(type, category, amount, description, date) {
|
|
||||||
const result = await apiRequest('POST', '/api/records', {
|
|
||||||
userId: USER_ID,
|
|
||||||
accountId: ACCOUNT_ID,
|
|
||||||
type,
|
|
||||||
amount,
|
|
||||||
category,
|
|
||||||
description,
|
|
||||||
date: date || new Date().toISOString().split('T')[0]
|
|
||||||
});
|
|
||||||
return result.body.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRecords(filterType = null) {
|
|
||||||
let url = `/api/records?userId=${USER_ID}`;
|
|
||||||
if (filterType) url += `&type=${filterType}`;
|
|
||||||
const result = await apiRequest('GET', url);
|
|
||||||
return result.body.data || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteRecord(id) {
|
|
||||||
const result = await apiRequest('DELETE', `/api/records/${id}`);
|
|
||||||
return result.status === 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 测试执行 ====================
|
|
||||||
|
|
||||||
async function runTests() {
|
|
||||||
// 创建截图目录
|
|
||||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
|
||||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
logSeparator('🚀 账单倒序排序功能 - 完整回归测试');
|
|
||||||
console.log(` 测试时间: ${new Date().toLocaleString('zh-CN')}`);
|
|
||||||
console.log(` 测试环境: ${BASE_URL}`);
|
|
||||||
console.log(` 用户ID: ${USER_ID} | 账户ID: ${ACCOUNT_ID}`);
|
|
||||||
|
|
||||||
// ---- 测试1: 添加5条支出记录 ----
|
|
||||||
logSeparator('📝 测试1: 添加5条支出记录(每条间隔1秒)');
|
|
||||||
|
|
||||||
const expenseRecords = [
|
|
||||||
{ category: '餐饮', amount: 35, description: '午餐外卖' },
|
|
||||||
{ category: '交通', amount: 15, description: '地铁通勤' },
|
|
||||||
{ category: '购物', amount: 199, description: '日用品采购' },
|
|
||||||
{ category: '娱乐', amount: 68, description: '电影票' },
|
|
||||||
{ category: '医疗', amount: 120, description: '感冒药' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const createdExpenseIds = [];
|
|
||||||
for (const exp of expenseRecords) {
|
|
||||||
const record = await createRecord('expense', exp.category, exp.amount, exp.description);
|
|
||||||
createdExpenseIds.push(record.id);
|
|
||||||
console.log(` ➕ 支出: ${exp.category} ¥${exp.amount} | ID:${record.id} | createdAt:${new Date(record.createdAt).toLocaleString('zh-CN')}`);
|
|
||||||
await sleep(1100); // 间隔1.1秒确保createdAt不同
|
|
||||||
}
|
|
||||||
recordTest('添加5条支出记录', 'PASS', `IDs: ${createdExpenseIds.join(', ')}`);
|
|
||||||
|
|
||||||
// ---- 测试2: 添加5条收入记录 ----
|
|
||||||
logSeparator('📝 测试2: 添加5条收入记录(每条间隔1秒)');
|
|
||||||
|
|
||||||
const incomeRecords = [
|
|
||||||
{ category: '工资', amount: 12000, description: '4月工资' },
|
|
||||||
{ category: '奖金', amount: 2000, description: '季度奖金' },
|
|
||||||
{ category: '投资', amount: 500, description: '基金收益' },
|
|
||||||
{ category: '兼职', amount: 800, description: '技术咨询' },
|
|
||||||
{ category: '理财', amount: 300, description: '银行理财到期' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const createdIncomeIds = [];
|
|
||||||
for (const inc of incomeRecords) {
|
|
||||||
const record = await createRecord('income', inc.category, inc.amount, inc.description);
|
|
||||||
createdIncomeIds.push(record.id);
|
|
||||||
console.log(` ➕ 收入: ${inc.category} ¥${inc.amount} | ID:${record.id} | createdAt:${new Date(record.createdAt).toLocaleString('zh-CN')}`);
|
|
||||||
await sleep(1100);
|
|
||||||
}
|
|
||||||
recordTest('添加5条收入记录', 'PASS', `IDs: ${createdIncomeIds.join(', ')}`);
|
|
||||||
|
|
||||||
// ---- 测试3: 验证API返回按createdAt倒序 ----
|
|
||||||
logSeparator('🔍 测试3: 验证API返回按createdAt倒序');
|
|
||||||
|
|
||||||
const allRecords = await getRecords();
|
|
||||||
console.log(` 📊 总记录数: ${allRecords.length}`);
|
|
||||||
console.log(` 📋 前10条记录:`);
|
|
||||||
allRecords.slice(0, 10).forEach((r, i) => {
|
|
||||||
const isNew = createdExpenseIds.includes(r.id) || createdIncomeIds.includes(r.id) ? ' ⬅️ 新' : '';
|
|
||||||
console.log(` [${i+1}] ${r.type==='income'?'收入':'支出'} ${r.category} ¥${r.amount} | createdAt: ${new Date(r.createdAt).toLocaleString('zh-CN')}${isNew}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查createdAt是否严格降序
|
|
||||||
let sortedCorrectly = true;
|
|
||||||
for (let i = 0; i < allRecords.length - 1; i++) {
|
|
||||||
if (new Date(allRecords[i].createdAt).getTime() < new Date(allRecords[i+1].createdAt).getTime()) {
|
|
||||||
sortedCorrectly = false;
|
|
||||||
console.log(` ❌ 排序异常: [${i+1}] ${new Date(allRecords[i].createdAt).toISOString()} < [${i+2}] ${new Date(allRecords[i+1].createdAt).toISOString()}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recordTest('API按createdAt倒序', sortedCorrectly ? 'PASS' : 'FAIL', `共${allRecords.length}条记录`);
|
|
||||||
|
|
||||||
// 验证最新创建的记录在第一位
|
|
||||||
const lastCreatedId = createdIncomeIds[createdIncomeIds.length - 1]; // 最后一条收入
|
|
||||||
const firstRecord = allRecords[0];
|
|
||||||
recordTest('最新记录在API第一位', firstRecord.id === lastCreatedId ? 'PASS' : 'FAIL',
|
|
||||||
`预期ID:${lastCreatedId}, 实际ID:${firstRecord.id}`);
|
|
||||||
|
|
||||||
// ---- 测试4: 验证首页最近5条记录 ----
|
|
||||||
logSeparator('🔍 测试4: 验证首页最近5条记录(前5条)');
|
|
||||||
|
|
||||||
const recent5 = allRecords.slice(0, 5);
|
|
||||||
console.log(` 📋 最近5条记录:`);
|
|
||||||
recent5.forEach((r, i) => {
|
|
||||||
console.log(` [${i+1}] ${r.type==='income'?'收入':'支出'} ${r.category} ¥${r.amount} | ${new Date(r.createdAt).toLocaleTimeString('zh-CN')}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 验证前5条也是按createdAt降序
|
|
||||||
let recent5Sorted = true;
|
|
||||||
for (let i = 0; i < recent5.length - 1; i++) {
|
|
||||||
if (new Date(recent5[i].createdAt).getTime() < new Date(recent5[i+1].createdAt).getTime()) {
|
|
||||||
recent5Sorted = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recordTest('首页最近5条按createdAt倒序', recent5Sorted ? 'PASS' : 'FAIL');
|
|
||||||
|
|
||||||
// ---- 测试5: 支出筛选后排序验证 ----
|
|
||||||
logSeparator('🔍 测试5: 支出筛选后排序验证');
|
|
||||||
|
|
||||||
const expenseFiltered = await getRecords('expense');
|
|
||||||
console.log(` 📊 支出记录数: ${expenseFiltered.length}`);
|
|
||||||
|
|
||||||
let expenseSorted = true;
|
|
||||||
for (let i = 0; i < expenseFiltered.length - 1; i++) {
|
|
||||||
if (new Date(expenseFiltered[i].createdAt).getTime() < new Date(expenseFiltered[i+1].createdAt).getTime()) {
|
|
||||||
expenseSorted = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` 📋 前5条支出:`);
|
|
||||||
expenseFiltered.slice(0, 5).forEach((r, i) => {
|
|
||||||
console.log(` [${i+1}] ${r.category} ¥${r.amount} | ${new Date(r.createdAt).toLocaleTimeString('zh-CN')}`);
|
|
||||||
});
|
|
||||||
recordTest('支出筛选后按createdAt倒序', expenseSorted ? 'PASS' : 'FAIL');
|
|
||||||
|
|
||||||
// ---- 测试6: 收入筛选后排序验证 ----
|
|
||||||
logSeparator('🔍 测试6: 收入筛选后排序验证');
|
|
||||||
|
|
||||||
const incomeFiltered = await getRecords('income');
|
|
||||||
console.log(` 📊 收入记录数: ${incomeFiltered.length}`);
|
|
||||||
|
|
||||||
let incomeSorted = true;
|
|
||||||
for (let i = 0; i < incomeFiltered.length - 1; i++) {
|
|
||||||
if (new Date(incomeFiltered[i].createdAt).getTime() < new Date(incomeFiltered[i+1].createdAt).getTime()) {
|
|
||||||
incomeSorted = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` 📋 前5条收入:`);
|
|
||||||
incomeFiltered.slice(0, 5).forEach((r, i) => {
|
|
||||||
console.log(` [${i+1}] ${r.category} ¥${r.amount} | ${new Date(r.createdAt).toLocaleTimeString('zh-CN')}`);
|
|
||||||
});
|
|
||||||
recordTest('收入筛选后按createdAt倒序', incomeSorted ? 'PASS' : 'FAIL');
|
|
||||||
|
|
||||||
// ---- 测试7: 同一秒内添加2条记录 ----
|
|
||||||
logSeparator('🔍 测试7: 同一秒内添加2条记录(排序稳定性)');
|
|
||||||
|
|
||||||
const sameTime1 = await createRecord('expense', '餐饮', 10, '同时记录1');
|
|
||||||
const sameTime2 = await createRecord('expense', '交通', 20, '同时记录2');
|
|
||||||
console.log(` ➕ 记录1: ID:${sameTime1.id} | createdAt:${new Date(sameTime1.createdAt).toISOString()}`);
|
|
||||||
console.log(` ➕ 记录2: ID:${sameTime2.id} | createdAt:${new Date(sameTime2.createdAt).toISOString()}`);
|
|
||||||
|
|
||||||
const recordsAfterSameTime = await getRecords('expense');
|
|
||||||
const idx1 = recordsAfterSameTime.findIndex(r => r.id === sameTime1.id);
|
|
||||||
const idx2 = recordsAfterSameTime.findIndex(r => r.id === sameTime2.id);
|
|
||||||
console.log(` 记录1位置: 第${idx1+1}位 | 记录2位置: 第${idx2+1}位`);
|
|
||||||
|
|
||||||
// SQLite的createdAt由数据库自动生成,即使同一秒也应该有微小差异或保持插入顺序
|
|
||||||
// 只要不出现排序混乱即可
|
|
||||||
recordTest('同秒记录排序稳定性', 'PASS', `记录1位置:${idx1+1}, 记录2位置:${idx2+1} (同秒允许顺序不定)`);
|
|
||||||
|
|
||||||
// ---- 测试8: 删除中间记录后排序验证 ----
|
|
||||||
logSeparator('🔍 测试8: 删除中间记录后排序验证');
|
|
||||||
|
|
||||||
// 删除第3条创建的支出记录(购物)
|
|
||||||
const deleteTargetId = createdExpenseIds[2]; // 购物记录
|
|
||||||
const beforeDelete = await getRecords();
|
|
||||||
const deleteTargetIndex = beforeDelete.findIndex(r => r.id === deleteTargetId);
|
|
||||||
console.log(` 🗑️ 删除记录: ID:${deleteTargetId} (${beforeDelete[deleteTargetIndex]?.category} ¥${beforeDelete[deleteTargetIndex]?.amount})`);
|
|
||||||
|
|
||||||
const deleteSuccess = await deleteRecord(deleteTargetId);
|
|
||||||
recordTest('删除记录', deleteSuccess ? 'PASS' : 'FAIL', `ID:${deleteTargetId}`);
|
|
||||||
|
|
||||||
const afterDelete = await getRecords();
|
|
||||||
let afterDeleteSorted = true;
|
|
||||||
for (let i = 0; i < afterDelete.length - 1; i++) {
|
|
||||||
if (new Date(afterDelete[i].createdAt).getTime() < new Date(afterDelete[i+1].createdAt).getTime()) {
|
|
||||||
afterDeleteSorted = false;
|
|
||||||
console.log(` ❌ 排序异常: [${i+1}] < [${i+2}]`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(` 📊 删除后记录数: ${afterDelete.length} (原${beforeDelete.length}条)`);
|
|
||||||
recordTest('删除后剩余记录排序正确', afterDeleteSorted ? 'PASS' : 'FAIL');
|
|
||||||
|
|
||||||
// ---- 测试9: 跨天记录排序验证 ----
|
|
||||||
logSeparator('🔍 测试9: 跨天记录排序验证');
|
|
||||||
|
|
||||||
// 创建昨天的记录
|
|
||||||
const yesterday = new Date();
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
const yesterdayRecord = await createRecord('expense', '餐饮', 50, '昨天晚餐', yesterdayStr);
|
|
||||||
console.log(` ➕ 昨天记录: ${yesterdayRecord.category} ¥${yesterdayRecord.amount} | date:${yesterdayStr} | createdAt:${new Date(yesterdayRecord.createdAt).toLocaleString('zh-CN')}`);
|
|
||||||
|
|
||||||
const recordsWithYesterday = await getRecords();
|
|
||||||
const yesterdayIdx = recordsWithYesterday.findIndex(r => r.id === yesterdayRecord.id);
|
|
||||||
console.log(` 昨天记录位置: 第${yesterdayIdx+1}位 (共${recordsWithYesterday.length}条)`);
|
|
||||||
|
|
||||||
// 昨天的记录应该排在今天的记录后面
|
|
||||||
const todayRecords = recordsWithYesterday.filter(r => {
|
|
||||||
const createdDate = new Date(r.createdAt).toLocaleDateString('zh-CN');
|
|
||||||
const todayDate = new Date().toLocaleDateString('zh-CN');
|
|
||||||
return createdDate === todayDate;
|
|
||||||
});
|
|
||||||
|
|
||||||
const yesterdayRecordsAfter = recordsWithYesterday.filter(r => {
|
|
||||||
const createdDate = new Date(r.createdAt).toLocaleDateString('zh-CN');
|
|
||||||
const todayDate = new Date().toLocaleDateString('zh-CN');
|
|
||||||
return createdDate !== todayDate;
|
|
||||||
});
|
|
||||||
|
|
||||||
let crossDaySorted = true;
|
|
||||||
// 验证所有今天的记录都在昨天的记录前面
|
|
||||||
if (todayRecords.length > 0 && yesterdayRecordsAfter.length > 0) {
|
|
||||||
const lastToday = new Date(todayRecords[todayRecords.length - 1].createdAt).getTime();
|
|
||||||
const firstYesterday = new Date(yesterdayRecordsAfter[0].createdAt).getTime();
|
|
||||||
if (lastToday < firstYesterday) {
|
|
||||||
crossDaySorted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recordTest('跨天记录排序正确', crossDaySorted ? 'PASS' : 'FAIL',
|
|
||||||
`今天:${todayRecords.length}条 | 昨天:${yesterdayRecordsAfter.length}条`);
|
|
||||||
|
|
||||||
// ---- 清理测试数据 ----
|
|
||||||
logSeparator('🧹 清理测试数据');
|
|
||||||
|
|
||||||
const allCreatedIds = [...createdExpenseIds.slice(0, 2), ...createdExpenseIds.slice(3), ...createdIncomeIds, sameTime1.id, sameTime2.id, yesterdayRecord.id];
|
|
||||||
// 注意: createdExpenseIds[2] (购物) 已经删除了
|
|
||||||
|
|
||||||
for (const id of allCreatedIds) {
|
|
||||||
await deleteRecord(id);
|
|
||||||
}
|
|
||||||
console.log(` ✅ 已清理 ${allCreatedIds.length} 条测试记录`);
|
|
||||||
|
|
||||||
// ---- 生成测试报告 ----
|
|
||||||
logSeparator('📊 测试报告');
|
|
||||||
|
|
||||||
const passCount = testResults.filter(r => r.status === 'PASS').length;
|
|
||||||
const failCount = testResults.filter(r => r.status === 'FAIL').length;
|
|
||||||
const totalCount = testResults.length;
|
|
||||||
|
|
||||||
console.log(`\n 总测试用例: ${totalCount}`);
|
|
||||||
console.log(` ✅ 通过: ${passCount}`);
|
|
||||||
console.log(` ❌ 失败: ${failCount}`);
|
|
||||||
console.log(` 通过率: ${((passCount / totalCount) * 100).toFixed(1)}%`);
|
|
||||||
|
|
||||||
console.log(`\n 详细结果:`);
|
|
||||||
testResults.forEach(r => {
|
|
||||||
const icon = r.status === 'PASS' ? '✅' : '❌';
|
|
||||||
console.log(` ${icon} [TC-${r.id}] ${r.name}${r.details ? ': ' + r.details : ''}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存测试报告到文件
|
|
||||||
const reportPath = path.join(SCREENSHOT_DIR, 'test-report.json');
|
|
||||||
const reportData = {
|
|
||||||
testDate: new Date().toISOString(),
|
|
||||||
environment: {
|
|
||||||
backendUrl: BASE_URL,
|
|
||||||
userId: USER_ID,
|
|
||||||
accountId: ACCOUNT_ID
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
total: totalCount,
|
|
||||||
passed: passCount,
|
|
||||||
failed: failCount,
|
|
||||||
passRate: `${((passCount / totalCount) * 100).toFixed(1)}%`
|
|
||||||
},
|
|
||||||
testCases: testResults
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
|
|
||||||
console.log(`\n 📁 测试报告已保存: ${reportPath}`);
|
|
||||||
|
|
||||||
if (failCount > 0) {
|
|
||||||
console.log(`\n ⚠️ 【高危】存在${failCount}个失败用例,建议修复后重新测试`);
|
|
||||||
} else {
|
|
||||||
console.log(`\n 🎉 所有测试用例通过!账单倒序排序功能修复验证成功`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests().catch(err => {
|
|
||||||
console.error('测试执行失败:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user