import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ReactElement } from 'react'; import { act, fireEvent, render as renderComponent, screen, waitFor } from '@testing-library/react'; import { MemoryRouter, useLocation } from 'react-router-dom'; import { TitleBar } from '@/components/layout/TitleBar'; import { CodingConversationHeader } from '@/pages/Chat/CodingConversationHeader'; import { useSettingsStore } from '@/stores/settings'; function render(ui: ReactElement) { return renderComponent(ui, { wrapper: ({ children }) => {children} }); } function LocationPath() { return {useLocation().pathname}; } const invokeIpcMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args), })); describe('TitleBar platform behavior', () => { beforeEach(() => { invokeIpcMock.mockReset(); invokeIpcMock.mockResolvedValue(false); vi.mocked(window.electron.ipcRenderer.on).mockReset(); useSettingsStore.setState({ sidebarCollapsed: false, classroomSidebarWidth: 171 }); }); it('extends the product surfaces through the macOS title bar', () => { window.electron.platform = 'darwin'; render(); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('border-b'); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('drag-region'); expect(screen.getByTestId('product-titlebar')).toHaveClass('glass-surface', 'bg-background/70'); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-64', 'sidebar-glass-surface', 'border-r'); expect(screen.getByTestId('titlebar-main-surface')).toHaveClass('border-b', 'bg-background', 'drag-region'); expect(screen.getByTestId('titlebar-project-context')).not.toHaveClass('border-l'); expect(screen.getByTestId('titlebar-project-context')).not.toHaveClass('translate-y-[3px]'); expect(screen.getByTestId('titlebar-logo')).toHaveClass('absolute', 'top-1/2', 'bg-background'); expect(screen.getByTestId('titlebar-logo')).toHaveStyle({ right: '0px' }); expect(screen.getByTestId('titlebar-logo').querySelector('img')).toHaveAttribute('data-logo-source', 'original-wordmark'); expect(screen.getByTestId('titlebar-sidebar-toggle-layer')).toHaveClass('no-drag', 'pointer-events-auto', 'fixed', 'left-0', 'top-0', 'z-[400]', 'h-10', 'w-[132px]'); expect(screen.getByTestId('titlebar-sidebar-toggle')).toHaveClass('no-drag', 'pointer-events-auto', 'absolute', 'top-1', 'z-[401]', 'left-[88px]'); expect(screen.getByTestId('titlebar-project-context')).toHaveTextContent('Makelore 工作台'); expect(screen.queryByRole('button', { name: '打开项目位置' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: '后退' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: '前进' })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '折叠侧栏' })); expect(useSettingsStore.getState().sidebarCollapsed).toBe(true); act(() => useSettingsStore.setState({ sidebarCollapsed: true })); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]'); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('border-r-0'); expect(screen.getByTestId('product-titlebar')).toHaveClass('bg-background'); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('bg-background'); expect(screen.getByTestId('product-titlebar')).toHaveClass('border-b'); expect(screen.getByTestId('titlebar-main-surface')).toHaveClass('border-b-0'); expect(screen.getByTestId('titlebar-sidebar-toggle')).toBeVisible(); }); it('keeps the collapsed sidebar expand control above the titlebar drag surfaces', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true }); render(); const toggle = screen.getByRole('button', { name: '展开侧栏' }); expect(screen.getByTestId('titlebar-sidebar-toggle-layer')).toContainElement(toggle); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('border-b'); expect(toggle).toHaveClass('pointer-events-auto', 'z-[401]'); fireEvent.click(toggle); expect(useSettingsStore.getState().sidebarCollapsed).toBe(false); }); it('renders macOS drag region', () => { window.electron.platform = 'darwin'; const { container } = render(); expect(container.querySelector('.drag-region')).toBeInTheDocument(); expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument(); expect(invokeIpcMock).toHaveBeenCalledWith('window:isFullScreen'); }); it('aligns the classroom title bar with one project sidebar and no duplicate branding', () => { window.electron.platform = 'darwin'; render(); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('glass-surface', 'border-b'); expect(screen.getByTestId('product-titlebar')).toHaveClass('bg-transparent'); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '171px', minWidth: '171px', flexBasis: '171px' }); act(() => useSettingsStore.getState().setClassroomSidebarWidth(256)); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '256px' }); expect(screen.getByTestId('titlebar-main-surface')).toHaveClass('border-b-0'); expect(screen.queryByTestId('titlebar-conversation-surface')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); expect(screen.queryByRole('img', { name: 'Makelore logo' })).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-project-context')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '折叠侧栏' })).toBeEnabled(); expect(screen.getByRole('button', { name: '返回首页' })).toBeEnabled(); expect(screen.getByTestId('classroom-titlebar-navigation')).toHaveClass('pl-[88px]'); expect(screen.getByTestId('titlebar-sidebar-toggle')).toHaveClass('right-2'); }); it('aligns fullscreen return with project buttons and restores native window-control space on exit', async () => { window.electron.platform = 'darwin'; invokeIpcMock.mockResolvedValue(true); const unsubscribe = vi.fn(); let notify: (...args: unknown[]) => void = () => {}; vi.mocked(window.electron.ipcRenderer.on).mockImplementation((channel, callback) => { expect(channel).toBe('window:fullscreen-changed'); notify = callback; return unsubscribe; }); const { unmount } = render(); const navigation = screen.getByTestId('classroom-titlebar-navigation'); await waitFor(() => expect(navigation).toHaveClass('pl-3')); act(() => notify(false)); expect(navigation).toHaveClass('pl-[88px]'); act(() => notify(true)); expect(navigation).toHaveClass('pl-3'); unmount(); expect(unsubscribe).toHaveBeenCalledOnce(); }); it('keeps a native fullscreen transition when an older initial query resolves later', async () => { window.electron.platform = 'darwin'; let resolveQuery: (value: boolean) => void = () => {}; invokeIpcMock.mockReturnValue(new Promise((resolve) => { resolveQuery = resolve; })); let notify: (...args: unknown[]) => void = () => {}; vi.mocked(window.electron.ipcRenderer.on).mockImplementation((_channel, callback) => { notify = callback; return vi.fn(); }); render(); act(() => notify(true)); await act(async () => resolveQuery(false)); expect(screen.getByTestId('classroom-titlebar-navigation')).toHaveClass('pl-3'); }); it('keeps the native safe inset when an older preload rejects fullscreen events', () => { window.electron.platform = 'darwin'; vi.mocked(window.electron.ipcRenderer.on).mockImplementation(() => { throw new Error('Invalid IPC channel'); }); render(); expect(screen.getByTestId('classroom-titlebar-navigation')).toHaveClass('pl-[88px]'); expect(invokeIpcMock).not.toHaveBeenCalled(); }); it('renders the painting title bar as a transparent overlay with the logo at the window edge', () => { window.electron.platform = 'darwin'; render(); expect(screen.getByTestId('product-titlebar')).toHaveClass( 'absolute', 'inset-x-0', 'top-0', 'pointer-events-none', 'bg-transparent', ); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('relative'); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('border-b', 'glass-surface'); expect(screen.getByTestId('titlebar-main-surface')).toHaveClass( 'pointer-events-none', 'border-b-0', 'bg-transparent', ); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('bg-transparent', 'border-r-0'); expect(screen.getByTestId('titlebar-sidebar-surface')).not.toHaveClass('sidebar-glass-surface'); expect(screen.getByTestId('titlebar-logo')).toHaveClass('bg-transparent'); expect(screen.getByTestId('titlebar-logo')).toHaveStyle({ right: '0px' }); expect(screen.getByTestId('titlebar-logo').querySelector('img')).toHaveClass('mix-blend-multiply'); expect(screen.queryByTestId('titlebar-project-context')).not.toBeInTheDocument(); }); it('keeps only the product chrome when a workspace owns its navigation', () => { window.electron.platform = 'darwin'; render(); expect(screen.queryByTestId('titlebar-sidebar-surface')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-sidebar-toggle')).not.toBeInTheDocument(); expect(screen.getByTestId('titlebar-main-surface')).toBeInTheDocument(); expect(screen.getByTestId('titlebar-logo')).toBeInTheDocument(); }); it('leaves the narrow Windows Canvas rail to the native window controls', () => { window.electron.platform = 'win32'; render(); expect(screen.queryByTestId('titlebar-sidebar-toggle')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); expect(screen.getByTitle('Minimize')).toBeInTheDocument(); expect(screen.getByTitle('Maximize')).toBeInTheDocument(); expect(screen.getByTitle('Close')).toBeInTheDocument(); }); it('keeps an optional page title after the macOS traffic-light safe area', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true }); render(); expect(screen.getByTestId('titlebar-page-title')).toHaveTextContent('页面标题'); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]'); expect(screen.queryByTestId('titlebar-project-context')).not.toBeInTheDocument(); expect(screen.getByTestId('titlebar-logo').querySelectorAll('img')).toHaveLength(1); }); it('previews the collapsed sidebar on hover and pins it on click', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true }); const onSidebarPeekChange = vi.fn(); render(); const toggle = screen.getByTestId('titlebar-sidebar-toggle'); fireEvent.pointerEnter(toggle); expect(onSidebarPeekChange).toHaveBeenCalledWith(true, 'toggle'); fireEvent.pointerLeave(toggle); expect(onSidebarPeekChange).toHaveBeenCalledWith(false, 'toggle'); fireEvent.click(toggle); expect(useSettingsStore.getState().sidebarCollapsed).toBe(false); }); it('keeps the classroom sidebar safe area fixed during preview and lets the student pin it', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true }); const { rerender } = render(); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '168px' }); expect(screen.queryByTestId('titlebar-conversation-surface')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '展开侧栏' })).toHaveAttribute('aria-expanded', 'false'); rerender(); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '168px' }); expect(screen.queryByTestId('titlebar-conversation-surface')).not.toBeInTheDocument(); expect(screen.queryByTestId('titlebar-logo')).not.toBeInTheDocument(); const pinButton = screen.getByRole('button', { name: '固定侧栏' }); expect(pinButton).toBeEnabled(); expect(pinButton).toHaveAttribute('aria-expanded', 'true'); fireEvent.click(pinButton); expect(useSettingsStore.getState().sidebarCollapsed).toBe(false); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '171px', minWidth: '171px', flexBasis: '171px' }); expect(screen.getByRole('button', { name: '折叠侧栏' })).toBeEnabled(); }); it.each([ ['darwin', 168, 168, 'pl-[88px]'], ['win32', 128, 88, 'pl-3'], ] as const)('keeps both %s navigation controls outside the conversation header at narrow and collapsed widths', (platform, narrowWidth, collapsedWidth, insetClass) => { window.electron.platform = platform; useSettingsStore.setState({ classroomSidebarWidth: 128 }); render(<> ); const assertWidth = (width: number) => { expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: `${width}px` }); expect(screen.getByTestId('titlebar-sidebar-toggle-layer')).toHaveStyle({ width: `${width}px` }); expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveStyle({ left: `${width}px` }); }; assertWidth(narrowWidth); const rail = screen.getByTestId('classroom-titlebar-navigation'); expect(rail).toHaveClass(insetClass); expect(rail).toContainElement(screen.getByRole('button', { name: '返回首页' })); expect(rail).toContainElement(screen.getByRole('button', { name: '折叠侧栏' })); expect(screen.getByRole('button', { name: '返回首页' })).toHaveTextContent(''); expect(screen.getAllByTestId('titlebar-sidebar-toggle')).toHaveLength(1); act(() => useSettingsStore.setState({ classroomSidebarWidth: 256 })); assertWidth(256); fireEvent.click(screen.getByRole('button', { name: '折叠侧栏' })); assertWidth(collapsedWidth); fireEvent.click(screen.getByRole('button', { name: '返回首页' })); expect(screen.getByTestId('location-path')).toHaveTextContent('/module-select'); }); it('preserves keyboard preview when focus moves between the classroom controls', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true }); const onSidebarPeekChange = vi.fn(); render(<> ); const toggle = screen.getByRole('button', { name: '固定侧栏' }); const back = screen.getByRole('button', { name: '返回首页' }); act(() => toggle.focus()); expect(onSidebarPeekChange).toHaveBeenLastCalledWith(true, 'focus'); onSidebarPeekChange.mockClear(); act(() => back.focus()); expect(onSidebarPeekChange).not.toHaveBeenCalledWith(false, 'focus'); expect(onSidebarPeekChange).toHaveBeenLastCalledWith(true, 'focus'); act(() => screen.getByRole('button', { name: 'Outside navigation' }).focus()); expect(onSidebarPeekChange).toHaveBeenLastCalledWith(false, 'focus'); const layer = screen.getByTestId('titlebar-sidebar-toggle-layer'); fireEvent.pointerEnter(layer); fireEvent.pointerLeave(layer); expect(onSidebarPeekChange).toHaveBeenCalledWith(true, 'titlebar'); expect(onSidebarPeekChange).toHaveBeenLastCalledWith(false, 'titlebar'); expect(layer).toHaveStyle({ width: '168px' }); }); it('extends the ordinary titlebar sidebar surface during a hover preview', () => { window.electron.platform = 'darwin'; useSettingsStore.setState({ sidebarCollapsed: true }); const { rerender } = render(); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]', 'border-r-0'); rerender(); expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-64', 'sidebar-peek-surface', 'border-r'); expect(screen.getByTestId('titlebar-sidebar-surface')).not.toHaveClass('sidebar-glass-surface'); expect(screen.getByTestId('titlebar-sidebar-surface')).not.toHaveClass('border-r-0'); expect(screen.getByTestId('product-titlebar')).not.toHaveClass('border-b'); const onSidebarPeekChange = vi.fn(); rerender(); const surface = screen.getByTestId('titlebar-sidebar-surface'); fireEvent.pointerEnter(surface); fireEvent.pointerLeave(surface); expect(onSidebarPeekChange).toHaveBeenNthCalledWith(1, true, 'titlebar'); expect(onSidebarPeekChange).toHaveBeenNthCalledWith(2, false, 'titlebar'); }); it('renders custom controls and reserves their space on Windows', async () => { window.electron.platform = 'win32'; render(); expect(screen.getByTitle('Minimize')).toBeInTheDocument(); expect(screen.getByTitle('Maximize')).toBeInTheDocument(); expect(screen.getByTitle('Close')).toBeInTheDocument(); expect(screen.getByTestId('titlebar-logo')).toHaveStyle({ right: '148px' }); await waitFor(() => { expect(invokeIpcMock).toHaveBeenCalledWith('window:isMaximized'); }); }); it('renders no custom title bar on Linux', () => { window.electron.platform = 'linux'; const { container } = render(); expect(container.firstChild).toBeNull(); expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument(); expect(invokeIpcMock).not.toHaveBeenCalled(); }); });