368 lines
18 KiB
TypeScript
368 lines
18 KiB
TypeScript
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 }) => <MemoryRouter initialEntries={['/chat']}>{children}</MemoryRouter> });
|
|
}
|
|
|
|
function LocationPath() {
|
|
return <span data-testid="location-path">{useLocation().pathname}</span>;
|
|
}
|
|
|
|
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(<TitleBar integrated />);
|
|
|
|
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(<TitleBar integrated workspaceLayout />);
|
|
|
|
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(<TitleBar />);
|
|
|
|
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(<TitleBar integrated workspaceLayout />);
|
|
|
|
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(<TitleBar integrated workspaceLayout />);
|
|
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<boolean>((resolve) => { resolveQuery = resolve; }));
|
|
let notify: (...args: unknown[]) => void = () => {};
|
|
vi.mocked(window.electron.ipcRenderer.on).mockImplementation((_channel, callback) => {
|
|
notify = callback;
|
|
return vi.fn();
|
|
});
|
|
render(<TitleBar integrated workspaceLayout />);
|
|
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(<TitleBar integrated workspaceLayout />);
|
|
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(<TitleBar integrated overlay />);
|
|
|
|
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(<TitleBar integrated overlay showSidebarControls={false} />);
|
|
|
|
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(<TitleBar integrated overlay showSidebarControls={false} />);
|
|
|
|
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(<TitleBar integrated pageTitle="页面标题" />);
|
|
|
|
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(<TitleBar integrated onSidebarPeekChange={onSidebarPeekChange} />);
|
|
|
|
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(<TitleBar integrated workspaceLayout sidebarPeekOpen={false} />);
|
|
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(<TitleBar integrated workspaceLayout sidebarPeekOpen />);
|
|
|
|
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(<>
|
|
<TitleBar integrated workspaceLayout />
|
|
<CodingConversationHeader conversation={null} snapshot={null} onRename={vi.fn()} onAbort={vi.fn()} onRecover={vi.fn()} />
|
|
<LocationPath />
|
|
</>);
|
|
|
|
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(<>
|
|
<TitleBar integrated workspaceLayout sidebarPeekOpen onSidebarPeekChange={onSidebarPeekChange} />
|
|
<button type="button">Outside navigation</button>
|
|
</>);
|
|
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(<TitleBar integrated sidebarPeekOpen={false} />);
|
|
expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]', 'border-r-0');
|
|
|
|
rerender(<TitleBar integrated sidebarPeekOpen />);
|
|
|
|
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(<TitleBar integrated sidebarPeekOpen onSidebarPeekChange={onSidebarPeekChange} />);
|
|
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(<TitleBar integrated />);
|
|
|
|
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(<TitleBar />);
|
|
|
|
expect(container.firstChild).toBeNull();
|
|
expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument();
|
|
expect(invokeIpcMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|