Align fullscreen return button with project actions
This commit is contained in:
@@ -126,6 +126,17 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -24,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 });
|
||||
});
|
||||
|
||||
@@ -84,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', () => {
|
||||
@@ -108,6 +109,50 @@ describe('TitleBar platform behavior', () => {
|
||||
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';
|
||||
|
||||
@@ -214,7 +259,7 @@ describe('TitleBar platform behavior', () => {
|
||||
|
||||
it.each([
|
||||
['darwin', 168, 168, 'pl-[88px]'],
|
||||
['win32', 128, 88, 'pl-2'],
|
||||
['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 });
|
||||
|
||||
Reference in New Issue
Block a user