Align Code navigation and branding with workspace header rows

This commit is contained in:
鲨鱼辣椒
2026-09-30 00:39:24 +08:00
parent 9d47cd2146
commit 883ae1d788
10 changed files with 169 additions and 50 deletions

View File

@@ -78,22 +78,26 @@ test.describe('Makelore module navigation without setup flow', () => {
await expect(page.getByTestId('sidebar-course-project-project-storybook')).toHaveAttribute('aria-current', 'page');
const sidebar = page.getByTestId('sidebar');
const moduleReturn = sidebar.getByTestId('sidebar-module-switcher-trigger');
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(sidebar.getByTestId('sidebar-module-return-icon')).toBeVisible();
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 sidebar.getByRole('button', { name: '折叠侧栏' }).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);
expect(returnBox!.y - sidebarBox!.y).toBe(0);
expect(brandBox!.y - sidebarBox!.y).toBe(40);
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);
@@ -107,8 +111,17 @@ test.describe('Makelore module navigation without setup flow', () => {
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(56);
await expect(community).toHaveCSS('background-color', 'rgb(232, 240, 223)');
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') });
@@ -117,10 +130,16 @@ test.describe('Makelore module navigation without setup flow', () => {
await sidebarResizer.press('Home');
await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '128');
const narrowReturn = await moduleReturn.boundingBox();
const narrowToggle = await sidebar.getByRole('button', { name: '折叠侧栏' }).boundingBox();
const narrowToggle = await page.getByRole('button', { name: '折叠侧栏' }).boundingBox();
expect(narrowReturn!.x + narrowReturn!.width).toBeLessThanOrEqual(narrowToggle!.x);
await expect(moduleReturn).toBeVisible();
await expect(sidebar.getByRole('button', { name: '折叠侧栏' })).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') });
@@ -155,8 +174,11 @@ test.describe('Makelore module navigation without setup flow', () => {
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();

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

@@ -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', () => ({
@@ -91,7 +102,10 @@ describe('TitleBar platform behavior', () => {
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.queryByRole('button', { name: '折叠侧栏' })).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('renders the painting title bar as a transparent overlay with the logo at the window edge', () => {
@@ -179,14 +193,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: '固定侧栏' });
@@ -195,7 +209,67 @@ describe('TitleBar platform behavior', () => {
fireEvent.click(pinButton);
expect(useSettingsStore.getState().sidebarCollapsed).toBe(false);
expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveStyle({ width: '171px', minWidth: '171px', flexBasis: '171px' });
expect(screen.queryByRole('button', { name: '折叠侧栏' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '折叠侧栏' })).toBeEnabled();
});
it.each([
['darwin', 168, 168, 'pl-[88px]'],
['win32', 128, 88, 'pl-2'],
] 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', () => {