feat: custom title bar that is more compact
This commit is contained in:
@@ -120,7 +120,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
symbolColor: '#cccccc',
|
||||
height: 34,
|
||||
},
|
||||
autoHideMenuBar: true,
|
||||
autoHideMenuBar: false,
|
||||
}));
|
||||
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
|
||||
@@ -1339,6 +1339,39 @@ describe('IPC Handlers', () => {
|
||||
expect(result).toBe('/Users/test/bds/project-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('app:triggerMenuAction', () => {
|
||||
it('should forward custom titlebar action to renderer menu channel', async () => {
|
||||
const send = vi.fn();
|
||||
const event = { sender: { send } };
|
||||
|
||||
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'newPost');
|
||||
|
||||
expect(send).toHaveBeenCalledWith('menu:newPost');
|
||||
});
|
||||
|
||||
it('should execute default edit actions on webContents sender', async () => {
|
||||
const undo = vi.fn();
|
||||
const send = vi.fn();
|
||||
const event = { sender: { undo, send } };
|
||||
|
||||
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'undo');
|
||||
|
||||
expect(undo).toHaveBeenCalled();
|
||||
expect(send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should execute toggleDevTools on sender when action is toggleDevTools', async () => {
|
||||
const toggleDevTools = vi.fn();
|
||||
const send = vi.fn();
|
||||
const event = { sender: { toggleDevTools, send } };
|
||||
|
||||
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'toggleDevTools');
|
||||
|
||||
expect(toggleDevTools).toHaveBeenCalled();
|
||||
expect(send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============ Error Handling ============
|
||||
|
||||
@@ -78,4 +78,12 @@ describe('TabBar', () => {
|
||||
expect(await screen.findByText('abc123d feat: improve commit diff tabs')).toBeInTheDocument();
|
||||
expect((window as any).electronAPI.git.getHistory).toHaveBeenCalledWith('/repo/path', 200);
|
||||
});
|
||||
|
||||
it('does not render the tab bar when there are no open tabs', () => {
|
||||
useAppStore.setState({ tabs: [], activeTabId: null });
|
||||
|
||||
const { container } = render(<TabBar />);
|
||||
|
||||
expect(container.querySelector('.tab-bar')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
33
tests/renderer/components/WindowTitleBar.styles.test.ts
Normal file
33
tests/renderer/components/WindowTitleBar.styles.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
describe('WindowTitleBar styles', () => {
|
||||
const cssPath = path.resolve(
|
||||
__dirname,
|
||||
'../../../src/renderer/components/WindowTitleBar/WindowTitleBar.css'
|
||||
);
|
||||
|
||||
it('defines both standard and webkit drag regions for cross-platform support', () => {
|
||||
const css = fs.readFileSync(cssPath, 'utf8');
|
||||
|
||||
expect(css).toMatch(/app-region:\s*drag;/);
|
||||
expect(css).toMatch(/-webkit-app-region:\s*drag;/);
|
||||
expect(css).toMatch(/app-region:\s*no-drag;/);
|
||||
expect(css).toMatch(/-webkit-app-region:\s*no-drag;/);
|
||||
});
|
||||
|
||||
it('reserves overlay control width to keep custom actions clickable on Windows/Linux', () => {
|
||||
const css = fs.readFileSync(cssPath, 'utf8');
|
||||
|
||||
expect(css).toMatch(/padding-right:\s*calc\(10px\s*\+\s*var\(--bds-titlebar-overlay-right,\s*0px\)\)/);
|
||||
});
|
||||
|
||||
it('uses neutral focus styling for menu buttons without blue accent outlines', () => {
|
||||
const css = fs.readFileSync(cssPath, 'utf8');
|
||||
|
||||
expect(css).toMatch(/\.window-titlebar-menu-button:focus/);
|
||||
expect(css).toMatch(/outline:\s*none;/);
|
||||
expect(css).toMatch(/box-shadow:\s*none;/);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { WindowTitleBar } from '../../../src/renderer/components/WindowTitleBar/WindowTitleBar';
|
||||
import { useAppStore } from '../../../src/renderer/store';
|
||||
@@ -36,4 +36,129 @@ describe('WindowTitleBar', () => {
|
||||
expect(iconFrame).toHaveAttribute('data-shape', 'frame-square');
|
||||
expect(iconPane).toHaveAttribute('data-shape', 'left-half');
|
||||
});
|
||||
|
||||
it('updates overlay inset CSS variables when window controls geometry changes', () => {
|
||||
const geometryListeners = new Set<EventListener>();
|
||||
let rect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 924,
|
||||
height: 34,
|
||||
top: 0,
|
||||
right: 924,
|
||||
bottom: 34,
|
||||
left: 0,
|
||||
toJSON: () => '',
|
||||
} as DOMRect;
|
||||
|
||||
const mockOverlay = {
|
||||
visible: true,
|
||||
getTitlebarAreaRect: () => rect,
|
||||
addEventListener: (type: string, listener: EventListenerOrEventListenerObject) => {
|
||||
if (type === 'geometrychange' && typeof listener === 'function') {
|
||||
geometryListeners.add(listener);
|
||||
}
|
||||
},
|
||||
removeEventListener: (type: string, listener: EventListenerOrEventListenerObject) => {
|
||||
if (type === 'geometrychange' && typeof listener === 'function') {
|
||||
geometryListeners.delete(listener);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', { value: 1024, configurable: true });
|
||||
(navigator as Navigator & { windowControlsOverlay?: typeof mockOverlay }).windowControlsOverlay = mockOverlay;
|
||||
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--bds-titlebar-overlay-right')).toBe('100px');
|
||||
expect(document.documentElement.style.getPropertyValue('--bds-titlebar-overlay-left')).toBe('0px');
|
||||
|
||||
rect = {
|
||||
...rect,
|
||||
width: 824,
|
||||
right: 824,
|
||||
} as DOMRect;
|
||||
|
||||
geometryListeners.forEach(listener => listener(new Event('geometrychange')));
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--bds-titlebar-overlay-right')).toBe('200px');
|
||||
});
|
||||
|
||||
it('renders the window title centered in the custom title bar', () => {
|
||||
document.title = 'Blogging Desktop Server';
|
||||
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
const title = screen.getByTestId('window-titlebar-title');
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('Blogging Desktop Server');
|
||||
});
|
||||
|
||||
it('renders VS Code-style top menu labels in the title bar', () => {
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'File' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Blog' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Help' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('dispatches menu action through electron API when menu item is clicked', () => {
|
||||
const triggerMenuAction = vi.fn().mockResolvedValue(undefined);
|
||||
window.electronAPI.app = {
|
||||
...(window.electronAPI.app || {}),
|
||||
triggerMenuAction,
|
||||
};
|
||||
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'File' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New Post Ctrl+N' }));
|
||||
|
||||
expect(triggerMenuAction).toHaveBeenCalledWith('newPost');
|
||||
});
|
||||
|
||||
it('shows default edit actions with accelerators in Edit menu', () => {
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Undo Ctrl+Z' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Redo Ctrl+Y' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Cut Ctrl+X' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Copy Ctrl+C' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Paste Ctrl+V' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Select All Ctrl+A' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows assigned accelerators in View and Blog menus', () => {
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View' }));
|
||||
expect(screen.getByRole('button', { name: 'Posts Ctrl+1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Media Ctrl+2' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Toggle Sidebar Ctrl+B' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Toggle Panel Ctrl+J' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Blog' }));
|
||||
expect(screen.getByRole('button', { name: 'Publish Selected Ctrl+Shift+P' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Toggle Developer Tools in View menu in development mode', () => {
|
||||
(window as Window & { __BDS_IS_DEV__?: boolean }).__BDS_IS_DEV__ = true;
|
||||
const triggerMenuAction = vi.fn().mockResolvedValue(undefined);
|
||||
window.electronAPI.app = {
|
||||
...(window.electronAPI.app || {}),
|
||||
triggerMenuAction,
|
||||
};
|
||||
|
||||
render(<WindowTitleBar />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Toggle Developer Tools Ctrl+Shift+I' }));
|
||||
|
||||
expect(triggerMenuAction).toHaveBeenCalledWith('toggleDevTools');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,6 +124,9 @@ Object.defineProperty(globalThis, 'window', {
|
||||
cancel: vi.fn(),
|
||||
clearCompleted: vi.fn(),
|
||||
},
|
||||
app: {
|
||||
triggerMenuAction: vi.fn(),
|
||||
},
|
||||
import: {
|
||||
selectAndAnalyze: vi.fn(),
|
||||
analyzeFile: vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user