From 0d66939eb73e9ffeb74a8d5609511cacd5c03353 Mon Sep 17 00:00:00 2001 From: hugo Date: Thu, 19 Feb 2026 22:50:21 +0100 Subject: [PATCH] fix: macosx UI cleanup --- src/main/ipc/handlers.ts | 38 +++++ src/main/main.ts | 146 +++++++++--------- src/main/shared/menuCommands.ts | 40 +++++ src/renderer/App.tsx | 19 +++ .../WindowTitleBar/WindowTitleBar.tsx | 121 +++------------ tests/ipc/handlers.test.ts | 104 +++++++++++++ .../components/WindowTitleBar.test.tsx | 14 +- tests/renderer/menuCommands.test.ts | 27 ++++ 8 files changed, 333 insertions(+), 176 deletions(-) diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index b60f9b7..d6cf421 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -83,6 +83,33 @@ function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean { case 'toggleDevTools': sender.toggleDevTools?.(); return true; + case 'reload': + sender.reload?.(); + return true; + case 'forceReload': + sender.reloadIgnoringCache?.(); + return true; + case 'resetZoom': + sender.setZoomLevel?.(0); + return true; + case 'zoomIn': { + const currentZoomLevel = sender.getZoomLevel?.() ?? 0; + sender.setZoomLevel?.(currentZoomLevel + 0.5); + return true; + } + case 'zoomOut': { + const currentZoomLevel = sender.getZoomLevel?.() ?? 0; + sender.setZoomLevel?.(currentZoomLevel - 0.5); + return true; + } + case 'toggleFullScreen': { + const ownerWindow = BrowserWindow.fromWebContents(sender); + if (!ownerWindow) { + return false; + } + ownerWindow.setFullScreen(!ownerWindow.isFullScreen()); + return true; + } default: return false; } @@ -748,6 +775,17 @@ export function registerIpcHandlers(): void { return; } + if (typedAction === 'openInBrowser') { + await shell.openExternal('http://localhost:4123/'); + return; + } + + if (typedAction === 'openDataFolder') { + const paths = getDatabase().getDataPaths(); + await shell.openPath(path.dirname(paths.database)); + return; + } + if (typedAction === 'reportIssue') { await shell.openExternal('https://github.com/rfc1437/bDS/issues'); return; diff --git a/src/main/main.ts b/src/main/main.ts index a940ba5..578fa9a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -8,13 +8,13 @@ import { eq } from 'drizzle-orm'; import { getMediaEngine } from './engine/MediaEngine'; import { getPostEngine } from './engine/PostEngine'; import { PreviewServer } from './engine/PreviewServer'; -import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands'; +import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, APP_MENU_ITEM_IDS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands'; let mainWindow: BrowserWindow | null = null; let previewServer: PreviewServer | null = null; let activePreviewPostId: string | null = null; const PREVIEW_SERVER_PORT = 4123; -const BLOG_PREVIEW_POST_MENU_ID = 'blog.previewPost'; +const BLOG_PREVIEW_POST_MENU_ID = APP_MENU_ITEM_IDS.previewPost; // Check if dev server is likely running (only in development) const isDev = process.env.NODE_ENV === 'development'; @@ -178,12 +178,70 @@ function createApplicationMenu(): Menu { return acc; }, {}); - const triggerMenuAction = (action: AppMenuAction): void => { + const triggerMenuAction = async (action: AppMenuAction): Promise => { if (action === 'quit') { app.quit(); return; } + if (action === 'openInBrowser') { + await openPreviewInBrowser().catch((error) => { + console.error('Failed to open preview in browser:', error); + }); + return; + } + + if (action === 'openDataFolder') { + const paths = getDatabase().getDataPaths(); + void shell.openPath(path.dirname(paths.database)); + return; + } + + if (action === 'previewPost') { + await openActivePostPreviewInBrowser().catch((error) => { + console.error('Failed to preview active post in browser:', error); + }); + return; + } + + if (action === 'reload') { + mainWindow?.webContents.reload(); + return; + } + + if (action === 'forceReload') { + mainWindow?.webContents.reloadIgnoringCache(); + return; + } + + if (action === 'resetZoom') { + mainWindow?.webContents.setZoomLevel(0); + return; + } + + if (action === 'zoomIn') { + if (mainWindow) { + const zoomLevel = mainWindow.webContents.getZoomLevel(); + mainWindow.webContents.setZoomLevel(zoomLevel + 0.5); + } + return; + } + + if (action === 'zoomOut') { + if (mainWindow) { + const zoomLevel = mainWindow.webContents.getZoomLevel(); + mainWindow.webContents.setZoomLevel(zoomLevel - 0.5); + } + return; + } + + if (action === 'toggleFullScreen') { + if (mainWindow) { + mainWindow.setFullScreen(!mainWindow.isFullScreen()); + } + return; + } + if (action === 'viewOnGitHub') { void shell.openExternal('https://github.com/rfc1437/bDS'); return; @@ -217,8 +275,10 @@ function createApplicationMenu(): Menu { return { label: definition.label, accelerator: definition.accelerator, - click: () => { - triggerMenuAction(action); + id: definition.id, + enabled: definition.enabled, + click: async () => { + await triggerMenuAction(action); }, }; }; @@ -229,7 +289,9 @@ function createApplicationMenu(): Menu { return []; } - return group.items.map((item) => { + const filteredItems = group.items.filter(item => isDev || item.action !== 'toggleDevTools'); + + return filteredItems.map((item) => { if (item.separator) { return { type: 'separator' }; } @@ -241,33 +303,7 @@ function createApplicationMenu(): Menu { const template: MenuItemConstructorOptions[] = [ { label: 'File', - submenu: [ - buildSharedMenuItem('newPost'), - buildSharedMenuItem('importMedia'), - { type: 'separator' }, - buildSharedMenuItem('save'), - { type: 'separator' }, - { - label: 'Open in Browser', - click: async () => { - try { - await openPreviewInBrowser(); - } catch (error) { - console.error('Failed to open preview in browser:', error); - } - }, - }, - { type: 'separator' }, - { - label: 'Open Data Folder', - click: async () => { - const paths = getDatabase().getDataPaths(); - shell.openPath(path.dirname(paths.database)); - }, - }, - { type: 'separator' }, - buildSharedMenuItem('quit'), - ], + submenu: buildSharedGroupMenuItems('File'), }, { label: 'Edit', @@ -275,51 +311,11 @@ function createApplicationMenu(): Menu { }, { label: 'View', - submenu: [ - ...buildSharedGroupMenuItems('View'), - { type: 'separator' }, - { role: 'reload' }, - { role: 'forceReload' }, - { - label: 'Toggle Developer Tools', - accelerator: process.platform === 'darwin' ? 'Cmd+Option+I' : 'Ctrl+Shift+I', - click: () => { - mainWindow?.webContents.toggleDevTools(); - }, - }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ], + submenu: buildSharedGroupMenuItems('View'), }, { label: 'Blog', - submenu: [ - buildSharedMenuItem('publishSelected'), - { type: 'separator' }, - { - label: 'Preview Post', - id: BLOG_PREVIEW_POST_MENU_ID, - enabled: false, - accelerator: 'CmdOrCtrl+Shift+V', - click: async () => { - try { - await openActivePostPreviewInBrowser(); - } catch (error) { - console.error('Failed to preview active post in browser:', error); - } - }, - }, - { type: 'separator' }, - buildSharedMenuItem('rebuildDatabase'), - buildSharedMenuItem('reindexText'), - { type: 'separator' }, - buildSharedMenuItem('metadataDiff'), - buildSharedMenuItem('generateSitemap'), - ], + submenu: buildSharedGroupMenuItems('Blog'), }, { label: 'Help', diff --git a/src/main/shared/menuCommands.ts b/src/main/shared/menuCommands.ts index 6fa84f3..fbdb9d9 100644 --- a/src/main/shared/menuCommands.ts +++ b/src/main/shared/menuCommands.ts @@ -2,6 +2,8 @@ export type AppMenuAction = | 'newPost' | 'importMedia' | 'save' + | 'openInBrowser' + | 'openDataFolder' | 'quit' | 'undo' | 'redo' @@ -17,7 +19,14 @@ export type AppMenuAction = | 'toggleSidebar' | 'togglePanel' | 'toggleDevTools' + | 'reload' + | 'forceReload' + | 'resetZoom' + | 'zoomIn' + | 'zoomOut' + | 'toggleFullScreen' | 'publishSelected' + | 'previewPost' | 'rebuildDatabase' | 'reindexText' | 'metadataDiff' @@ -35,6 +44,8 @@ export interface AppMenuItemDefinition { accelerator?: string; separator?: boolean; role?: AppMenuRole; + id?: string; + enabled?: boolean; } export interface AppMenuGroupDefinition { @@ -42,14 +53,23 @@ export interface AppMenuGroupDefinition { items: AppMenuItemDefinition[]; } +export const APP_MENU_ITEM_IDS = { + previewPost: 'blog.previewPost', +} as const; + export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [ { label: 'File', items: [ { label: 'New Post', action: 'newPost', accelerator: 'CmdOrCtrl+N' }, { label: 'Import Media...', action: 'importMedia', accelerator: 'CmdOrCtrl+I' }, + { label: '', action: 'file-separator-0', separator: true }, { label: 'Save', action: 'save', accelerator: 'CmdOrCtrl+S' }, { label: '', action: 'file-separator-1', separator: true }, + { label: 'Open in Browser', action: 'openInBrowser' }, + { label: '', action: 'file-separator-2', separator: true }, + { label: 'Open Data Folder', action: 'openDataFolder' }, + { label: '', action: 'file-separator-3', separator: true }, { label: 'Quit', action: 'quit', accelerator: 'CmdOrCtrl+Q' }, ], }, @@ -78,14 +98,27 @@ export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [ { label: 'Toggle Sidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' }, { label: 'Toggle Panel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' }, { label: 'Toggle Developer Tools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' }, + { label: '', action: 'view-separator-1', separator: true }, + { label: 'Reload', action: 'reload' }, + { label: 'Force Reload', action: 'forceReload' }, + { label: '', action: 'view-separator-2', separator: true }, + { label: 'Actual Size', action: 'resetZoom' }, + { label: 'Zoom In', action: 'zoomIn' }, + { label: 'Zoom Out', action: 'zoomOut' }, + { label: '', action: 'view-separator-3', separator: true }, + { label: 'Toggle Full Screen', action: 'toggleFullScreen' }, ], }, { label: 'Blog', items: [ { label: 'Publish Selected', action: 'publishSelected', accelerator: 'CmdOrCtrl+Shift+P' }, + { label: '', action: 'blog-separator-1', separator: true }, + { label: 'Preview Post', action: 'previewPost', id: APP_MENU_ITEM_IDS.previewPost, enabled: false, accelerator: 'CmdOrCtrl+Shift+V' }, + { label: '', action: 'blog-separator-2', separator: true }, { label: 'Rebuild Database from Files', action: 'rebuildDatabase' }, { label: 'Reindex Search Text', action: 'reindexText' }, + { label: '', action: 'blog-separator-3', separator: true }, { label: 'Metadata Diff Tool', action: 'metadataDiff' }, { label: 'Generate Sitemap', action: 'generateSitemap' }, ], @@ -113,6 +146,7 @@ export const APP_MENU_ACTION_EVENT_MAP: Partial> = toggleSidebar: 'menu:toggleSidebar', togglePanel: 'menu:togglePanel', toggleDevTools: 'menu:toggleDevTools', + previewPost: 'menu:previewPost', publishSelected: 'menu:publishSelected', rebuildDatabase: 'menu:rebuildDatabase', reindexText: 'menu:reindexText', @@ -131,4 +165,10 @@ export const APP_MENU_WEB_CONTENTS_ACTIONS: ReadonlySet = new Set 'delete', 'selectAll', 'toggleDevTools', + 'reload', + 'forceReload', + 'resetZoom', + 'zoomIn', + 'zoomOut', + 'toggleFullScreen', ]); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 720b1d0..92323a7 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -288,6 +288,25 @@ const App: React.FC = () => { }) || (() => {}) ); + unsubscribers.push( + window.electronAPI?.on('menu:previewPost', async () => { + try { + const selectedPostId = useAppStore.getState().selectedPostId; + if (!selectedPostId) { + return; + } + + const previewUrl = await window.electronAPI?.posts.getPreviewUrl(selectedPostId); + if (typeof previewUrl === 'string' && previewUrl.length > 0) { + window.open(previewUrl, '_blank', 'noopener'); + } + } catch (error) { + console.error('Failed to open selected post preview:', error); + showToast.error('Failed to open selected post preview'); + } + }) || (() => {}) + ); + unsubscribers.push( window.electronAPI?.on('menu:openDocumentation', () => { openTab({ id: 'documentation', type: 'documentation', isTransient: false }); diff --git a/src/renderer/components/WindowTitleBar/WindowTitleBar.tsx b/src/renderer/components/WindowTitleBar/WindowTitleBar.tsx index 24935c8..b3fcd70 100644 --- a/src/renderer/components/WindowTitleBar/WindowTitleBar.tsx +++ b/src/renderer/components/WindowTitleBar/WindowTitleBar.tsx @@ -133,83 +133,6 @@ export const WindowTitleBar: React.FC = () => { }; }, []); - useEffect(() => { - if (!isMac) { - return; - } - - const rootStyle = document.documentElement.style; - let isDisposed = false; - let resolutionQuery: MediaQueryList | null = null; - - const syncMacInset = async () => { - const metrics = await window.electronAPI?.app?.getTitleBarMetrics?.(); - if (isDisposed) { - return; - } - - if (metrics && Number.isFinite(metrics.macosLeftInset)) { - rootStyle.setProperty('--bds-titlebar-macos-left-inset', `${Math.max(0, Math.round(metrics.macosLeftInset))}px`); - } - }; - - const bindResolutionListener = () => { - if (typeof window.matchMedia !== 'function') { - return; - } - - if (resolutionQuery && typeof resolutionQuery.removeEventListener === 'function') { - resolutionQuery.removeEventListener('change', onResolutionChange); - } - - resolutionQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); - if (typeof resolutionQuery.addEventListener === 'function') { - resolutionQuery.addEventListener('change', onResolutionChange); - } - }; - - const onResolutionChange = () => { - void syncMacInset(); - bindResolutionListener(); - }; - - const onResize = () => { - void syncMacInset(); - }; - - const onVisibilityChange = () => { - if (!document.hidden) { - void syncMacInset(); - } - }; - - const canListenToWindowEvents = typeof window.addEventListener === 'function'; - const canListenToDocumentEvents = typeof document.addEventListener === 'function'; - - void syncMacInset(); - bindResolutionListener(); - if (canListenToWindowEvents) { - window.addEventListener('resize', onResize); - } - if (canListenToDocumentEvents) { - document.addEventListener('visibilitychange', onVisibilityChange); - } - - return () => { - isDisposed = true; - if (canListenToWindowEvents && typeof window.removeEventListener === 'function') { - window.removeEventListener('resize', onResize); - } - if (canListenToDocumentEvents && typeof document.removeEventListener === 'function') { - document.removeEventListener('visibilitychange', onVisibilityChange); - } - - if (resolutionQuery && typeof resolutionQuery.removeEventListener === 'function') { - resolutionQuery.removeEventListener('change', onResolutionChange); - } - }; - }, [isMac]); - useEffect(() => { const updateTitle = () => { setWindowTitle(document.title || 'Blogging Desktop Server'); @@ -363,6 +286,10 @@ export const WindowTitleBar: React.FC = () => { }, [openMenu?.label]); useEffect(() => { + if (isMac) { + return; + } + const onKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented || event.metaKey || event.ctrlKey) { return; @@ -401,7 +328,7 @@ export const WindowTitleBar: React.FC = () => { document.removeEventListener('keydown', onKeyDown); document.removeEventListener('mousedown', onDocumentMouseDown); }; - }, [mnemonicByKey, showMnemonics]); + }, [isMac, mnemonicByKey, showMnemonics]); const handleMenuButtonClick = (event: React.MouseEvent, label: string) => { const left = getMenuLeft(label); @@ -461,23 +388,25 @@ export const WindowTitleBar: React.FC = () => { return (
-
- {visibleMenuGroups.map(group => ( - - ))} -
+ {!isMac && ( +
+ {visibleMenuGroups.map(group => ( + + ))} +
+ )}
{windowTitle} @@ -512,7 +441,7 @@ export const WindowTitleBar: React.FC = () => {
- {openMenu && activeMenu && ( + {!isMac && openMenu && activeMenu && (
{ expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/rfc1437/bDS'); expect(send).not.toHaveBeenCalled(); }); + + it('should open preview root URL when action is openInBrowser', async () => { + const { shell } = await import('electron'); + const send = vi.fn(); + const event = { sender: { send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'openInBrowser'); + + expect(shell.openExternal).toHaveBeenCalledWith('http://localhost:4123/'); + expect(send).not.toHaveBeenCalled(); + }); + + it('should open the data folder when action is openDataFolder', async () => { + const { shell } = await import('electron'); + const send = vi.fn(); + const event = { sender: { send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'openDataFolder'); + + expect(shell.openPath).toHaveBeenCalledWith('/mock/data'); + expect(send).not.toHaveBeenCalled(); + }); + + it('should forward previewPost to renderer menu channel', async () => { + const send = vi.fn(); + const event = { sender: { send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'previewPost'); + + expect(send).toHaveBeenCalledWith('menu:previewPost'); + }); + + it('should reload sender when action is reload', async () => { + const reload = vi.fn(); + const send = vi.fn(); + const event = { sender: { reload, send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'reload'); + + expect(reload).toHaveBeenCalled(); + expect(send).not.toHaveBeenCalled(); + }); + + it('should force reload sender when action is forceReload', async () => { + const reloadIgnoringCache = vi.fn(); + const send = vi.fn(); + const event = { sender: { reloadIgnoringCache, send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'forceReload'); + + expect(reloadIgnoringCache).toHaveBeenCalled(); + expect(send).not.toHaveBeenCalled(); + }); + + it('should reset zoom level when action is resetZoom', async () => { + const setZoomLevel = vi.fn(); + const send = vi.fn(); + const event = { sender: { setZoomLevel, send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'resetZoom'); + + expect(setZoomLevel).toHaveBeenCalledWith(0); + expect(send).not.toHaveBeenCalled(); + }); + + it('should zoom in when action is zoomIn', async () => { + const getZoomLevel = vi.fn(() => 0); + const setZoomLevel = vi.fn(); + const send = vi.fn(); + const event = { sender: { getZoomLevel, setZoomLevel, send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'zoomIn'); + + expect(setZoomLevel).toHaveBeenCalledWith(0.5); + expect(send).not.toHaveBeenCalled(); + }); + + it('should zoom out when action is zoomOut', async () => { + const getZoomLevel = vi.fn(() => 0.5); + const setZoomLevel = vi.fn(); + const send = vi.fn(); + const event = { sender: { getZoomLevel, setZoomLevel, send } }; + + await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'zoomOut'); + + expect(setZoomLevel).toHaveBeenCalledWith(0); + expect(send).not.toHaveBeenCalled(); + }); + + it('should toggle fullscreen on owner window when action is toggleFullScreen', async () => { + const { BrowserWindow } = await import('electron'); + const sender = { send: vi.fn() }; + const ownerWindow = { + isFullScreen: vi.fn(() => false), + setFullScreen: vi.fn(), + }; + + vi.mocked(BrowserWindow.fromWebContents).mockReturnValue(ownerWindow as unknown as ReturnType); + + await invokeHandlerWithEvent({ sender }, 'app:triggerMenuAction', 'toggleFullScreen'); + + expect(BrowserWindow.fromWebContents).toHaveBeenCalledWith(sender); + expect(ownerWindow.setFullScreen).toHaveBeenCalledWith(true); + }); }); }); diff --git a/tests/renderer/components/WindowTitleBar.test.tsx b/tests/renderer/components/WindowTitleBar.test.tsx index f7db61b..3d5551b 100644 --- a/tests/renderer/components/WindowTitleBar.test.tsx +++ b/tests/renderer/components/WindowTitleBar.test.tsx @@ -19,7 +19,7 @@ describe('WindowTitleBar', () => { }); }); - it('applies a macOS class to the title bar root for platform-specific spacing', () => { + it('renders title bar on macOS but hides simulated menu buttons', () => { Object.defineProperty(navigator, 'platform', { value: 'MacIntel', configurable: true, @@ -27,10 +27,14 @@ describe('WindowTitleBar', () => { render(); - expect(screen.getByTestId('window-titlebar')).toHaveClass('is-mac'); + expect(screen.getByTestId('window-titlebar')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'File' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull(); + expect(screen.getByLabelText('Toggle Sidebar')).toBeInTheDocument(); + expect(screen.getByLabelText('Toggle Panel')).toBeInTheDocument(); }); - it('sets macOS title bar inset CSS variable from dynamic native metrics', async () => { + it('does not request macOS title bar metrics when simulated title bar is disabled', async () => { Object.defineProperty(navigator, 'platform', { value: 'MacIntel', configurable: true, @@ -48,8 +52,8 @@ describe('WindowTitleBar', () => { await Promise.resolve(); }); - expect(getTitleBarMetrics).toHaveBeenCalled(); - expect(document.documentElement.style.getPropertyValue('--bds-titlebar-macos-left-inset')).toBe('102px'); + expect(getTitleBarMetrics).not.toHaveBeenCalled(); + expect(document.documentElement.style.getPropertyValue('--bds-titlebar-macos-left-inset')).toBe(''); }); it('renders a right-side sidebar toggle button and toggles store state', () => { diff --git a/tests/renderer/menuCommands.test.ts b/tests/renderer/menuCommands.test.ts index 6f94af8..6101efe 100644 --- a/tests/renderer/menuCommands.test.ts +++ b/tests/renderer/menuCommands.test.ts @@ -12,4 +12,31 @@ describe('Help menu documentation entry', () => { it('maps Open Documentation to a renderer menu event', () => { expect(APP_MENU_ACTION_EVENT_MAP.openDocumentation).toBe('menu:openDocumentation'); }); + + it('includes Open in Browser and Open Data Folder actions in File menu', () => { + const fileGroup = APP_MENU_GROUPS.find((group) => group.label === 'File'); + + expect(fileGroup).toBeDefined(); + expect(fileGroup?.items.some((item) => item.action === 'openInBrowser')).toBe(true); + expect(fileGroup?.items.some((item) => item.action === 'openDataFolder')).toBe(true); + }); + + it('includes Preview Post action in Blog menu', () => { + const blogGroup = APP_MENU_GROUPS.find((group) => group.label === 'Blog'); + + expect(blogGroup).toBeDefined(); + expect(blogGroup?.items.some((item) => item.action === 'previewPost')).toBe(true); + }); + + it('includes shared View actions for reload, zoom and fullscreen controls', () => { + const viewGroup = APP_MENU_GROUPS.find((group) => group.label === 'View'); + + expect(viewGroup).toBeDefined(); + expect(viewGroup?.items.some((item) => item.action === 'reload')).toBe(true); + expect(viewGroup?.items.some((item) => item.action === 'forceReload')).toBe(true); + expect(viewGroup?.items.some((item) => item.action === 'resetZoom')).toBe(true); + expect(viewGroup?.items.some((item) => item.action === 'zoomIn')).toBe(true); + expect(viewGroup?.items.some((item) => item.action === 'zoomOut')).toBe(true); + expect(viewGroup?.items.some((item) => item.action === 'toggleFullScreen')).toBe(true); + }); });