import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import Roles from '@/pages/Roles' // ── Mock data ────────────────────────────────────────────────── const mockRoles = [ { id: 'role-admin', name: 'admin', description: '管理员', permissions: ['admin:full'], account_count: 2, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }, { id: 'role-user', name: 'user', description: '普通用户', permissions: ['model:read', 'relay:use', 'config:read', 'prompt:read'], account_count: 15, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }, ] const mockTemplates = [ { id: 'tpl-read-only', name: '只读模板', description: '仅查看权限', permissions: ['model:read', 'config:read'], created_at: '2026-02-01T00:00:00Z', updated_at: '2026-02-01T00:00:00Z', }, { id: 'tpl-operator', name: '操作员模板', description: '操作权限', permissions: ['model:read', 'relay:use', 'config:read', 'prompt:read', 'hand:use'], created_at: '2026-02-15T00:00:00Z', updated_at: '2026-02-15T00:00:00Z', }, ] const server = setupServer() beforeEach(() => { server.listen({ onUnhandledRequest: 'bypass' }) }) afterEach(() => { server.close() }) function renderWithProviders(ui: React.ReactElement) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) return render( {ui} , ) } function setupRolesHandlers(overrides: Record = {}) { server.use( http.get('*/api/v1/roles', () => { return HttpResponse.json(overrides.roles ?? mockRoles) }), http.get('*/api/v1/permission-templates', () => { return HttpResponse.json(overrides.templates ?? mockTemplates) }), ) } describe('Roles', () => { it('renders page title', () => { setupRolesHandlers() renderWithProviders() expect(screen.getByText('角色与权限')).toBeInTheDocument() }) it('displays tabs', () => { setupRolesHandlers() renderWithProviders() // Tabs use label spans with icons expect(screen.getByText('角色')).toBeInTheDocument() expect(screen.getByText('权限模板')).toBeInTheDocument() }) it('displays roles in default tab', async () => { setupRolesHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('admin')).toBeInTheDocument() }) expect(screen.getByText('user')).toBeInTheDocument() expect(screen.getByText('管理员')).toBeInTheDocument() expect(screen.getByText('普通用户')).toBeInTheDocument() }) it('displays permissions count tags', async () => { setupRolesHandlers() renderWithProviders() await waitFor(() => { // "1 项" for admin (1 permission), "4 项" for user (4 permissions) expect(screen.getByText('1 项')).toBeInTheDocument() expect(screen.getByText('4 项')).toBeInTheDocument() }) }) it('displays account count column', async () => { setupRolesHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('admin')).toBeInTheDocument() }) // account_count: admin=2, user=15 expect(screen.getByText('2')).toBeInTheDocument() expect(screen.getByText('15')).toBeInTheDocument() }) it('has 新建角色 button', async () => { setupRolesHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('新建角色')).toBeInTheDocument() }) }) it('renders 操作 column for role rows', async () => { setupRolesHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('admin')).toBeInTheDocument() }) // 操作 column header should exist expect(screen.getByText('操作')).toBeInTheDocument() }) it('shows empty roles state', async () => { setupRolesHandlers({ roles: [], templates: mockTemplates }) renderWithProviders() await waitFor(() => { const empties = screen.getAllByText('暂无数据') expect(empties.length).toBeGreaterThanOrEqual(1) }) }) it('switches to templates tab and displays templates', async () => { setupRolesHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('admin')).toBeInTheDocument() }) // Click 权限模板 tab screen.getByText('权限模板').click() await waitFor(() => { expect(screen.getByText('只读模板')).toBeInTheDocument() }) expect(screen.getByText('操作员模板')).toBeInTheDocument() }) it('displays template permission counts', async () => { setupRolesHandlers() renderWithProviders() screen.getByText('权限模板').click() await waitFor(() => { // read-only: 2 permissions, operator: 5 permissions expect(screen.getByText('2 项')).toBeInTheDocument() }) }) it('shows empty templates state', async () => { setupRolesHandlers({ roles: mockRoles, templates: [] }) renderWithProviders() await waitFor(() => { expect(screen.getByText('admin')).toBeInTheDocument() }) screen.getByText('权限模板').click() await waitFor(() => { const empties = screen.getAllByText('暂无数据') expect(empties.length).toBeGreaterThanOrEqual(1) }) }) it('has 新建模板 button in templates tab', async () => { setupRolesHandlers() renderWithProviders() screen.getByText('权限模板').click() await waitFor(() => { expect(screen.getByText('新建模板')).toBeInTheDocument() }) }) it('shows empty on roles API failure', async () => { server.use( http.get('*/api/v1/roles', () => { return HttpResponse.json( { error: 'internal_error', message: '数据库错误' }, { status: 500 }, ) }), http.get('*/api/v1/permission-templates', () => { return HttpResponse.json(mockTemplates) }), ) renderWithProviders() await waitFor(() => { // ProTable shows 暂无数据 when data fetch fails const empties = screen.getAllByText('暂无数据') expect(empties.length).toBeGreaterThanOrEqual(1) }) }) })