import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AppRouteTabs } from '@/components/app/AppRouteTabs' function navigate(path: string) { act(() => { window.location.hash = path window.dispatchEvent(new HashChangeEvent('hashchange')) }) } beforeEach(() => { vi.stubGlobal('ResizeObserver', class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} }) window.sessionStorage.clear() window.location.hash = '#/tickets/42' }) afterEach(() => { cleanup() vi.unstubAllGlobals() }) describe('已打开页面标签', () => { it('只显示实际打开过的页面,并允许关闭非当前标签', async () => { const user = userEvent.setup() render() await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(1)) expect(screen.getByRole('tab', { name: '工单详情' })).toHaveAttribute('data-state', 'active') navigate('/knowledge') await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2)) expect(screen.getByRole('tab', { name: '知识库' })).toHaveAttribute('data-state', 'active') await user.click(screen.getByRole('button', { name: '关闭工单详情(/tickets/42)' })) expect(screen.queryByRole('tab', { name: '工单详情' })).not.toBeInTheDocument() expect(window.location.hash).toBe('#/knowledge') }) it('关闭当前标签后切换到相邻页面,并在关闭最后一个标签时回到工作台', async () => { const user = userEvent.setup() render() navigate('/knowledge') await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2)) await user.click(screen.getByRole('button', { name: '关闭知识库(/knowledge)' })) expect(window.location.hash).toBe('#/tickets/42') navigate('/tickets/42') await waitFor(() => expect(screen.getByRole('tab', { name: '工单详情' })).toHaveAttribute('data-state', 'active')) await user.click(screen.getByRole('button', { name: '关闭工单详情(/tickets/42)' })) expect(window.location.hash).toBe('#/') navigate('/') await waitFor(() => expect(screen.getByRole('tab', { name: '工作台' })).toHaveAttribute('data-state', 'active')) }) it('刷新式重新挂载后保留本次会话已打开的标签', async () => { const { unmount } = render() navigate('/knowledge') await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2)) unmount() render() expect(screen.getByRole('tab', { name: '工单详情' })).toBeInTheDocument() expect(screen.getByRole('tab', { name: '知识库' })).toBeInTheDocument() }) it('右键标签可以关闭其左侧和右侧标签', async () => { const user = userEvent.setup() render() navigate('/knowledge') navigate('/solutions') await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(3)) fireEvent.contextMenu(screen.getByRole('tab', { name: '知识库' })) await user.click(await screen.findByRole('menuitem', { name: '关闭左侧标签' })) expect(screen.queryByRole('tab', { name: '工单详情' })).not.toBeInTheDocument() expect(screen.getByRole('tab', { name: '知识库' })).toBeInTheDocument() expect(screen.getByRole('tab', { name: '解决方案库' })).toBeInTheDocument() fireEvent.contextMenu(screen.getByRole('tab', { name: '知识库' })) await user.click(await screen.findByRole('menuitem', { name: '关闭右侧标签' })) expect(screen.queryByRole('tab', { name: '解决方案库' })).not.toBeInTheDocument() expect(screen.getAllByRole('tab')).toHaveLength(1) expect(window.location.hash).toBe('#/knowledge') }) it('批量菜单可以关闭其他标签和关闭全部标签', async () => { const user = userEvent.setup() render() navigate('/knowledge') navigate('/solutions') await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(3)) await user.click(screen.getByRole('button', { name: '查看全部已打开页面,共 3 个' })) await user.click(screen.getByRole('menuitem', { name: '关闭其他标签' })) expect(screen.getAllByRole('tab')).toHaveLength(1) expect(screen.getByRole('tab', { name: '解决方案库' })).toBeInTheDocument() await user.click(screen.getByRole('button', { name: '查看全部已打开页面,共 1 个' })) await user.click(screen.getByRole('menuitem', { name: '关闭全部标签' })) expect(screen.getAllByRole('tab')).toHaveLength(1) expect(screen.getByRole('tab', { name: '工作台' })).toBeInTheDocument() expect(window.location.hash).toBe('#/') }) it('标签溢出时隐藏原生滚动条,并提供左右切换和全部页面菜单', async () => { const user = userEvent.setup() const { container } = render() navigate('/knowledge') await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2)) const scrollArea = container.querySelector('[data-slot="app-route-tabs-scroll"]') as HTMLDivElement const scrollBy = vi.fn() Object.defineProperties(scrollArea, { clientWidth: { configurable: true, value: 320 }, scrollWidth: { configurable: true, value: 720 }, scrollLeft: { configurable: true, value: 0, writable: true }, scrollBy: { configurable: true, value: scrollBy } }) fireEvent.scroll(scrollArea) const nextButton = await screen.findByRole('button', { name: '向右查看已打开页面' }) expect(scrollArea).toHaveClass('scrollbar-hidden', 'scroll-fade-x') expect(screen.getByRole('button', { name: '向左查看已打开页面' })).toBeDisabled() await user.click(nextButton) expect(scrollBy).toHaveBeenCalledWith(expect.objectContaining({ behavior: 'smooth' })) await user.click(screen.getByRole('button', { name: '查看全部已打开页面,共 2 个' })) expect(screen.getByRole('menuitem', { name: /工单详情/ })).toBeInTheDocument() expect(screen.getByRole('menuitem', { name: /知识库.*当前/ })).toHaveAttribute('aria-current', 'page') }) })