import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; // Mock the auth store const mockAuthState = { user: null as any, isAuthenticated: false, isLoading: false, quota: null as any, logout: vi.fn(), initialize: vi.fn(), }; vi.mock('../../src/store/auth', () => ({ useAuthStore: (selector: (s: any) => any) => selector(mockAuthState), })); describe('ProtectedRoute', () => { beforeEach(() => { vi.clearAllMocks(); mockAuthState.user = null; mockAuthState.isAuthenticated = false; mockAuthState.isLoading = false; mockAuthState.quota = null; }); it('should show loading when isLoading is true', async () => { mockAuthState.isLoading = true; const { ProtectedRoute } = await import('../../src/components/ProtectedRoute'); render(
Protected Content
); expect(screen.getByText('加载中...')).toBeInTheDocument(); expect(screen.queryByText('Protected Content')).not.toBeInTheDocument(); }); it('should redirect to /login when not authenticated', async () => { mockAuthState.isAuthenticated = false; mockAuthState.isLoading = false; const { ProtectedRoute } = await import('../../src/components/ProtectedRoute'); const { container } = render(
Protected Content
); expect(screen.queryByText('Protected Content')).not.toBeInTheDocument(); }); it('should render children when authenticated', async () => { mockAuthState.isAuthenticated = true; mockAuthState.isLoading = false; mockAuthState.user = { id: 1, username: 'test', email: 'e', is_staff: false }; const { ProtectedRoute } = await import('../../src/components/ProtectedRoute'); render(
Protected Content
); expect(screen.getByText('Protected Content')).toBeInTheDocument(); }); it('should redirect non-admin from admin routes', async () => { mockAuthState.isAuthenticated = true; mockAuthState.isLoading = false; mockAuthState.user = { id: 1, username: 'test', email: 'e', is_staff: false }; const { ProtectedRoute } = await import('../../src/components/ProtectedRoute'); render(
Admin Content
); expect(screen.queryByText('Admin Content')).not.toBeInTheDocument(); }); it('should render admin content for staff users', async () => { mockAuthState.isAuthenticated = true; mockAuthState.isLoading = false; mockAuthState.user = { id: 1, username: 'admin', email: 'e', is_staff: true }; const { ProtectedRoute } = await import('../../src/components/ProtectedRoute'); render(
Admin Content
); expect(screen.getByText('Admin Content')).toBeInTheDocument(); }); }); describe('UserInfoBar', () => { beforeEach(() => { vi.clearAllMocks(); mockAuthState.user = null; mockAuthState.isAuthenticated = false; mockAuthState.isLoading = false; mockAuthState.quota = null; mockAuthState.logout = vi.fn(); }); it('should render nothing when user is null', async () => { mockAuthState.user = null; const { UserInfoBar } = await import('../../src/components/UserInfoBar'); const { container } = render( ); expect(container.children.length).toBe(0); }); it('should display username and avatar', async () => { mockAuthState.user = { id: 1, username: 'johndoe', email: 'j@t.com', is_staff: false }; const { UserInfoBar } = await import('../../src/components/UserInfoBar'); render( ); expect(screen.getByText('johndoe')).toBeInTheDocument(); expect(screen.getByText('J')).toBeInTheDocument(); // avatar letter }); it('should display quota information in seconds (Phase 3)', async () => { mockAuthState.user = { id: 1, username: 'test', email: 'e', is_staff: false }; mockAuthState.quota = { daily_seconds_limit: 600, daily_seconds_used: 100, monthly_seconds_limit: 6000, monthly_seconds_used: 1000 }; const { UserInfoBar } = await import('../../src/components/UserInfoBar'); render( ); // Phase 3: quota display shows seconds format: "剩余: Xs/Xs(日)" expect(screen.getByText(/剩余/)).toBeInTheDocument(); expect(screen.getByText(/500s\/600s/)).toBeInTheDocument(); }); it('should show admin button for staff users', async () => { mockAuthState.user = { id: 1, username: 'admin', email: 'e', is_staff: true }; const { UserInfoBar } = await import('../../src/components/UserInfoBar'); render( ); expect(screen.getByText('管理后台')).toBeInTheDocument(); }); it('should NOT show admin button for non-staff users', async () => { mockAuthState.user = { id: 1, username: 'regular', email: 'e', is_staff: false }; const { UserInfoBar } = await import('../../src/components/UserInfoBar'); render( ); expect(screen.queryByText('管理后台')).not.toBeInTheDocument(); }); it('should show logout button', async () => { mockAuthState.user = { id: 1, username: 'test', email: 'e', is_staff: false }; const { UserInfoBar } = await import('../../src/components/UserInfoBar'); render( ); expect(screen.getByText('退出')).toBeInTheDocument(); }); }); describe('Phase 2 Type Definitions', () => { it('should have User interface with required fields', async () => { const types = await import('../../src/types/index'); // Type verification through instantiation const user: typeof types extends { User: infer U } ? never : any = { id: 1, username: 'test', email: 'test@test.com', is_staff: false, }; expect(user).toBeDefined(); }); it('should have Quota interface with seconds-based fields (Phase 3)', async () => { const quota = { daily_seconds_limit: 600, daily_seconds_used: 100, monthly_seconds_limit: 6000, monthly_seconds_used: 1000, }; expect(quota.daily_seconds_limit).toBe(600); expect(quota.monthly_seconds_limit).toBe(6000); }); it('should have AuthTokens interface', async () => { const tokens = { access: 'test', refresh: 'test' }; expect(tokens.access).toBeDefined(); expect(tokens.refresh).toBeDefined(); }); it('should have AdminStats interface with seconds fields (Phase 3)', async () => { const stats = { total_users: 100, new_users_today: 5, seconds_consumed_today: 4560, seconds_consumed_this_month: 89010, today_change_percent: -5.0, month_change_percent: 8.0, daily_trend: [{ date: '2026-03-01', seconds: 1200 }], top_users: [{ user_id: 1, username: 'alice', seconds_consumed: 2340 }], }; expect(stats.total_users).toBe(100); expect(stats.daily_trend).toHaveLength(1); expect(stats.daily_trend[0].seconds).toBe(1200); expect(stats.top_users[0].seconds_consumed).toBe(2340); }); it('should have AdminUser interface with seconds-based quota (Phase 3)', async () => { const user = { id: 1, username: 'test', email: 'test@test.com', is_active: true, date_joined: '2026-03-01', daily_seconds_limit: 600, monthly_seconds_limit: 6000, seconds_today: 120, seconds_this_month: 3500, }; expect(user.daily_seconds_limit).toBe(600); expect(user.monthly_seconds_limit).toBe(6000); }); it('should have ProfileOverview interface (Phase 3)', async () => { const overview = { daily_seconds_limit: 600, daily_seconds_used: 120, monthly_seconds_limit: 6000, monthly_seconds_used: 3500, total_seconds_used: 15000, daily_trend: [{ date: '2026-03-01', seconds: 45 }], }; expect(overview.daily_seconds_limit).toBe(600); expect(overview.daily_trend).toHaveLength(1); }); it('should have SystemSettings interface (Phase 3)', async () => { const settings = { default_daily_seconds_limit: 600, default_monthly_seconds_limit: 6000, announcement: 'Test announcement', announcement_enabled: true, }; expect(settings.default_daily_seconds_limit).toBe(600); expect(settings.announcement_enabled).toBe(true); }); it('UploadedFile type should not include audio', async () => { // Verify audio type was removed from UploadedFile (fix from previous sessions) const { readFileSync } = await import('fs'); const typesSrc = readFileSync( '/Users/maidong/Desktop/zyc/研究openclaw/视频生成平台/jimeng-clone/src/types/index.ts', 'utf-8' ); // UploadedFile.type should only be 'image' | 'video' const uploadedFileMatch = typesSrc.match(/interface UploadedFile[\s\S]*?type:\s*(.+?);/); expect(uploadedFileMatch).toBeTruthy(); expect(uploadedFileMatch![1]).not.toContain('audio'); }); });