Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion electron/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WindowManager } from './window.js';
import { DatabaseManager } from '../services/database/database.js';
import { setupIpcHandlers } from '../services/ipc/index.js';
import { BackupService } from '../services/backup/index.js';
import { UpdateService } from '../services/update/update.service.js';

let windowManager: WindowManager | null = null;
let dbManager: DatabaseManager | null = null;
Expand All @@ -29,7 +30,7 @@ const initializeApp = async (): Promise<void> => {
},
);

setupIpcHandlers(dbManager, backupService);
setupIpcHandlers(dbManager, backupService, new UpdateService());
windowManager = new WindowManager();
await windowManager.createMainWindow();
};
Expand Down
29 changes: 27 additions & 2 deletions electron/src/preload/preload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer, shell } from 'electron';
import { contextBridge, ipcRenderer } from 'electron';

/**
* Type definitions for database entities
Expand Down Expand Up @@ -128,8 +128,21 @@ interface BackupResult {
path?: string;
}

interface GitHubRelease {
tag_name: string;
html_url: string;
body: string;
name: string;
published_at: string;
}

try {
const electronAPI = {
// ...
// Release Info
getReleaseByTag: (tag: string): Promise<GitHubRelease> =>
ipcRenderer.invoke('get-release-by-tag', tag),

// Projects
getProjects: (): Promise<Project[]> => ipcRenderer.invoke('get-projects'),
createProject: (name: string, description?: string): Promise<Project> =>
Expand Down Expand Up @@ -395,7 +408,19 @@ try {
getBackupDir: (): Promise<string> => ipcRenderer.invoke('backup-get-dir'),

// System
openExternal: (url: string): Promise<void> => shell.openExternal(url),
openExternal: (url: string): Promise<void> =>
ipcRenderer.invoke('open-external', url),

// Updates
checkForUpdates: (): Promise<{
updateAvailable: boolean;
version: string;
url: string;
releaseNotes?: string;
}> => ipcRenderer.invoke('check-for-updates'),

// App Info
getVersion: (): Promise<string> => ipcRenderer.invoke('get-version'),
};

contextBridge.exposeInMainWorld('electronAPI', electronAPI);
Expand Down
19 changes: 14 additions & 5 deletions electron/src/services/ipc/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { DatabaseManager } from '../database/database.js';
import { BackupService } from '../backup/index.js';
import { UpdateService } from '../update/update.service.js';
import { setupDatabaseHandlers } from './database-handlers.js';
import { setupThemeHandlers } from './theme-handlers.js';
import { setupLanguageHandlers } from './language-handlers.js';
import { setupBackupHandlers } from './backup-handlers.js';
import { setupUpdateHandlers } from './update-handlers.js';

import { setupSystemHandlers } from './system-handlers.js';

/**
* Sets up all IPC handlers
*/
export const setupIpcHandlers = (
dbManager: DatabaseManager,
backupService?: BackupService,
dbManager: DatabaseManager | null,
backupService: BackupService | null,
updateService: UpdateService,
): void => {
setupDatabaseHandlers(dbManager);
setupThemeHandlers(dbManager);
setupLanguageHandlers(dbManager);
if (dbManager) {
setupDatabaseHandlers(dbManager);
setupThemeHandlers(dbManager);
setupLanguageHandlers(dbManager);
}
if (backupService) {
setupBackupHandlers(backupService);
}
setupUpdateHandlers(updateService);
setupSystemHandlers();
};
50 changes: 50 additions & 0 deletions electron/src/services/ipc/system-handlers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { ipcMain, shell } from 'electron';
import { setupSystemHandlers } from './system-handlers.js';

vi.mock('electron', () => ({
ipcMain: {
handle: vi.fn(),
},
shell: {
openExternal: vi.fn(),
},
}));

describe('System Handlers', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should setup open-external handler', () => {
setupSystemHandlers();
expect(ipcMain.handle).toHaveBeenCalledWith(
'open-external',
expect.any(Function),
);
});

