- web/: React + Vite + TypeScript 前端 - backend/: Django + DRF + SimpleJWT 后端 - prototype/: HTML 设计原型 - docs/: PRD 和设计评审文档 - test: 单元测试 + E2E 极限测试
550 lines
24 KiB
TypeScript
550 lines
24 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
|
|
const ROOT = resolve(__dirname, '../..');
|
|
|
|
/**
|
|
* Phase 3 Feature Tests
|
|
* Tests for: Seconds-based quota, Admin multi-page layout, Profile page, ECharts integration
|
|
*/
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 1. Seconds-based Quota System Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Seconds-Based Quota System', () => {
|
|
it('User model should have daily_seconds_limit and monthly_seconds_limit fields', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/accounts/models.py'), 'utf-8');
|
|
expect(src).toContain('daily_seconds_limit');
|
|
expect(src).toContain('monthly_seconds_limit');
|
|
expect(src).not.toContain('daily_limit = models.'); // old field should be gone
|
|
expect(src).not.toContain('monthly_limit = models.'); // old field should be gone
|
|
});
|
|
|
|
it('User model default quota should be 600s/day and 6000s/month', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/accounts/models.py'), 'utf-8');
|
|
expect(src).toMatch(/daily_seconds_limit.*default=600/);
|
|
expect(src).toMatch(/monthly_seconds_limit.*default=6000/);
|
|
});
|
|
|
|
it('GenerationRecord should have seconds_consumed field', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/models.py'), 'utf-8');
|
|
expect(src).toContain('seconds_consumed');
|
|
expect(src).toMatch(/seconds_consumed\s*=\s*models\.FloatField/);
|
|
});
|
|
|
|
it('Video generate view should use seconds-based quota check', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/views.py'), 'utf-8');
|
|
expect(src).toContain('daily_seconds_limit');
|
|
expect(src).toContain('monthly_seconds_limit');
|
|
expect(src).toContain("seconds_consumed");
|
|
// Check that quota enforcement uses Sum of seconds_consumed
|
|
expect(src).toContain("Sum('seconds_consumed')");
|
|
});
|
|
|
|
it('Frontend Quota type should use seconds fields', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/types/index.ts'), 'utf-8');
|
|
expect(src).toContain('daily_seconds_limit: number');
|
|
expect(src).toContain('daily_seconds_used: number');
|
|
expect(src).toContain('monthly_seconds_limit: number');
|
|
expect(src).toContain('monthly_seconds_used: number');
|
|
});
|
|
|
|
it('UserInfoBar should display quota in seconds format', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/components/UserInfoBar.tsx'), 'utf-8');
|
|
// Should show seconds remaining, not calls remaining
|
|
expect(src).toContain('daily_seconds_limit');
|
|
expect(src).toContain('daily_seconds_used');
|
|
expect(src).toMatch(/s\/.*s\(日\)/); // Format: Xs/Xs(日)
|
|
});
|
|
|
|
it('Auth store should have quota state with seconds fields', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/store/auth.ts'), 'utf-8');
|
|
expect(src).toContain('quota: Quota | null');
|
|
expect(src).toContain('fetchUserInfo');
|
|
// After login, should fetch quota info
|
|
expect(src).toContain('get().fetchUserInfo()');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 2. Admin Multi-Page Layout Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Admin Multi-Page Layout', () => {
|
|
it('App.tsx should have nested admin routes under /admin', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/App.tsx'), 'utf-8');
|
|
expect(src).toContain('path="/admin"');
|
|
expect(src).toContain('path="dashboard"');
|
|
expect(src).toContain('path="users"');
|
|
expect(src).toContain('path="records"');
|
|
expect(src).toContain('path="settings"');
|
|
// Should redirect /admin to /admin/dashboard
|
|
expect(src).toContain('Navigate to="/admin/dashboard"');
|
|
});
|
|
|
|
it('Admin routes should require admin access (requireAdmin)', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/App.tsx'), 'utf-8');
|
|
expect(src).toContain('requireAdmin');
|
|
});
|
|
|
|
it('AdminLayout should have sidebar with 4 navigation items', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/AdminLayout.tsx'), 'utf-8');
|
|
expect(src).toContain('/admin/dashboard');
|
|
expect(src).toContain('/admin/users');
|
|
expect(src).toContain('/admin/records');
|
|
expect(src).toContain('/admin/settings');
|
|
expect(src).toContain('仪表盘');
|
|
expect(src).toContain('用户管理');
|
|
expect(src).toContain('消费记录');
|
|
expect(src).toContain('系统设置');
|
|
});
|
|
|
|
it('AdminLayout should support sidebar collapse', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/AdminLayout.tsx'), 'utf-8');
|
|
expect(src).toContain('collapsed');
|
|
expect(src).toContain('setCollapsed');
|
|
});
|
|
|
|
it('AdminLayout should use NavLink for active state highlighting', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/AdminLayout.tsx'), 'utf-8');
|
|
expect(src).toContain('NavLink');
|
|
expect(src).toContain('isActive');
|
|
expect(src).toContain('navItemActive');
|
|
});
|
|
|
|
it('AdminLayout should have back-to-home button', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/AdminLayout.tsx'), 'utf-8');
|
|
expect(src).toContain('返回首页');
|
|
expect(src).toContain("navigate('/')");
|
|
});
|
|
|
|
it('AdminLayout should render child routes via Outlet', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/AdminLayout.tsx'), 'utf-8');
|
|
expect(src).toContain('Outlet');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 3. Dashboard Page Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Dashboard Page', () => {
|
|
it('should use ECharts for charts', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('echarts');
|
|
expect(src).toContain('ReactEChartsCore');
|
|
expect(src).toContain('LineChart');
|
|
expect(src).toContain('BarChart');
|
|
});
|
|
|
|
it('should display 4 stat cards (total users, new users, today seconds, month seconds)', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('总用户数');
|
|
expect(src).toContain('今日新增用户');
|
|
expect(src).toContain('今日消费秒数');
|
|
expect(src).toContain('本月消费秒数');
|
|
});
|
|
|
|
it('should show trend arrows with color coding', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
// Positive = green (↑), negative = red (↓)
|
|
expect(src).toContain('positive');
|
|
expect(src).toContain('negative');
|
|
expect(src).toMatch(/card\.change\s*>=\s*0/);
|
|
});
|
|
|
|
it('should render consumption trend line chart (30 days)', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('消费趋势');
|
|
expect(src).toContain("type: 'line'");
|
|
expect(src).toContain('daily_trend');
|
|
});
|
|
|
|
it('should render top users bar chart (horizontal)', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('用户消费排行');
|
|
expect(src).toContain("type: 'bar'");
|
|
expect(src).toContain('top_users');
|
|
});
|
|
|
|
it('should have mock data fallback for development', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('generateMockTrend');
|
|
expect(src).toContain('generateMockTopUsers');
|
|
});
|
|
|
|
it('should show skeleton loading state', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('skeleton');
|
|
expect(src).toContain('skeletonCard');
|
|
expect(src).toContain('skeletonChart');
|
|
});
|
|
|
|
it('should call adminApi.getStats for data', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('adminApi.getStats');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 4. Users Management Page Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Users Management Page', () => {
|
|
it('should render user table with correct columns', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/UsersPage.tsx'), 'utf-8');
|
|
expect(src).toContain('用户名');
|
|
expect(src).toContain('邮箱');
|
|
expect(src).toContain('注册时间');
|
|
expect(src).toContain('状态');
|
|
expect(src).toContain('日限额(秒)');
|
|
expect(src).toContain('月限额(秒)');
|
|
expect(src).toContain('今日消费(秒)');
|
|
expect(src).toContain('本月消费(秒)');
|
|
});
|
|
|
|
it('should support search and status filter', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/UsersPage.tsx'), 'utf-8');
|
|
expect(src).toContain('搜索用户名/邮箱');
|
|
expect(src).toContain('全部状态');
|
|
expect(src).toContain('statusFilter');
|
|
});
|
|
|
|
it('should have quota edit modal', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/UsersPage.tsx'), 'utf-8');
|
|
expect(src).toContain('编辑配额');
|
|
expect(src).toContain('每日秒数限额');
|
|
expect(src).toContain('每月秒数限额');
|
|
expect(src).toContain('handleSaveQuota');
|
|
});
|
|
|
|
it('should have user detail drawer', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/UsersPage.tsx'), 'utf-8');
|
|
expect(src).toContain('用户详情');
|
|
expect(src).toContain('drawerOpen');
|
|
expect(src).toContain('detailUser');
|
|
expect(src).toContain('近期消费记录');
|
|
});
|
|
|
|
it('should have enable/disable user toggle', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/UsersPage.tsx'), 'utf-8');
|
|
expect(src).toContain('handleToggleStatus');
|
|
expect(src).toContain('updateUserStatus');
|
|
});
|
|
|
|
it('should have pagination', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/UsersPage.tsx'), 'utf-8');
|
|
expect(src).toContain('totalPages');
|
|
expect(src).toContain('pagination');
|
|
expect(src).toContain('setPage');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 5. Records Page Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Records Page', () => {
|
|
it('should render records table with correct columns', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/RecordsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('时间');
|
|
expect(src).toContain('用户名');
|
|
expect(src).toContain('消费秒数');
|
|
expect(src).toContain('视频描述');
|
|
expect(src).toContain('模式');
|
|
expect(src).toContain('状态');
|
|
});
|
|
|
|
it('should support date range filtering', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/RecordsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('startDate');
|
|
expect(src).toContain('endDate');
|
|
expect(src).toContain('type="date"');
|
|
});
|
|
|
|
it('should support CSV export', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/RecordsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('导出 CSV');
|
|
expect(src).toContain('handleExportCSV');
|
|
expect(src).toContain('text/csv');
|
|
expect(src).toContain('Blob');
|
|
});
|
|
|
|
it('CSV export should escape special characters to prevent injection', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/RecordsPage.tsx'), 'utf-8');
|
|
// Should escape double quotes and formula injection characters
|
|
expect(src).toContain('replace(/"/g');
|
|
expect(src).toMatch(/replace\(.*\[=\+\\\-@\]/);
|
|
});
|
|
|
|
it('should support username search', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/RecordsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('按用户名搜索');
|
|
expect(src).toContain('search');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 6. Settings Page Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Settings Page', () => {
|
|
it('should have global quota settings form', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/SettingsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('全局默认配额');
|
|
expect(src).toContain('默认每日限额 (秒)');
|
|
expect(src).toContain('默认每月限额 (秒)');
|
|
expect(src).toContain('保存配额设置');
|
|
});
|
|
|
|
it('should have announcement management', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/SettingsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('系统公告');
|
|
expect(src).toContain('announcement_enabled');
|
|
expect(src).toContain('announcement');
|
|
expect(src).toContain('保存公告');
|
|
});
|
|
|
|
it('should have announcement toggle switch', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/SettingsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('type="checkbox"');
|
|
expect(src).toContain('announcement_enabled');
|
|
});
|
|
|
|
it('should call adminApi.getSettings and adminApi.updateSettings', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/SettingsPage.tsx'), 'utf-8');
|
|
expect(src).toContain('adminApi.getSettings');
|
|
expect(src).toContain('adminApi.updateSettings');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 7. Profile Page Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Profile Page', () => {
|
|
it('should have consumption overview with gauge chart', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('GaugeChart');
|
|
expect(src).toContain("type: 'gauge'");
|
|
expect(src).toContain('消费概览');
|
|
});
|
|
|
|
it('should show daily and monthly quota progress bars', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('今日额度');
|
|
expect(src).toContain('本月额度');
|
|
expect(src).toContain('progressBar');
|
|
expect(src).toContain('progressFill');
|
|
});
|
|
|
|
it('should have consumption trend sparkline with 7d/30d toggle', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('消费趋势');
|
|
expect(src).toContain('近7天');
|
|
expect(src).toContain('近30天');
|
|
expect(src).toContain('trendPeriod');
|
|
});
|
|
|
|
it('should show warning banner when daily quota > 80%', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('warningBanner');
|
|
expect(src).toContain('dangerBanner');
|
|
expect(src).toMatch(/dailyPercent\s*>=\s*80/);
|
|
expect(src).toMatch(/dailyPercent\s*>=\s*100/);
|
|
});
|
|
|
|
it('should have consumption records list with load more', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('消费记录');
|
|
expect(src).toContain('加载更多');
|
|
expect(src).toContain('recordsPage');
|
|
});
|
|
|
|
it('should call profileApi for data', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('profileApi.getOverview');
|
|
expect(src).toContain('profileApi.getRecords');
|
|
});
|
|
|
|
it('should have back-to-home button', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('返回首页');
|
|
expect(src).toContain("navigate('/')");
|
|
});
|
|
|
|
it('should have logout functionality', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('退出');
|
|
expect(src).toContain('handleLogout');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 8. Backend API Routes Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Backend API Routes', () => {
|
|
it('should have all Phase 3 URL patterns', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/urls.py'), 'utf-8');
|
|
const requiredPaths = [
|
|
'video/generate',
|
|
'admin/stats',
|
|
'admin/users',
|
|
'admin/records',
|
|
'admin/settings',
|
|
'profile/overview',
|
|
'profile/records',
|
|
];
|
|
for (const path of requiredPaths) {
|
|
expect(src).toContain(path);
|
|
}
|
|
});
|
|
|
|
it('should have user quota and status management endpoints', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/urls.py'), 'utf-8');
|
|
expect(src).toContain('quota');
|
|
expect(src).toContain('status');
|
|
});
|
|
|
|
it('admin views should require IsAdminUser permission', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/views.py'), 'utf-8');
|
|
// All admin views should use IsAdminUser
|
|
const adminViews = ['admin_stats_view', 'admin_users_list_view', 'admin_user_detail_view',
|
|
'admin_user_quota_view', 'admin_user_status_view', 'admin_records_view', 'admin_settings_view'];
|
|
for (const view of adminViews) {
|
|
// Find the view function and check it has IsAdminUser
|
|
const viewRegex = new RegExp(`@permission_classes\\(\\[IsAdminUser\\]\\)\\s*\\ndef\\s+${view}`);
|
|
expect(src).toMatch(viewRegex);
|
|
}
|
|
});
|
|
|
|
it('profile views should require IsAuthenticated permission', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/views.py'), 'utf-8');
|
|
const profileViews = ['profile_overview_view', 'profile_records_view'];
|
|
for (const view of profileViews) {
|
|
const viewRegex = new RegExp(`@permission_classes\\(\\[IsAuthenticated\\]\\)\\s*\\ndef\\s+${view}`);
|
|
expect(src).toMatch(viewRegex);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 9. QuotaConfig Model Verification
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: QuotaConfig Model (System Settings)', () => {
|
|
it('should have default quota fields in seconds', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/models.py'), 'utf-8');
|
|
expect(src).toContain('default_daily_seconds_limit');
|
|
expect(src).toContain('default_monthly_seconds_limit');
|
|
});
|
|
|
|
it('should have announcement fields', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/models.py'), 'utf-8');
|
|
expect(src).toContain('announcement');
|
|
expect(src).toContain('announcement_enabled');
|
|
});
|
|
|
|
it('should enforce singleton pattern (pk=1)', () => {
|
|
const src = readFileSync(resolve(ROOT, 'backend/apps/generation/models.py'), 'utf-8');
|
|
expect(src).toContain('self.pk = 1');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 10. ProtectedRoute Admin Guard
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: ProtectedRoute Admin Guard', () => {
|
|
it('should redirect non-admin users away from admin routes', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/components/ProtectedRoute.tsx'), 'utf-8');
|
|
expect(src).toContain('requireAdmin');
|
|
expect(src).toContain('is_staff');
|
|
// Non-admin should be redirected to /
|
|
expect(src).toContain('Navigate to="/"');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 11. UserInfoBar Phase 3 Features
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: UserInfoBar Navigation', () => {
|
|
it('should have profile link', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/components/UserInfoBar.tsx'), 'utf-8');
|
|
expect(src).toContain('个人中心');
|
|
expect(src).toContain("'/profile'");
|
|
});
|
|
|
|
it('should show admin link only for staff users', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/components/UserInfoBar.tsx'), 'utf-8');
|
|
expect(src).toContain('is_staff');
|
|
expect(src).toContain('管理后台');
|
|
expect(src).toContain("'/admin/dashboard'");
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 12. ECharts Integration
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: ECharts Integration', () => {
|
|
it('package.json should include echarts dependencies', () => {
|
|
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'));
|
|
expect(pkg.dependencies.echarts).toBeDefined();
|
|
expect(pkg.dependencies['echarts-for-react']).toBeDefined();
|
|
});
|
|
|
|
it('DashboardPage should use ECharts components', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/DashboardPage.tsx'), 'utf-8');
|
|
expect(src).toContain('echarts/core');
|
|
expect(src).toContain('echarts/charts');
|
|
expect(src).toContain('CanvasRenderer');
|
|
expect(src).toContain('TooltipComponent');
|
|
expect(src).toContain('DataZoomComponent');
|
|
});
|
|
|
|
it('ProfilePage should use ECharts for gauge and sparkline', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/pages/ProfilePage.tsx'), 'utf-8');
|
|
expect(src).toContain('GaugeChart');
|
|
expect(src).toContain('LineChart');
|
|
expect(src).toContain('CanvasRenderer');
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 13. Frontend Route Configuration
|
|
// ──────────────────────────────────────────────
|
|
|
|
describe('Phase 3: Route Configuration', () => {
|
|
it('should have /profile route', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/App.tsx'), 'utf-8');
|
|
expect(src).toContain('path="/profile"');
|
|
expect(src).toContain('ProfilePage');
|
|
});
|
|
|
|
it('/profile should be protected', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/App.tsx'), 'utf-8');
|
|
// ProfilePage should be wrapped in ProtectedRoute
|
|
const profileSection = src.slice(
|
|
src.indexOf('path="/profile"'),
|
|
src.indexOf('path="/admin"')
|
|
);
|
|
expect(profileSection).toContain('ProtectedRoute');
|
|
});
|
|
|
|
it('/admin should redirect to /admin/dashboard', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/App.tsx'), 'utf-8');
|
|
expect(src).toContain('Navigate to="/admin/dashboard" replace');
|
|
});
|
|
|
|
it('unknown routes should redirect to /', () => {
|
|
const src = readFileSync(resolve(ROOT, 'src/App.tsx'), 'utf-8');
|
|
expect(src).toContain('path="*"');
|
|
expect(src).toContain('Navigate to="/"');
|
|
});
|
|
});
|