65b0bb04f8
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline: - server/src/ (13 个路由 + middleware + services + config) - server/migrations/ (0001~0018 共 18 个迁移 + mysql) - server/test/ (12 文件 101 测试) - client/src/ (20 个 view + components + stores + api + composables) - client/public/ + client/scripts/ - 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql) - .husky/pre-commit (git hooks) - docs/install/ (宝塔部署文档) 不含: - node_modules/ (本地 npm install) - .env (敏感, 走 .env.example) - *.zip / *.log / *.sqlite / .DS_Store 新增文档 docs/DEV-PLAN.md: - Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view) - Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单) - Phase 3: 验证 (测试 + E2E + DB 完整性) - 交付清单 + commit 模板 + 给 Mavis review 的材料 后续 Trae 实施, 提交后我 code review + 跑测试。
134 lines
5.7 KiB
JavaScript
134 lines
5.7 KiB
JavaScript
// server/test/routes.extra.test.js — v2.8 高 ROI 三件套测试
|
|
// Trae 加的 reminders / cost-breakdown / search / compare
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
|
|
vi.mock('../src/db.js', () => ({
|
|
db: () => ({
|
|
all: vi.fn(async (sql, params = []) => {
|
|
if (/notification_prefs/.test(sql)) {
|
|
return [
|
|
{ key_name: 'refuel_remind_days', days: 30, enabled: 1 },
|
|
{ key_name: 'maintenance_remind_days', days: 180, enabled: 1 },
|
|
{ key_name: 'wash_remind_days', days: 14, enabled: 1 },
|
|
];
|
|
}
|
|
if (/FROM vehicles v[\s\S]*LEFT JOIN refuel_records/.test(sql)) {
|
|
// 给车辆 1 返 last_date = 60 天前(需要加油)
|
|
// 车辆 2 没记录
|
|
return [
|
|
{ vehicle_id: 1, name: '我的 Tiguan', plate: '粤B12345', is_active: 1, last_date: '2026-04-15' },
|
|
{ vehicle_id: 2, name: '测试车', plate: null, is_active: 1, last_date: null },
|
|
];
|
|
}
|
|
if (/FROM vehicles v[\s\S]*LEFT JOIN maintenance_records/.test(sql)) {
|
|
return [
|
|
{ vehicle_id: 1, name: '我的 Tiguan', plate: '粤B12345', last_date: '2025-12-01' }, // >180 天前
|
|
{ vehicle_id: 2, name: '测试车', plate: null, last_date: null },
|
|
];
|
|
}
|
|
if (/FROM vehicles v[\s\S]*LEFT JOIN wash_records/.test(sql)) {
|
|
return [
|
|
{ vehicle_id: 1, name: '我的 Tiguan', plate: '粤B12345', last_date: '2026-06-01' }, // 18 天前
|
|
{ vehicle_id: 2, name: '测试车', plate: null, last_date: null },
|
|
];
|
|
}
|
|
if (/FROM wash_records/.test(sql) && /FROM vehicles/.test(sql) === false) {
|
|
// search 用
|
|
if (/grocy_product_id/.test(sql)) return [];
|
|
if (/insurance_records/.test(sql)) return [];
|
|
if (/maintenance_records/.test(sql)) return [];
|
|
if (/charging_records/.test(sql)) return [];
|
|
if (/refuel_records/.test(sql)) return [];
|
|
if (/wash_records/.test(sql)) return [];
|
|
return [];
|
|
}
|
|
return [];
|
|
}),
|
|
get: vi.fn(async (sql) => {
|
|
if (/SUM.*cost/.test(sql)) return { total: 1000 };
|
|
if (/SUM.*total_cost/.test(sql)) return { total: 5000 };
|
|
if (/SUM.*premium/.test(sql)) return { total: 2000 };
|
|
if (/FROM wash_records WHERE is_deleted = 0/.test(sql) && !/JOIN/.test(sql)) return { total: 1000, cnt: 5 };
|
|
return { total: 0, cnt: 0 };
|
|
}),
|
|
run: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
import extraRouter from '../src/routes/extra.js';
|
|
|
|
describe('GET /api/reminders', () => {
|
|
let app;
|
|
beforeEach(() => { app = express(); app.use('/api', extraRouter); });
|
|
|
|
it('返 {ok, data} 包装', async () => {
|
|
const r = await request(app).get('/api/reminders');
|
|
expect(r.status).toBe(200);
|
|
expect(r.body.ok).toBe(true);
|
|
expect(r.body.data).toBeDefined();
|
|
});
|
|
|
|
it('包含 items + prefs', async () => {
|
|
const r = await request(app).get('/api/reminders');
|
|
expect(Array.isArray(r.body.data.items)).toBe(true);
|
|
expect(r.body.data.prefs).toHaveProperty('refuel');
|
|
expect(r.body.data.prefs.refuel.days).toBe(30);
|
|
});
|
|
|
|
it('加油提醒超过 30 天触发', async () => {
|
|
const r = await request(app).get('/api/reminders');
|
|
const refuelReminders = r.body.data.items.filter(it => it.type === 'refuel' && it.days !== null);
|
|
expect(refuelReminders.length).toBeGreaterThan(0);
|
|
expect(refuelReminders[0].days).toBeGreaterThan(30);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/stats/cost-breakdown', () => {
|
|
let app;
|
|
beforeEach(() => { app = express(); app.use('/api', extraRouter); });
|
|
|
|
it('返 5 个分类 + 百分比合计 100', async () => {
|
|
const r = await request(app).get('/api/stats/cost-breakdown');
|
|
expect(r.status).toBe(200);
|
|
expect(r.body.ok).toBe(true);
|
|
const cats = r.body.data.categories;
|
|
expect(cats).toHaveLength(5);
|
|
const sumPct = cats.reduce((s, c) => s + c.pct, 0);
|
|
// 允许 0.1 误差(4 舍 5 入)
|
|
expect(Math.abs(sumPct - 100)).toBeLessThan(1);
|
|
});
|
|
|
|
it('分类包含 label + key + total + pct + color', async () => {
|
|
const r = await request(app).get('/api/stats/cost-breakdown');
|
|
const labels = r.body.data.categories.map(c => c.key);
|
|
expect(labels).toEqual(['wash', 'refuel', 'charge', 'maintenance', 'insurance']);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/stats/compare', () => {
|
|
let app;
|
|
beforeEach(() => { app = express(); app.use('/api', extraRouter); });
|
|
|
|
it('返本月/上月/同比/环比', async () => {
|
|
const r = await request(app).get('/api/stats/compare');
|
|
expect(r.status).toBe(200);
|
|
expect(r.body.ok).toBe(true);
|
|
expect(r.body.data.by_category).toBeDefined();
|
|
const wash = r.body.data.by_category.wash;
|
|
expect(wash).toHaveProperty('this_month');
|
|
expect(wash).toHaveProperty('last_month');
|
|
expect(wash).toHaveProperty('mom_pct');
|
|
expect(wash).toHaveProperty('this_ytd');
|
|
expect(wash).toHaveProperty('last_ytd');
|
|
expect(wash).toHaveProperty('yoy_pct');
|
|
});
|
|
|
|
it('5 个领域都返了', async () => {
|
|
const r = await request(app).get('/api/stats/compare');
|
|
expect(Object.keys(r.body.data.by_category)).toEqual(
|
|
expect.arrayContaining(['wash', 'refuel', 'charge', 'maintenance', 'insurance'])
|
|
);
|
|
});
|
|
}); |