video-shuoshan/web/test/unit/phase2Components.test.tsx
zyc ffe92f7b15 Initial commit: 即梦视频生成平台
- web/: React + Vite + TypeScript 前端
- backend/: Django + DRF + SimpleJWT 后端
- prototype/: HTML 设计原型
- docs/: PRD 和设计评审文档
- test: 单元测试 + E2E 极限测试
2026-03-13 09:59:33 +08:00

292 lines
9.5 KiB
TypeScript

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(
<MemoryRouter>
<ProtectedRoute><div>Protected Content</div></ProtectedRoute>
</MemoryRouter>
);
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(
<MemoryRouter initialEntries={['/']}>
<ProtectedRoute><div>Protected Content</div></ProtectedRoute>
</MemoryRouter>
);
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(
<MemoryRouter>
<ProtectedRoute><div>Protected Content</div></ProtectedRoute>
</MemoryRouter>
);
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(
<MemoryRouter initialEntries={['/admin/dashboard']}>
<ProtectedRoute requireAdmin><div>Admin Content</div></ProtectedRoute>
</MemoryRouter>
);
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(
<MemoryRouter>
<ProtectedRoute requireAdmin><div>Admin Content</div></ProtectedRoute>
</MemoryRouter>
);
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(
<MemoryRouter>
<UserInfoBar />
</MemoryRouter>
);
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(
<MemoryRouter>
<UserInfoBar />
</MemoryRouter>
);
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(
<MemoryRouter>
<UserInfoBar />
</MemoryRouter>
);
// 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(
<MemoryRouter>
<UserInfoBar />
</MemoryRouter>
);
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(
<MemoryRouter>
<UserInfoBar />
</MemoryRouter>
);
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(
<MemoryRouter>
<UserInfoBar />
</MemoryRouter>
);
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');
});
});