Merge branch 'codex/20260929-code-back-entry-33d87a81-code-back-entry' into codex/20260930-code-nav-merge-2f867bc4-code-nav-merge

This commit is contained in:
鲨鱼辣椒
2026-09-30 01:02:20 +08:00
16 changed files with 595 additions and 35 deletions

View File

@@ -267,7 +267,7 @@ describe('PI-130 feature-complete Coding UI', () => {
);
expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveClass('fixed', 'top-0', 'h-10');
expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveStyle({ left: '132px' });
expect(screen.getByTestId('coding-conversation-header-titlebar')).toHaveStyle({ left: '168px' });
expect(screen.queryByTestId('coding-conversation-sidebar')).not.toBeInTheDocument();
window.electron.platform = originalPlatform;

View File

@@ -9,7 +9,7 @@ function createWindow(id: number) {
isDestroyed: vi.fn(() => false),
send: vi.fn(),
});
return {
return Object.assign(new EventEmitter(), {
id,
webContents: contents,
isDestroyed: vi.fn(() => false),
@@ -17,8 +17,9 @@ function createWindow(id: number) {
maximize: vi.fn(),
unmaximize: vi.fn(),
isMaximized: vi.fn(() => false),
isFullScreen: vi.fn(() => false),
close: vi.fn(),
};
});
}
async function setup() {
@@ -104,6 +105,74 @@ describe('IPC after closing and reopening the main window', () => {
expect(ctx.second.webContents.listenerCount('destroyed')).toBe(1);
});
it('queries fullscreen for the requesting window before and after reopening', async () => {
const ctx = await setup();
ctx.first.isFullScreen.mockReturnValue(true);
expect(ctx.invoke('window:isFullScreen', ctx.first)).toBe(true);
expect(ctx.invoke('window:isFullScreen', ctx.second)).toBe(false);
ctx.reopen();
expect(ctx.invoke('window:isFullScreen', ctx.second)).toBe(false);
ctx.second.isFullScreen.mockReturnValue(true);
expect(ctx.invoke('window:isFullScreen', ctx.second)).toBe(true);
expect(() => ctx.invoke('window:isFullScreen', ctx.first)).toThrow('Main window is unavailable');
});
it('notifies each bound renderer once when its window enters or leaves fullscreen', async () => {
const ctx = await setup();
ctx.bindings.bindWindow(ctx.first as unknown as BrowserWindow);
expect(ctx.first.listenerCount('enter-full-screen')).toBe(1);
expect(ctx.first.listenerCount('leave-full-screen')).toBe(1);
ctx.first.emit('enter-full-screen');
ctx.first.emit('leave-full-screen');
expect(ctx.first.webContents.send.mock.calls).toEqual([
['window:fullscreen-changed', true],
['window:fullscreen-changed', false],
]);
expect(ctx.second.webContents.send).not.toHaveBeenCalled();
const registeredCount = ctx.handle.mock.calls.length;
ctx.reopen();
ctx.bindings.bindWindow(ctx.second as unknown as BrowserWindow);
expect(ctx.second.listenerCount('enter-full-screen')).toBe(1);
expect(ctx.second.listenerCount('leave-full-screen')).toBe(1);
ctx.second.emit('enter-full-screen');
ctx.second.emit('leave-full-screen');
expect(ctx.second.webContents.send.mock.calls).toEqual([
['window:fullscreen-changed', true],
['window:fullscreen-changed', false],
]);
expect(ctx.first.webContents.send).toHaveBeenCalledTimes(2);
expect(ctx.handle).toHaveBeenCalledTimes(registeredCount);
});
it('does not send fullscreen events after a window or renderer is destroyed', async () => {
const ctx = await setup();
ctx.reopen();
ctx.first.emit('enter-full-screen');
ctx.first.emit('leave-full-screen');
expect(ctx.first.webContents.send).not.toHaveBeenCalled();
ctx.second.webContents.isDestroyed.mockReturnValue(true);
ctx.second.emit('enter-full-screen');
ctx.second.emit('leave-full-screen');
expect(ctx.second.webContents.send).not.toHaveBeenCalled();
});
it('does not bind fullscreen events to an already destroyed window or renderer', async () => {
const ctx = await setup();
const closed = createWindow(3);
closed.isDestroyed.mockReturnValue(true);
ctx.bindings.bindWindow(closed as unknown as BrowserWindow);
expect(closed.listenerCount('enter-full-screen')).toBe(0);
expect(closed.listenerCount('leave-full-screen')).toBe(0);
ctx.second.webContents.isDestroyed.mockReturnValue(true);
ctx.bindings.bindWindow(ctx.second as unknown as BrowserWindow);
expect(ctx.second.listenerCount('enter-full-screen')).toBe(0);
expect(ctx.second.listenerCount('leave-full-screen')).toBe(0);
});
it('parents dialogs and link menus to the reopened renderer', async () => {
const ctx = await setup();
ctx.reopen();

View File

@@ -1,8 +1,19 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
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', () => ({
@@ -13,6 +24,7 @@ describe('TitleBar platform behavior', () => {
beforeEach(() => {
invokeIpcMock.mockReset();
invokeIpcMock.mockResolvedValue(false);
vi.mocked(window.electron.ipcRenderer.on).mockReset();
useSettingsStore.setState({ sidebarCollapsed: false, classroomSidebarWidth: 171 });
});
@@ -73,7 +85,7 @@ describe('TitleBar platform behavior', () => {
expect(container.querySelector('.drag-region')).toBeInTheDocument();
expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument();
expect(invokeIpcMock).not.toHaveBeenCalled();
expect(invokeIpcMock).toHaveBeenCalledWith('window:isFullScreen');
});
it('aligns the classroom title bar with one project sidebar and no duplicate branding', () => {
@@ -92,6 +104,53 @@ describe('TitleBar platform behavior', () => {
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', () => {
@@ -179,14 +238,14 @@ describe('TitleBar platform behavior', () => {
useSettingsStore.setState({ sidebarCollapsed: true });
const { rerender } = render(<TitleBar integrated workspaceLayout sidebarPeekOpen={false} />);
expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]');
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')).toHaveClass('w-[132px]');
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: '固定侧栏' });
@@ -198,6 +257,66 @@ describe('TitleBar platform behavior', () => {
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 });