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:
@@ -50,7 +50,7 @@ async function seedInactiveCodingProjects(resources: { rootDir: string; userData
|
||||
}
|
||||
|
||||
test.describe('Makelore module navigation without setup flow', () => {
|
||||
test('offers a create CTA and horizontal cards for remembered inactive Code projects', async ({
|
||||
test('opens remembered Code projects and keeps the module return entry in chat instead of project settings', async ({
|
||||
electronTestResources,
|
||||
launchElectronApp,
|
||||
}) => {
|
||||
@@ -76,6 +76,125 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
await expect(page.getByTestId('coding-chat-panel')).toBeVisible();
|
||||
await expect(page.getByTestId('coding-existing-projects')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-course-project-project-storybook')).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
const sidebar = page.getByTestId('sidebar');
|
||||
const moduleReturn = page.getByTestId('sidebar-module-switcher-trigger');
|
||||
await expect(moduleReturn).toBeVisible();
|
||||
await expect(moduleReturn).toHaveText('');
|
||||
await expect(moduleReturn).toHaveAttribute('aria-label', '返回首页');
|
||||
await expect(moduleReturn).toHaveAttribute('title', '返回首页');
|
||||
await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible();
|
||||
const brandBox = await sidebar.locator('.classroom-brand').boundingBox();
|
||||
const returnBox = await moduleReturn.boundingBox();
|
||||
const collapseBox = await page.getByRole('button', { name: '折叠侧栏' }).boundingBox();
|
||||
const sidebarBox = await sidebar.boundingBox();
|
||||
expect(brandBox).not.toBeNull();
|
||||
expect(returnBox).not.toBeNull();
|
||||
expect(collapseBox).not.toBeNull();
|
||||
expect(returnBox!.y + returnBox!.height).toBeLessThanOrEqual(brandBox!.y);
|
||||
const headerBox = await page.getByTestId('coding-conversation-header').boundingBox();
|
||||
const tabsBox = await page.getByRole('tablist', { name: '当前工作' }).boundingBox();
|
||||
expect(Math.abs(returnBox!.y + returnBox!.height / 2 - headerBox!.y - headerBox!.height / 2)).toBeLessThan(1);
|
||||
expect(Math.abs(brandBox!.y + brandBox!.height / 2 - tabsBox!.y - tabsBox!.height / 2)).toBeLessThan(1);
|
||||
expect(brandBox!.y - sidebarBox!.y).toBe(0);
|
||||
expect(brandBox!.height).toBe(tabsBox!.height);
|
||||
expect(returnBox!.width).toBe(32);
|
||||
expect(Math.abs(returnBox!.y + returnBox!.height / 2 - collapseBox!.y - collapseBox!.height / 2)).toBeLessThan(1);
|
||||
expect(returnBox!.x + returnBox!.width).toBeLessThanOrEqual(collapseBox!.x);
|
||||
expect(sidebarBox!.x + sidebarBox!.width - collapseBox!.x - collapseBox!.width).toBeLessThanOrEqual(14);
|
||||
await expect(page.getByTestId('titlebar-sidebar-toggle')).toHaveCount(1);
|
||||
const community = sidebar.getByRole('button', { name: '作品广场', exact: true });
|
||||
await expect(community).toHaveCount(1);
|
||||
const communityBox = await community.boundingBox();
|
||||
const createBox = await sidebar.getByTestId('sidebar-create-project').boundingBox();
|
||||
const headingBox = await sidebar.locator('.classroom-projects-heading').boundingBox();
|
||||
expect(communityBox!.y).toBeGreaterThanOrEqual(brandBox!.y + brandBox!.height);
|
||||
expect(communityBox!.y - brandBox!.y - brandBox!.height).toBe(10);
|
||||
expect(createBox!.y - communityBox!.y - communityBox!.height).toBe(10);
|
||||
expect(createBox!.y - brandBox!.y - brandBox!.height).toBe(61);
|
||||
expect(communityBox!.height).toBe(createBox!.height);
|
||||
await expect(community).toHaveCSS('background-color', 'rgb(248, 250, 245)');
|
||||
const buttonStyles = await page.evaluate(() => {
|
||||
const styles = (selector: string) => {
|
||||
const s = getComputedStyle(document.querySelector(selector)!);
|
||||
return [s.borderRadius, s.borderWidth, s.borderColor, s.boxShadow, s.padding, s.fontSize, s.fontWeight];
|
||||
};
|
||||
return { community: styles('[data-testid=sidebar] .community-link'), create: styles('[data-testid=sidebar-create-project]') };
|
||||
});
|
||||
expect(buttonStyles.community).toEqual(buttonStyles.create);
|
||||
expect(headingBox!.y).toBeGreaterThanOrEqual(createBox!.y + createBox!.height);
|
||||
expect(headingBox!.y - createBox!.y - createBox!.height).toBeLessThanOrEqual(14);
|
||||
await page.screenshot({ path: test.info().outputPath('chat-code-return-entry.png') });
|
||||
|
||||
// Exercise native state events without switching the test app into a macOS Space.
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].emit('enter-full-screen'));
|
||||
await expect.poll(async () => (await moduleReturn.boundingBox())!.x).toBe(createBox!.x);
|
||||
const fullscreenReturn = await moduleReturn.boundingBox();
|
||||
expect(fullscreenReturn!.y).toBe(returnBox!.y);
|
||||
expect(await page.getByRole('button', { name: '折叠侧栏' }).boundingBox()).toEqual(collapseBox);
|
||||
expect(await sidebar.locator('.classroom-brand').boundingBox()).toEqual(brandBox);
|
||||
await page.screenshot({ path: test.info().outputPath('chat-fullscreen-return-aligned.png') });
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].emit('leave-full-screen'));
|
||||
await expect.poll(async () => (await moduleReturn.boundingBox())!.x).toBe(returnBox!.x);
|
||||
|
||||
const sidebarResizer = sidebar.getByRole('separator', { name: '调整左侧功能栏宽度' });
|
||||
await sidebarResizer.press('Home');
|
||||
await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '128');
|
||||
const narrowReturn = await moduleReturn.boundingBox();
|
||||
const narrowToggle = await page.getByRole('button', { name: '折叠侧栏' }).boundingBox();
|
||||
expect(narrowReturn!.x + narrowReturn!.width).toBeLessThanOrEqual(narrowToggle!.x);
|
||||
await expect(moduleReturn).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '折叠侧栏' })).toBeVisible();
|
||||
const narrowHeader = await page.getByTestId('coding-conversation-header').boundingBox();
|
||||
expect(narrowToggle!.x + narrowToggle!.width).toBeLessThanOrEqual(narrowHeader!.x);
|
||||
expect(await moduleReturn.evaluate(el => {
|
||||
const b = el.getBoundingClientRect();
|
||||
return el.contains(document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2));
|
||||
})).toBe(true);
|
||||
await expect(community).toBeVisible();
|
||||
expect(await community.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||
await page.screenshot({ path: test.info().outputPath('chat-narrow-navigation-row.png') });
|
||||
await sidebarResizer.dblclick();
|
||||
await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '171');
|
||||
|
||||
await sidebar.getByRole('button', { name: '项目设置', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/project-config$/);
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await expect(sidebar.getByTestId('sidebar-module-switcher')).toHaveCount(0);
|
||||
const projectBack = page.getByTestId('project-configuration-back-button');
|
||||
await expect(projectBack).toBeVisible();
|
||||
await page.screenshot({ path: test.info().outputPath('project-config-without-module-return.png') });
|
||||
await projectBack.click();
|
||||
await expect(page).toHaveURL(/\/chat$/);
|
||||
await expect(moduleReturn).toBeVisible();
|
||||
|
||||
await sidebar.getByRole('button', { name: '项目设置', exact: true }).click();
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
await expect(page).toHaveURL(/\/project-config\/plugins\?/);
|
||||
const plugins = page.getByTestId('project-plugins-sheet');
|
||||
await expect(plugins).toBeVisible();
|
||||
await expect(sidebar.getByTestId('sidebar-module-switcher')).toHaveCount(0);
|
||||
await page.screenshot({ path: test.info().outputPath('project-config-plugins-without-module-return.png') });
|
||||
await plugins.getByRole('button', { name: '关闭', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/project-config$/);
|
||||
await expect(projectBack).toBeVisible();
|
||||
await sidebar.getByTestId('sidebar-course-project-project-storybook').click();
|
||||
await expect(page).toHaveURL(/\/chat$/);
|
||||
|
||||
const sidebarToggle = page.getByTestId('titlebar-sidebar-toggle');
|
||||
await sidebarToggle.click();
|
||||
await page.getByTestId('coding-chat-panel').click({ position: { x: 500, y: 200 } });
|
||||
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
|
||||
const collapsedToggleBox = await sidebarToggle.boundingBox();
|
||||
await expect(moduleReturn).toBeVisible();
|
||||
await sidebarToggle.hover();
|
||||
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
|
||||
expect(await sidebarToggle.boundingBox()).toEqual(collapsedToggleBox);
|
||||
await expect(moduleReturn).toBeVisible();
|
||||
await page.screenshot({ path: test.info().outputPath('chat-collapsed-hover-code-return.png') });
|
||||
await moduleReturn.click();
|
||||
await expect(page).toHaveURL(/\/module-select$/);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user