diff --git a/frontend/src/pages/usr/UserDetail/PermissionGroupList.tsx b/frontend/src/pages/usr/UserDetail/PermissionGroupList.tsx new file mode 100644 index 0000000..3413a9c --- /dev/null +++ b/frontend/src/pages/usr/UserDetail/PermissionGroupList.tsx @@ -0,0 +1,57 @@ +// REQ-USR-001 / REQ-USR-002: 权限分类勾选列表(表头全选 indeterminate + 逐项 Checkbox,BR10/BR11/D3) +import { Checkbox } from 'antd'; +import type { PermissionItem } from '../../../api/types'; +import { PERM_LIST_HEADER } from './constants'; +import styles from './UserDetail.module.css'; + +export interface PermissionGroupListProps { + permissions: PermissionItem[]; + checkedIds: number[]; + onToggle(id: number, checked: boolean): void; + onToggleAll(checked: boolean): void; + loading?: boolean; +} + +export default function PermissionGroupList({ + permissions, + checkedIds, + onToggle, + onToggleAll, +}: PermissionGroupListProps) { + const total = permissions.length; + const checkedCount = permissions.filter((p) => checkedIds.includes(p.id)).length; + const allChecked = total > 0 && checkedCount === total; + const indeterminate = checkedCount > 0 && checkedCount < total; + + return ( +
+
+ onToggleAll(e.target.checked)} + data-testid="perm-check-all" + /> + {PERM_LIST_HEADER} + +
+ + {total === 0 ? ( +
+ ) : ( + permissions.map((p) => ( + + )) + )} +
+ ); +} diff --git a/frontend/tests/unit/PermissionGroupList.test.tsx b/frontend/tests/unit/PermissionGroupList.test.tsx new file mode 100644 index 0000000..e7922df --- /dev/null +++ b/frontend/tests/unit/PermissionGroupList.test.tsx @@ -0,0 +1,102 @@ +// REQ-USR-001 / REQ-USR-002: PermissionGroupList 权限分类勾选列表单测(渲染/勾选集合/全选 indeterminate/回勾,BR10/BR11/D3) +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderShell } from './renderShell'; +import PermissionGroupList from '../../src/pages/usr/UserDetail/PermissionGroupList'; +import type { PermissionItem } from '../../src/api/types'; + +const PERMS: PermissionItem[] = [ + { id: 1, name: '默认显示', category: '基础' }, + { id: 2, name: '高级查看', category: '基础' }, + { id: 3, name: '导出', category: '报表' }, +]; + +function setup(over: { + permissions?: PermissionItem[]; + checkedIds?: number[]; +} = {}) { + const onToggle = vi.fn(); + const onToggleAll = vi.fn(); + renderShell( + , + { preloadedAuth: { token: 't', user: { id: 1, sUserName: 'a', sUserType: 'x', sLanguage: '中文' } } }, + ); + return { onToggle, onToggleAll }; +} + +function rowCheckbox(id: number): HTMLInputElement { + return screen.getByTestId('perm-check-' + id) as HTMLInputElement; +} + +function allCheckbox(): HTMLInputElement { + return screen.getByTestId('perm-check-all') as HTMLInputElement; +} + +describe('PermissionGroupList', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders header 权限分类 and one row per permission', () => { + setup(); + expect(screen.getByText('权限分类')).toBeInTheDocument(); + expect(screen.getByText('默认显示')).toBeInTheDocument(); + expect(screen.getByText('高级查看')).toBeInTheDocument(); + expect(screen.getByText('导出')).toBeInTheDocument(); + }); + + it('checked rows reflect checkedIds', () => { + setup({ checkedIds: [1] }); + expect(rowCheckbox(1).checked).toBe(true); + expect(rowCheckbox(2).checked).toBe(false); + expect(rowCheckbox(3).checked).toBe(false); + }); + + it('toggling a row calls onToggle(id, checked)', async () => { + const user = userEvent.setup(); + const { onToggle } = setup({ checkedIds: [1] }); + await user.click(rowCheckbox(2)); + expect(onToggle).toHaveBeenCalledWith(2, true); + await user.click(rowCheckbox(1)); + expect(onToggle).toHaveBeenCalledWith(1, false); + }); + + it('header select-all checked when all selected; indeterminate when partial', () => { + const { } = setup({ checkedIds: [1, 2, 3] }); + expect(allCheckbox().checked).toBe(true); + }); + + it('header indeterminate when partial; unchecked when none', () => { + setup({ checkedIds: [1] }); + const all = allCheckbox(); + expect(all.checked).toBe(false); + // AntD 半选用 aria-checked='mixed' 表达于 wrapper;input 仍未 checked + const wrapper = all.closest('.ant-checkbox'); + expect(wrapper?.classList.contains('ant-checkbox-indeterminate')).toBe(true); + }); + + it('header toggle calls onToggleAll', async () => { + const user = userEvent.setup(); + const { onToggleAll } = setup({ checkedIds: [] }); + await user.click(allCheckbox()); + expect(onToggleAll).toHaveBeenCalledWith(true); + }); + + it('header toggle off when all selected', async () => { + const user = userEvent.setup(); + const { onToggleAll } = setup({ checkedIds: [1, 2, 3] }); + await user.click(allCheckbox()); + expect(onToggleAll).toHaveBeenCalledWith(false); + }); + + it('empty permissions renders empty list (no rows)', () => { + setup({ permissions: [] }); + expect(screen.queryByTestId('perm-check-1')).toBeNull(); + }); +});