Commit 87c3d1f00733ca8d9cb1e44ad88d78d61a021bb5

Authored by zichun
1 parent 981d8e4a

feat(usr): 用户单据权限分类勾选列表 PermissionGroupList REQ-USR-001 REQ-USR-002

frontend/src/pages/usr/UserDetail/PermissionGroupList.tsx 0 → 100644
  1 +// REQ-USR-001 / REQ-USR-002: 权限分类勾选列表(表头全选 indeterminate + 逐项 Checkbox,BR10/BR11/D3)
  2 +import { Checkbox } from 'antd';
  3 +import type { PermissionItem } from '../../../api/types';
  4 +import { PERM_LIST_HEADER } from './constants';
  5 +import styles from './UserDetail.module.css';
  6 +
  7 +export interface PermissionGroupListProps {
  8 + permissions: PermissionItem[];
  9 + checkedIds: number[];
  10 + onToggle(id: number, checked: boolean): void;
  11 + onToggleAll(checked: boolean): void;
  12 + loading?: boolean;
  13 +}
  14 +
  15 +export default function PermissionGroupList({
  16 + permissions,
  17 + checkedIds,
  18 + onToggle,
  19 + onToggleAll,
  20 +}: PermissionGroupListProps) {
  21 + const total = permissions.length;
  22 + const checkedCount = permissions.filter((p) => checkedIds.includes(p.id)).length;
  23 + const allChecked = total > 0 && checkedCount === total;
  24 + const indeterminate = checkedCount > 0 && checkedCount < total;
  25 +
  26 + return (
  27 + <div className={styles.permList} data-testid="perm-list">
  28 + <div className={styles.permHead}>
  29 + <Checkbox
  30 + checked={allChecked}
  31 + indeterminate={indeterminate}
  32 + onChange={(e) => onToggleAll(e.target.checked)}
  33 + data-testid="perm-check-all"
  34 + />
  35 + <span>{PERM_LIST_HEADER}</span>
  36 + <span className={styles.permHeadSort} aria-hidden="true">
  37 + ⇅
  38 + </span>
  39 + </div>
  40 +
  41 + {total === 0 ? (
  42 + <div className={styles.permEmpty} data-testid="perm-empty" />
  43 + ) : (
  44 + permissions.map((p) => (
  45 + <label key={p.id} className={styles.permRow}>
  46 + <Checkbox
  47 + checked={checkedIds.includes(p.id)}
  48 + onChange={(e) => onToggle(p.id, e.target.checked)}
  49 + data-testid={'perm-check-' + p.id}
  50 + />
  51 + <span>{p.name}</span>
  52 + </label>
  53 + ))
  54 + )}
  55 + </div>
  56 + );
  57 +}
frontend/tests/unit/PermissionGroupList.test.tsx 0 → 100644
  1 +// REQ-USR-001 / REQ-USR-002: PermissionGroupList 权限分类勾选列表单测(渲染/勾选集合/全选 indeterminate/回勾,BR10/BR11/D3)
  2 +import { describe, it, expect, vi, beforeEach } from 'vitest';
  3 +import { screen } from '@testing-library/react';
  4 +import userEvent from '@testing-library/user-event';
  5 +import { renderShell } from './renderShell';
  6 +import PermissionGroupList from '../../src/pages/usr/UserDetail/PermissionGroupList';
  7 +import type { PermissionItem } from '../../src/api/types';
  8 +
  9 +const PERMS: PermissionItem[] = [
  10 + { id: 1, name: '默认显示', category: '基础' },
  11 + { id: 2, name: '高级查看', category: '基础' },
  12 + { id: 3, name: '导出', category: '报表' },
  13 +];
  14 +
  15 +function setup(over: {
  16 + permissions?: PermissionItem[];
  17 + checkedIds?: number[];
  18 +} = {}) {
  19 + const onToggle = vi.fn();
  20 + const onToggleAll = vi.fn();
  21 + renderShell(
  22 + <PermissionGroupList
  23 + permissions={over.permissions ?? PERMS}
  24 + checkedIds={over.checkedIds ?? []}
  25 + onToggle={onToggle}
  26 + onToggleAll={onToggleAll}
  27 + />,
  28 + { preloadedAuth: { token: 't', user: { id: 1, sUserName: 'a', sUserType: 'x', sLanguage: '中文' } } },
  29 + );
  30 + return { onToggle, onToggleAll };
  31 +}
  32 +
  33 +function rowCheckbox(id: number): HTMLInputElement {
  34 + return screen.getByTestId('perm-check-' + id) as HTMLInputElement;
  35 +}
  36 +
  37 +function allCheckbox(): HTMLInputElement {
  38 + return screen.getByTestId('perm-check-all') as HTMLInputElement;
  39 +}
  40 +
  41 +describe('PermissionGroupList', () => {
  42 + beforeEach(() => {
  43 + vi.clearAllMocks();
  44 + });
  45 +
  46 + it('renders header 权限分类 and one row per permission', () => {
  47 + setup();
  48 + expect(screen.getByText('权限分类')).toBeInTheDocument();
  49 + expect(screen.getByText('默认显示')).toBeInTheDocument();
  50 + expect(screen.getByText('高级查看')).toBeInTheDocument();
  51 + expect(screen.getByText('导出')).toBeInTheDocument();
  52 + });
  53 +
  54 + it('checked rows reflect checkedIds', () => {
  55 + setup({ checkedIds: [1] });
  56 + expect(rowCheckbox(1).checked).toBe(true);
  57 + expect(rowCheckbox(2).checked).toBe(false);
  58 + expect(rowCheckbox(3).checked).toBe(false);
  59 + });
  60 +
  61 + it('toggling a row calls onToggle(id, checked)', async () => {
  62 + const user = userEvent.setup();
  63 + const { onToggle } = setup({ checkedIds: [1] });
  64 + await user.click(rowCheckbox(2));
  65 + expect(onToggle).toHaveBeenCalledWith(2, true);
  66 + await user.click(rowCheckbox(1));
  67 + expect(onToggle).toHaveBeenCalledWith(1, false);
  68 + });
  69 +
  70 + it('header select-all checked when all selected; indeterminate when partial', () => {
  71 + const { } = setup({ checkedIds: [1, 2, 3] });
  72 + expect(allCheckbox().checked).toBe(true);
  73 + });
  74 +
  75 + it('header indeterminate when partial; unchecked when none', () => {
  76 + setup({ checkedIds: [1] });
  77 + const all = allCheckbox();
  78 + expect(all.checked).toBe(false);
  79 + // AntD 半选用 aria-checked='mixed' 表达于 wrapper;input 仍未 checked
  80 + const wrapper = all.closest('.ant-checkbox');
  81 + expect(wrapper?.classList.contains('ant-checkbox-indeterminate')).toBe(true);
  82 + });
  83 +
  84 + it('header toggle calls onToggleAll', async () => {
  85 + const user = userEvent.setup();
  86 + const { onToggleAll } = setup({ checkedIds: [] });
  87 + await user.click(allCheckbox());
  88 + expect(onToggleAll).toHaveBeenCalledWith(true);
  89 + });
  90 +
  91 + it('header toggle off when all selected', async () => {
  92 + const user = userEvent.setup();
  93 + const { onToggleAll } = setup({ checkedIds: [1, 2, 3] });
  94 + await user.click(allCheckbox());
  95 + expect(onToggleAll).toHaveBeenCalledWith(false);
  96 + });
  97 +
  98 + it('empty permissions renders empty list (no rows)', () => {
  99 + setup({ permissions: [] });
  100 + expect(screen.queryByTestId('perm-check-1')).toBeNull();
  101 + });
  102 +});