import { describe, it, expect, vi, afterEach } from 'vitest'; // Mock the database connection module to prevent the native libsql binary from loading. // Some modules (e.g. generatedFileHashStore) import directly from connection.ts // instead of the barrel, so mocking the barrel alone isn't sufficient. vi.mock('../../src/main/database/connection', () => ({ DatabaseConnection: vi.fn(), getDatabase: vi.fn(() => ({ initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => null), getLocalClient: vi.fn(() => null), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), })), initDatabase: vi.fn(() => ({ initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => null), getLocalClient: vi.fn(() => null), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), })), })); describe('main bootstrap preview behavior', () => { afterEach(() => { vi.restoreAllMocks(); vi.resetModules(); }); it.each(['win32', 'linux'])('uses compact unified window decorations on %s', async (platform) => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: platform }); const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn(), quit: vi.fn(), getPath: vi.fn(() => '/tmp/mock-userdata'), isPackaged: false, }; const browserWindowCalls: any[] = []; class BrowserWindowMock { static getAllWindows = vi.fn(() => [{ id: 1 }]); loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; constructor(options: any) { browserWindowCalls.push(options); } } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: BrowserWindowMock, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn(), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getPrimaryDisplay: vi.fn(() => ({ workArea: { x: 0, y: 0, width: 1920, height: 1080 }, workAreaSize: { width: 1920, height: 1080 }, })), }, })); class MockPreviewServer { start = vi.fn().mockResolvedValue(4123); stop = vi.fn().mockResolvedValue(undefined); getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); } vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(browserWindowCalls[0]).toEqual(expect.objectContaining({ titleBarStyle: 'hidden', titleBarOverlay: { color: '#252526', symbolColor: '#cccccc', height: 34, }, autoHideMenuBar: false, })); Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('starts preview server during app startup', async () => { const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn(), quit: vi.fn(), getPath: vi.fn(() => '/tmp/mock-userdata'), isPackaged: false, }; const mockBrowserWindowGetAllWindows = vi.fn(() => [{ id: 1 }]); class MockBrowserWindow { static getAllWindows = mockBrowserWindowGetAllWindows; loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn(), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getPrimaryDisplay: vi.fn(() => ({ workArea: { x: 0, y: 0, width: 1920, height: 1080 }, workAreaSize: { width: 1920, height: 1080 }, })), }, })); const mockPreviewStart = vi.fn().mockResolvedValue(4123); const mockPreviewStop = vi.fn().mockResolvedValue(undefined); const mockPreviewGetBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); class MockPreviewServer { start = mockPreviewStart; stop = mockPreviewStop; getBaseUrl = mockPreviewGetBaseUrl; } vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(mockPreviewStart).toHaveBeenCalledWith(4123); expect(mockApp.whenReady).toHaveBeenCalled(); }); it('enables Blog Preview Post only for active post tab and opens canonical URL', async () => { const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn(), quit: vi.fn(), getPath: vi.fn(() => '/tmp/mock-userdata'), isPackaged: false, }; const mockBrowserWindowGetAllWindows = vi.fn(() => [{ id: 1 }]); class MockBrowserWindow { static getAllWindows = mockBrowserWindowGetAllWindows; loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; } const findMenuItemById = (template: any[], id: string): any | null => { for (const item of template) { if (item && item.id === id) { return item; } if (item?.submenu && Array.isArray(item.submenu)) { const found = findMenuItemById(item.submenu, id); if (found) return found; } } return null; }; let capturedTemplate: any[] = []; const menuObject = { getMenuItemById: (id: string) => findMenuItemById(capturedTemplate, id), }; const ipcMainHandle = vi.fn(); const shellOpenExternal = vi.fn().mockResolvedValue(undefined); vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn((template: any[]) => { capturedTemplate = template; return menuObject; }), setApplicationMenu: vi.fn(), getApplicationMenu: vi.fn(() => menuObject), }, ipcMain: { on: vi.fn(), handle: ipcMainHandle, removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: shellOpenExternal, openPath: vi.fn(), }, })); const mockPreviewStart = vi.fn().mockResolvedValue(4123); const mockPreviewStop = vi.fn().mockResolvedValue(undefined); const mockPreviewGetBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); class MockPreviewServer { start = mockPreviewStart; stop = mockPreviewStop; getBaseUrl = mockPreviewGetBaseUrl; } vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); const postCreatedAt = new Date('2026-02-17T10:00:00.000Z'); const getPost = vi.fn().mockResolvedValue({ id: 'post-42', slug: 'current-post', createdAt: postCreatedAt, }); vi.doMock('../../src/main/engine/PostEngine', () => ({ PostEngine: vi.fn().mockImplementation(function() { return { getPost, }; }), })); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); const previewMenuItem = findMenuItemById(capturedTemplate, 'blog.previewPost'); expect(previewMenuItem).toBeTruthy(); expect(previewMenuItem.enabled).toBe(false); const setPreviewTargetCall = ipcMainHandle.mock.calls.find((call: unknown[]) => call[0] === 'app:setPreviewPostTarget'); expect(setPreviewTargetCall).toBeTruthy(); const setPreviewTargetHandler = setPreviewTargetCall?.[1] as ((event: unknown, postId: string | null) => Promise); await setPreviewTargetHandler({}, 'post-42'); expect(previewMenuItem.enabled).toBe(true); await previewMenuItem.click(); expect(getPost).toHaveBeenCalledWith('post-42'); expect(shellOpenExternal).toHaveBeenCalledWith('http://127.0.0.1:4123/2026/02/17/current-post'); await setPreviewTargetHandler({}, null); expect(previewMenuItem.enabled).toBe(false); }); it('restores previous window bounds unchanged when they fit the current display work area', async () => { const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn(), quit: vi.fn(), getPath: vi.fn((name: string) => (name === 'userData' ? '/tmp/bds-user-data' : '/tmp')), isPackaged: false, }; const browserWindowCalls: any[] = []; class MockBrowserWindow { static getAllWindows = vi.fn(() => [{ id: 1 }]); loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; constructor(options: any) { browserWindowCalls.push(options); } } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn(), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getDisplayMatching: vi.fn(() => ({ workArea: { x: 0, y: 25, width: 1920, height: 1055 }, })), }, })); class MockPreviewServer { start = vi.fn().mockResolvedValue(4123); stop = vi.fn().mockResolvedValue(undefined); getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); } vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('fs', async (importOriginal) => { const actual = await importOriginal(); const mocked = { ...actual, existsSync: vi.fn((targetPath: string) => targetPath.includes('window-state.json')), readFileSync: vi.fn(() => JSON.stringify({ x: 120, y: 80, width: 1280, height: 820 })), writeFileSync: vi.fn(), }; return { ...mocked, default: mocked }; }); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(browserWindowCalls[0]).toEqual(expect.objectContaining({ x: 120, y: 80, width: 1280, height: 820, })); }); it('clamps restored window bounds only when they overflow the current display work area', async () => { const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn(), quit: vi.fn(), getPath: vi.fn((name: string) => (name === 'userData' ? '/tmp/bds-user-data' : '/tmp')), isPackaged: false, }; const browserWindowCalls: any[] = []; class MockBrowserWindow { static getAllWindows = vi.fn(() => [{ id: 1 }]); loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; constructor(options: any) { browserWindowCalls.push(options); } } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn(), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getDisplayMatching: vi.fn(() => ({ workArea: { x: 0, y: 25, width: 1280, height: 775 }, })), }, })); class MockPreviewServer { start = vi.fn().mockResolvedValue(4123); stop = vi.fn().mockResolvedValue(undefined); getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); } vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('fs', async (importOriginal) => { const actual = await importOriginal(); const mocked = { ...actual, existsSync: vi.fn((targetPath: string) => targetPath.includes('window-state.json')), readFileSync: vi.fn(() => JSON.stringify({ x: -40, y: -10, width: 1800, height: 1000 })), writeFileSync: vi.fn(), }; return { ...mocked, default: mocked }; }); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(browserWindowCalls[0]).toEqual(expect.objectContaining({ x: 0, y: 25, width: 1280, height: 775, })); }); it('handles bds deep-link by creating a blogmark post with preferred category', async () => { const listeners = new Map void>(); const ipcHandlers = new Map any>(); const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn((event: string, callback: (...args: any[]) => void) => { listeners.set(event, callback); }), quit: vi.fn(), requestSingleInstanceLock: vi.fn(() => true), setAsDefaultProtocolClient: vi.fn(() => true), getPath: vi.fn(() => '/tmp/mock-userdata'), isPackaged: false, }; const windows: Array<{ webContents: { send: ReturnType } }> = []; class MockBrowserWindow { static getAllWindows = vi.fn(() => windows as any); loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; constructor() { windows.push(this as any); } } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn((channel: string, handler: (...args: any[]) => any) => { ipcHandlers.set(channel, handler); }), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getPrimaryDisplay: vi.fn(() => ({ workArea: { x: 0, y: 0, width: 1920, height: 1080 }, workAreaSize: { width: 1920, height: 1080 }, })), }, })); class MockPreviewServer { start = vi.fn().mockResolvedValue(4123); stop = vi.fn().mockResolvedValue(undefined); getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); } const createPost = vi.fn().mockResolvedValue({ id: 'new-post-id', title: 'Example title', content: '[Example title](https://example.com/)', categories: ['article'], }); vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('../../src/main/engine/PostEngine', () => ({ PostEngine: vi.fn().mockImplementation(function() { return { getPost: vi.fn().mockResolvedValue(null), createPost, }; }), })); vi.doMock('../../src/main/engine/MetaEngine', () => ({ MetaEngine: vi.fn().mockImplementation(function() { return { getProjectMetadata: vi.fn().mockResolvedValue({ blogmarkCategory: 'article' }), }; }), })); vi.doMock('../../src/main/engine/BlogmarkTransformService', () => ({ BlogmarkTransformService: vi.fn().mockImplementation(function() { return { applyTransforms: vi.fn(async (input: { post: { title: string; content: string; categories: string[] } }) => ({ post: input.post, appliedScriptIds: [], errors: [], toasts: [], })), }; }), })); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); const openUrl = listeners.get('open-url'); expect(openUrl).toBeTruthy(); const preventDefault = vi.fn(); openUrl?.({ preventDefault } as any, 'bds://new-post?title=Example%20title&url=https%3A%2F%2Fexample.com%2F'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(preventDefault).toHaveBeenCalled(); expect(createPost).toHaveBeenCalledWith( expect.objectContaining({ title: 'Example title', content: '[Example title](https://example.com/)', categories: ['article'], }), ); const rendererReadyHandler = ipcHandlers.get('app:rendererReady'); await rendererReadyHandler?.(); expect(windows[0]?.webContents.send).toHaveBeenCalledWith( 'blogmark:created', expect.objectContaining({ post: expect.objectContaining({ id: 'new-post-id' }), transform: expect.objectContaining({ appliedScriptIds: [], errors: [], toasts: [], }), }), ); }); it('queues blogmark created event until renderer has finished loading', async () => { const listeners = new Map void>(); const webContentsListeners = new Map void>(); const ipcHandlers = new Map any>(); const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn((event: string, callback: (...args: any[]) => void) => { listeners.set(event, callback); }), quit: vi.fn(), requestSingleInstanceLock: vi.fn(() => true), setAsDefaultProtocolClient: vi.fn(() => true), getPath: vi.fn(() => '/tmp/mock-userdata'), isPackaged: false, }; const windows: Array<{ webContents: { send: ReturnType } }> = []; let rendererLoading = true; class MockBrowserWindow { static getAllWindows = vi.fn(() => windows as any); loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn((event: string, callback: (...args: any[]) => void) => { webContentsListeners.set(event, callback); }), isLoadingMainFrame: vi.fn(() => rendererLoading), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; constructor() { windows.push(this as any); } } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn((channel: string, handler: (...args: any[]) => any) => { ipcHandlers.set(channel, handler); }), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getPrimaryDisplay: vi.fn(() => ({ workArea: { x: 0, y: 0, width: 1920, height: 1080 }, workAreaSize: { width: 1920, height: 1080 }, })), }, })); class MockPreviewServer { start = vi.fn().mockResolvedValue(4123); stop = vi.fn().mockResolvedValue(undefined); getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); } const createPost = vi.fn().mockResolvedValue({ id: 'queued-post-id', title: 'Queued title', content: '[Queued title](https://example.com/)', categories: ['article'], }); vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('../../src/main/engine/PostEngine', () => ({ PostEngine: vi.fn().mockImplementation(function() { return { getPost: vi.fn().mockResolvedValue(null), createPost, }; }), })); vi.doMock('../../src/main/engine/MetaEngine', () => ({ MetaEngine: vi.fn().mockImplementation(function() { return { getProjectMetadata: vi.fn().mockResolvedValue({ blogmarkCategory: 'article' }), }; }), })); vi.doMock('../../src/main/engine/BlogmarkTransformService', () => ({ BlogmarkTransformService: vi.fn().mockImplementation(function() { return { applyTransforms: vi.fn(async (input: { post: { title: string; content: string; categories: string[] } }) => ({ post: input.post, appliedScriptIds: [], errors: [], toasts: [], })), }; }), })); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); const openUrl = listeners.get('open-url'); expect(openUrl).toBeTruthy(); const preventDefault = vi.fn(); openUrl?.({ preventDefault } as any, 'bds://new-post?title=Queued%20title&url=https%3A%2F%2Fexample.com%2F'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(createPost).toHaveBeenCalledTimes(1); expect(windows[0]?.webContents.send).not.toHaveBeenCalledWith( 'blogmark:created', expect.anything(), ); rendererLoading = false; const didFinishLoad = webContentsListeners.get('did-finish-load'); didFinishLoad?.(); expect(windows[0]?.webContents.send).not.toHaveBeenCalledWith( 'blogmark:created', expect.anything(), ); const rendererReadyHandler = ipcHandlers.get('app:rendererReady'); await rendererReadyHandler?.(); expect(windows[0]?.webContents.send).toHaveBeenCalledWith( 'blogmark:created', expect.objectContaining({ post: expect.objectContaining({ id: 'queued-post-id' }), transform: expect.objectContaining({ appliedScriptIds: [], errors: [], toasts: [], }), }), ); }); it('uses active project blogmark category during cold-start deep-link processing', async () => { const originalArgv = process.argv; process.argv = [ 'bds', 'bds://new-post?title=Cold%20start&url=https%3A%2F%2Fexample.com%2Fcold', ]; const mockApp = { name: 'bDS', whenReady: vi.fn(() => Promise.resolve()), on: vi.fn(), quit: vi.fn(), requestSingleInstanceLock: vi.fn(() => true), setAsDefaultProtocolClient: vi.fn(() => true), getPath: vi.fn(() => '/tmp/mock-userdata'), isPackaged: false, }; class MockBrowserWindow { static getAllWindows = vi.fn(() => [{ id: 1 }]); loadURL = vi.fn(); loadFile = vi.fn(); on = vi.fn(); isDestroyed = vi.fn(() => false); webContents = { on: vi.fn(), send: vi.fn(), openDevTools: vi.fn(), toggleDevTools: vi.fn(), }; } vi.doMock('electron', () => ({ app: mockApp, BrowserWindow: MockBrowserWindow, Menu: { buildFromTemplate: vi.fn(() => ({})), setApplicationMenu: vi.fn(), }, ipcMain: { on: vi.fn(), handle: vi.fn(), removeHandler: vi.fn(), }, protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), }, net: { fetch: vi.fn(), }, shell: { openExternal: vi.fn(), openPath: vi.fn(), }, screen: { getPrimaryDisplay: vi.fn(() => ({ workArea: { x: 0, y: 0, width: 1920, height: 1080 }, workAreaSize: { width: 1920, height: 1080 }, })), }, })); class MockPreviewServer { start = vi.fn().mockResolvedValue(4123); stop = vi.fn().mockResolvedValue(undefined); getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123'); } const createPost = vi.fn().mockResolvedValue({ id: 'cold-start-id', title: 'Cold start', content: '[Cold start](https://example.com/cold)', categories: ['aside'], }); vi.doMock('../../src/main/engine/PreviewServer', () => ({ PreviewServer: MockPreviewServer, })); vi.doMock('../../src/main/engine/PostEngine', () => ({ PostEngine: vi.fn().mockImplementation(function() { return { getPost: vi.fn().mockResolvedValue(null), createPost, setProjectContext: vi.fn(), setSearchLanguage: vi.fn(), setMainLanguage: vi.fn(), }; }), })); let currentProjectId = 'default'; vi.doMock('../../src/main/engine/MetaEngine', () => ({ MetaEngine: vi.fn().mockImplementation(function() { return { setProjectContext: vi.fn((projectId: string) => { currentProjectId = projectId; }), syncOnStartup: vi.fn().mockResolvedValue(undefined), getProjectMetadata: vi.fn(async () => ({ blogmarkCategory: currentProjectId === 'project-2' ? 'aside' : 'article', })), }; }), })); vi.doMock('../../src/main/engine/ProjectEngine', () => ({ ProjectEngine: vi.fn().mockImplementation(function() { return { getActiveProject: vi.fn().mockResolvedValue({ id: 'project-2', dataPath: '/tmp/project-2', }), getDataDir: vi.fn(() => '/tmp/project-2'), }; }), })); vi.doMock('../../src/main/database', () => { const mockDb = { initializeLocal: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), getLocal: vi.fn(() => ({ select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ get: vi.fn().mockResolvedValue(null), })), })), })), })), getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })), getDbPath: vi.fn(() => '/tmp/mock.db'), }; return { initDatabase: vi.fn(() => mockDb), getDatabase: vi.fn(() => mockDb), }; }); vi.doMock('../../src/main/ipc', () => ({ registerIpcHandlers: vi.fn(), registerEventForwarding: vi.fn(), registerChatHandlers: vi.fn(), initializeChatHandlers: vi.fn(), cleanupChatHandlers: vi.fn().mockResolvedValue(undefined), })); vi.doMock('../../src/main/database/schema', () => ({ media: {}, projects: {}, dbNotifications: {}, })); vi.doMock('drizzle-orm', () => ({ eq: vi.fn(), })); vi.doMock('../../src/main/engine/MediaEngine', () => ({ MediaEngine: vi.fn().mockImplementation(function() { return { getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }), setProjectContext: vi.fn(), setSearchLanguage: vi.fn(), }; }), })); vi.doMock('../../src/main/engine/EmbeddingEngine', () => ({ EmbeddingEngine: vi.fn().mockImplementation(function() { return { setProjectContext: vi.fn().mockResolvedValue(undefined), initialize: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), indexUnindexedPosts: vi.fn().mockResolvedValue(undefined), }; }), })); vi.doMock('../../src/main/engine/BlogmarkTransformService', () => ({ BlogmarkTransformService: vi.fn().mockImplementation(function() { return { applyTransforms: vi.fn(async (input: { post: { title: string; content: string; categories: string[]; tags: string[] } }) => ({ post: input.post, appliedScriptIds: [], errors: [], toasts: [], })), }; }), })); await import('../../src/main/main'); await new Promise((resolve) => setTimeout(resolve, 0)); expect(createPost).toHaveBeenCalledWith( expect.objectContaining({ categories: ['aside'], }), ); process.argv = originalArgv; }); });