it('should handle open-external call', async () => {
setupSystemHandlers();
const handler = (ipcMain.handle as Mock).mock.calls.find(
(call) => call[0] === 'open-external',
)?.[1];

const url = 'https://example.com';
await handler(null, url);

expect(shell.openExternal).toHaveBeenCalledWith(url);
});

it('should handle errors in open-external', async () => {
setupSystemHandlers();
const handler = (ipcMain.handle as Mock).mock.calls.find(
(call) => call[0] === 'open-external',
)?.[1];

const error = new Error('Failed to open');
(shell.openExternal as Mock).mockRejectedValue(error);

await expect(handler(null, 'https://example.com')).rejects.toThrow(error);
});
});
16 changes: 16 additions & 0 deletions electron/src/services/ipc/system-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ipcMain, shell } from 'electron';

/**
* Sets up system-related IPC handlers
*/
export const setupSystemHandlers = (): void => {
// Open external links
ipcMain.handle('open-external', async (_event, url: string) => {
try {
await shell.openExternal(url);
} catch (error) {
console.error('Error opening external URL:', error);
throw error;
}
});
};
131 changes: 131 additions & 0 deletions electron/src/services/ipc/update-handlers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { ipcMain, app } from 'electron';
import { setupUpdateHandlers } from './update-handlers.js';
import { UpdateService } from '../update/update.service.js';

// Mock electron modules
vi.mock('electron', () => ({
ipcMain: {
handle: vi.fn(),
},
app: {
getVersion: vi.fn(),
},
}));

describe('Update Handlers', () => {
let mockUpdateService: {
checkForUpdates: Mock;
getReleaseByTag: Mock;
};

beforeEach(() => {
vi.clearAllMocks();

mockUpdateService = {
checkForUpdates: vi.fn(),
getReleaseByTag: vi.fn(),
};
});

describe('setupUpdateHandlers', () => {
it('should register all IPC handlers', () => {
setupUpdateHandlers(mockUpdateService as unknown as UpdateService);

expect(ipcMain.handle).toHaveBeenCalledTimes(3);
expect(ipcMain.handle).toHaveBeenCalledWith(
'check-for-updates',
expect.any(Function)
);
expect(ipcMain.handle).toHaveBeenCalledWith(
'get-version',
expect.any(Function)
);
expect(ipcMain.handle).toHaveBeenCalledWith(
'get-release-by-tag',
expect.any(Function)
);
});

it('should call updateService.checkForUpdates when check-for-updates is invoked', async () => {
const mockResult = { updateAvailable: true, version: '2.0.0', url: 'https://example.com' };
mockUpdateService.checkForUpdates.mockResolvedValue(mockResult);

setupUpdateHandlers(mockUpdateService as unknown as UpdateService);

const handlers = (ipcMain.handle as Mock).mock.calls;
const checkForUpdatesHandler = handlers.find(
(call) => call[0] === 'check-for-updates'
)?.[1];

const result = await checkForUpdatesHandler();

expect(mockUpdateService.checkForUpdates).toHaveBeenCalled();
expect(result).toEqual(mockResult);
});

it('should return app version when get-version is invoked', async () => {
(app.getVersion as Mock).mockReturnValue('1.5.0');

setupUpdateHandlers(mockUpdateService as unknown as UpdateService);

const handlers = (ipcMain.handle as Mock).mock.calls;
const getVersionHandler = handlers.find(
(call) => call[0] === 'get-version'
)?.[1];

const result = getVersionHandler();

expect(app.getVersion).toHaveBeenCalled();
expect(result).toBe('1.5.0');
});

it('should call updateService.getReleaseByTag when get-release-by-tag is invoked', async () => {
const mockRelease = { body: 'Release notes for v1.2.3' };
mockUpdateService.getReleaseByTag.mockResolvedValue(mockRelease);

setupUpdateHandlers(mockUpdateService as unknown as UpdateService);

const handlers = (ipcMain.handle as Mock).mock.calls;
const getReleaseByTagHandler = handlers.find(
(call) => call[0] === 'get-release-by-tag'
)?.[1];

const result = await getReleaseByTagHandler({}, 'v1.2.3');

expect(mockUpdateService.getReleaseByTag).toHaveBeenCalledWith('v1.2.3');
expect(result).toEqual(mockRelease);
});

it('should handle checkForUpdates when no update is available', async () => {
const mockResult = { updateAvailable: false, version: '1.0.0', url: '' };
mockUpdateService.checkForUpdates.mockResolvedValue(mockResult);

setupUpdateHandlers(mockUpdateService as unknown as UpdateService);

const handlers = (ipcMain.handle as Mock).mock.calls;
const checkForUpdatesHandler = handlers.find(
(call) => call[0] === 'check-for-updates'
)?.[1];

const result = await checkForUpdatesHandler();

expect(result.updateAvailable).toBe(false);
});

it('should handle getReleaseByTag returning null', async () => {
mockUpdateService.getReleaseByTag.mockResolvedValue(null);

setupUpdateHandlers(mockUpdateService as unknown as UpdateService);

const handlers = (ipcMain.handle as Mock).mock.calls;
const getReleaseByTagHandler = handlers.find(
(call) => call[0] === 'get-release-by-tag'
)?.[1];

const result = await getReleaseByTagHandler({}, 'v99.99.99');

expect(result).toBeNull();
});
});
});
8 changes: 8 additions & 0 deletions electron/src/services/ipc/update-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ipcMain, app } from 'electron';
import { UpdateService } from '../update/update.service.js';

