AppLayout.topbar.test.tsx 4.86 KB
// REQ-USR-004: 顶栏结构 + 当前用户 + 退出登录(BR3/BR9)
import { describe, it, expect, vi } from 'vitest';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useNavigate, useLocation, Routes, Route } from 'react-router-dom';
import { App as AntdApp } from 'antd';
import { renderShell } from './renderShell';
import TopBar from '../../src/layouts/AppLayout/TopBar';
import { useAppDispatch } from '../../src/store/hooks';
import { clearCredentials } from '../../src/store/slices/authSlice';
import { LOGOUT_SUCCESS_TEXT } from '../../src/layouts/AppLayout/shellMessages';
import { HOME_TAB, BIZ_TABS, type TabItem } from '../../src/layouts/AppLayout/useTabStack';
import type { AuthUser } from '../../src/api/types';

const ADMIN: AuthUser = { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' };

// 顶栏测试宿主:以真实 onLogout(dispatch + message + navigate)驱动,模拟 AppLayout 提供的回调
function TopBarHost({
  user,
  tabs,
  activeKey = 'home',
  navOverlayOpen = false,
  onToggleNav = vi.fn(),
  onSelectTab = vi.fn(),
  onCloseTab = vi.fn(),
  onLogoHome = vi.fn(),
}: {
  user: AuthUser | null;
  tabs: TabItem[];
  activeKey?: string;
  navOverlayOpen?: boolean;
  onToggleNav?: () => void;
  onSelectTab?: (k: string) => void;
  onCloseTab?: (k: string) => void;
  onLogoHome?: () => void;
}) {
  const dispatch = useAppDispatch();
  const navigate = useNavigate();
  const { message } = AntdApp.useApp();
  const handleLogout = () => {
    dispatch(clearCredentials());
    message.success(LOGOUT_SUCCESS_TEXT);
    navigate('/login', { replace: true });
  };
  return (
    <TopBar
      user={user}
      tabs={tabs}
      activeKey={activeKey}
      navOverlayOpen={navOverlayOpen}
      onToggleNav={onToggleNav}
      onSelectTab={onSelectTab}
      onCloseTab={onCloseTab}
      onLogout={handleLogout}
      onLogoHome={onLogoHome}
    />
  );
}

function LoginProbe() {
  const loc = useLocation();
  return <div data-testid="login-probe">{loc.pathname}</div>;
}

describe('TopBar', () => {
  it('renders brand logo / 全部导航 button / 主页 tab', () => {
    renderShell(<TopBarHost user={ADMIN} tabs={[HOME_TAB]} />, {
      preloadedAuth: { token: 't', user: ADMIN },
    });
    expect(screen.getByRole('button', { name: '全部导航' })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '品牌Logo 回到主页' })).toBeInTheDocument();
    const homeTab = screen.getByTestId('tab-home');
    expect(homeTab).toHaveTextContent('主页');
    // 主页 tab 无关闭按钮
    expect(within(homeTab).queryByText('✕')).not.toBeInTheDocument();
  });

  it('renders current user as sUserName(sUserType)', () => {
    renderShell(<TopBarHost user={ADMIN} tabs={[HOME_TAB]} />, {
      preloadedAuth: { token: 't', user: ADMIN },
    });
    expect(screen.getByText('朱子纯(超级管理员)')).toBeInTheDocument();
  });

  it('user fallback when user is null', () => {
    renderShell(<TopBarHost user={null} tabs={[HOME_TAB]} />, {
      preloadedAuth: { token: 't', user: null },
    });
    expect(screen.getByText('未登录用户')).toBeInTheDocument();
  });

  it('logout menu dispatches clearCredentials, shows success, navigates /login', async () => {
    localStorage.setItem('xly_erp_token', 't');
    const { getState } = renderShell(
      <Routes>
        <Route path="/" element={<TopBarHost user={ADMIN} tabs={[HOME_TAB]} />} />
        <Route path="/login" element={<LoginProbe />} />
      </Routes>,
      { initialEntries: ['/'], preloadedAuth: { token: 't', user: ADMIN } },
    );
    // 展开当前用户下拉
    await userEvent.click(screen.getByText('朱子纯(超级管理员)'));
    const logout = await screen.findByText('退出登录');
    await userEvent.click(logout);
    expect(getState().auth.token).toBeNull();
    expect(localStorage.getItem('xly_erp_token')).toBeNull();
    expect(await screen.findByText(LOGOUT_SUCCESS_TEXT)).toBeInTheDocument();
    expect(screen.getByTestId('login-probe').textContent).toBe('/login');
  });

  it('nav toggle button highlights when navOverlayOpen', () => {
    renderShell(<TopBarHost user={ADMIN} tabs={[HOME_TAB]} navOverlayOpen />, {
      preloadedAuth: { token: 't', user: ADMIN },
    });
    const navBtn = screen.getByRole('button', { name: '全部导航' });
    expect(navBtn.getAttribute('aria-pressed')).toBe('true');
  });

  it('clicking business tab close calls onCloseTab', async () => {
    const onCloseTab = vi.fn();
    renderShell(
      <TopBarHost
        user={ADMIN}
        tabs={[HOME_TAB, BIZ_TABS.userlist]}
        activeKey="userlist"
        onCloseTab={onCloseTab}
      />,
      { preloadedAuth: { token: 't', user: ADMIN } },
    );
    await userEvent.click(screen.getByTestId('tab-close-userlist'));
    expect(onCloseTab).toHaveBeenCalledWith('userlist');
  });
});