export const setupUpdateHandlers = (updateService: UpdateService): void => {
ipcMain.handle('check-for-updates', () => updateService.checkForUpdates());
ipcMain.handle('get-version', () => app.getVersion());
ipcMain.handle('get-release-by-tag', (_event, tag: string) => updateService.getReleaseByTag(tag));
};
54 changes: 31 additions & 23 deletions electron/src/services/menu/menu-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ const menuTranslations: Record<string, Record<string, string>> = {
maintenance: 'Mantenimiento',
tags: 'Etiquetas',
dayTypes: 'Tipos de Día',
taskStatuses: 'Estados de Tarea',
taskStatuses: 'Tipos de Día',
checkForUpdates: 'Buscar Actualizaciones',
},
en: {
home: 'Home',
Expand Down Expand Up @@ -92,6 +93,7 @@ const menuTranslations: Record<string, Record<string, string>> = {
tags: 'Tags',
dayTypes: 'Day Types',
taskStatuses: 'Task Statuses',
checkForUpdates: 'Check for Updates',
},
};

Expand Down Expand Up @@ -121,21 +123,21 @@ export class MenuManager {
const template: MenuItemConstructorOptions[] = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: 'about' as const },
{ type: 'separator' as const },
{ role: 'services' as const },
{ type: 'separator' as const },
{ role: 'hide' as const },
{ role: 'hideOthers' as const },
{ role: 'unhide' as const },
{ type: 'separator' as const },
{ role: 'quit' as const },
],
},
]
{
label: app.name,
submenu: [
{ role: 'about' as const },
{ type: 'separator' as const },
{ role: 'services' as const },
{ type: 'separator' as const },
{ role: 'hide' as const },
{ role: 'hideOthers' as const },
{ role: 'unhide' as const },
{ type: 'separator' as const },
{ role: 'quit' as const },
],
},
]
: []),

{
Expand Down Expand Up @@ -172,13 +174,13 @@ export class MenuManager {
...(isMac
? []
: [
{ type: 'separator' as const },
{
label: this.t('exit'),
accelerator: 'Alt+F4',
role: 'quit' as const,
},
]),
{ type: 'separator' as const },
{
label: this.t('exit'),
accelerator: 'Alt+F4',
role: 'quit' as const,
},
]),
],
},

Expand Down Expand Up @@ -287,6 +289,12 @@ export class MenuManager {
{
label: this.t('help'),
submenu: [
{
label: this.t('checkForUpdates'),
click: (): void => {
this.navigateTo('/settings/updates');
},
},
{
label: this.t('documentation'),
click: (): void => {
Expand Down
Loading