@@ -76,23 +64,22 @@ jest.mock('../layout/Sidebar', () => ({
),
}));
-jest.mock('../ui/Toast', () => ({
- __esModule: true,
+mock.module('../ui/Toast', () => ({
default: () =>
Toast Container
,
}));
// Mock contexts
-jest.mock('../../contexts/ThemeContext', () => ({
+mock.module('../../contexts/ThemeContext', () => ({
ThemeProvider: ({ children }: { children: React.ReactNode }) =>
{children}
,
useTheme: () => ({
theme: 'light',
effectiveTheme: 'light',
- changeTheme: jest.fn(),
+ changeTheme: mock(() => {}),
isDark: false,
}),
}));
-jest.mock('../../contexts/PomodoroContext', () => ({
+mock.module('../../contexts/PomodoroContext', () => ({
PomodoroProvider: ({ children }: { children: React.ReactNode }) =>
{children}
,
usePomodoro: () => ({
isRunning: false,
@@ -101,17 +88,19 @@ jest.mock('../../contexts/PomodoroContext', () => ({
timeLeft: 1500,
currentInterval: 1,
totalIntervals: 4,
- start: jest.fn(),
- pause: jest.fn(),
- resume: jest.fn(),
- stop: jest.fn(),
- skip: jest.fn(),
+ start: mock(() => {}),
+ pause: mock(() => {}),
+ resume: mock(() => {}),
+ stop: mock(() => {}),
+ skip: mock(() => {}),
}),
}));
+import App from '../../App';
+
describe('App', () => {
beforeEach(() => {
- jest.clearAllMocks();
+ // Clear mocks before each test
});
describe('Initial Rendering', () => {
diff --git a/src/components/__tests__/DarkMode.integration.test.tsx b/src/components/__tests__/DarkMode.integration.test.tsx
index e23c606..eda1998 100644
--- a/src/components/__tests__/DarkMode.integration.test.tsx
+++ b/src/components/__tests__/DarkMode.integration.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import Dashboard from '../pages/Dashboard';
@@ -9,41 +10,41 @@ import { ThemeProvider } from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => true),
})),
});
// Mock i18next
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
// Mock useLanguage hook
-jest.mock('../../hooks/useLanguage', () => ({
+mock.module('../../hooks/useLanguage', () => ({
useLanguage: () => ({
language: 'en',
- changeLanguage: jest.fn(),
+ changeLanguage: mock(() => {}),
}),
}));
// Mock electronAPI
const mockElectronAPI = {
- getTodayStats: jest.fn(),
- getRecentTimeEntries: jest.fn(),
- getRecentAppUsage: jest.fn(),
- getStats: jest.fn(),
- getAllTimeEntries: jest.fn(),
- getAllAppUsage: jest.fn(),
+ getTodayStats: mock(() => Promise.resolve({})),
+ getRecentTimeEntries: mock(() => Promise.resolve([])),
+ getRecentAppUsage: mock(() => Promise.resolve([])),
+ getStats: mock(() => Promise.resolve({})),
+ getAllTimeEntries: mock(() => Promise.resolve([])),
+ getAllAppUsage: mock(() => Promise.resolve([])),
};
// Helper function to render with ThemeProvider
@@ -56,29 +57,28 @@ describe('Dark Mode Integration Tests', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).electronAPI = mockElectronAPI;
- // Setup default mocks
- mockElectronAPI.getTodayStats.mockResolvedValue({
+ // Setup default mocks - reassign mock implementations
+ mockElectronAPI.getTodayStats = mock(() => Promise.resolve({
totalTime: 3600000,
tasksCompleted: 5,
activeTask: 'Test Task'
- });
+ }));
- mockElectronAPI.getRecentTimeEntries.mockResolvedValue([]);
- mockElectronAPI.getRecentAppUsage.mockResolvedValue([]);
- mockElectronAPI.getStats.mockResolvedValue({
+ mockElectronAPI.getRecentTimeEntries = mock(() => Promise.resolve([]));
+ mockElectronAPI.getRecentAppUsage = mock(() => Promise.resolve([]));
+ mockElectronAPI.getStats = mock(() => Promise.resolve({
totalTrackedTime: 7200000,
completedTasks: 10,
averageTaskTime: 1800000,
totalAppTime: 5400000
- });
- mockElectronAPI.getAllTimeEntries.mockResolvedValue([]);
- mockElectronAPI.getAllAppUsage.mockResolvedValue([]);
+ }));
+ mockElectronAPI.getAllTimeEntries = mock(() => Promise.resolve([]));
+ mockElectronAPI.getAllAppUsage = mock(() => Promise.resolve([]));
});
afterEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).electronAPI;
- jest.clearAllMocks();
});
describe('Dashboard Dark Mode Classes', () => {
@@ -164,35 +164,35 @@ describe('Dark Mode Integration Tests', () => {
describe('Sidebar Dark Mode Classes', () => {
it('should have dark mode background', () => {
- const { container } = renderWithTheme(
);
+ const { container } = renderWithTheme(
{})} />);
const darkBgElement = container.querySelector('.dark\\:bg-gray-800');
expect(darkBgElement).toBeInTheDocument();
});
it('should have dark mode border', () => {
- const { container } = renderWithTheme();
+ const { container } = renderWithTheme( {})} />);
const darkBorderElement = container.querySelector('.dark\\:border-gray-700');
expect(darkBorderElement).toBeInTheDocument();
});
it('should have dark mode text for app name', () => {
- const { container } = renderWithTheme();
+ const { container } = renderWithTheme( {})} />);
const darkTextElement = container.querySelector('.dark\\:text-primary-400');
expect(darkTextElement).toBeInTheDocument();
});
it('should have dark mode hover states for menu items', () => {
- const { container } = renderWithTheme();
+ const { container } = renderWithTheme( {})} />);
const darkHoverElements = container.querySelectorAll('.dark\\:hover\\:bg-gray-700');
expect(darkHoverElements.length).toBeGreaterThan(0);
});
it('should have dark mode active menu item styling', () => {
- const { container } = renderWithTheme();
+ const { container } = renderWithTheme( {})} />);
const darkActiveElements = container.querySelectorAll('.dark\\:bg-primary-900\\/30');
expect(darkActiveElements.length).toBeGreaterThan(0);
@@ -242,19 +242,19 @@ describe('Dark Mode Integration Tests', () => {
describe('Loading States Dark Mode', () => {
it('should have dark mode loading text in Dashboard', async () => {
- mockElectronAPI.getTodayStats.mockImplementation(() => new Promise(() => {}));
-
+ mockElectronAPI.getTodayStats = mock(() => new Promise(() => {}));
+
const { container } = renderWithTheme();
-
+
const loadingElement = container.querySelector('.dark\\:text-gray-400');
expect(loadingElement).toBeInTheDocument();
});
it('should have dark mode loading spinner in Reports', async () => {
- mockElectronAPI.getStats.mockImplementation(() => new Promise(() => {}));
-
+ mockElectronAPI.getStats = mock(() => new Promise(() => {}));
+
const { container } = renderWithTheme();
-
+
const loadingElement = container.querySelector('.dark\\:text-gray-400');
expect(loadingElement).toBeInTheDocument();
});
diff --git a/src/components/__tests__/Dashboard.i18n.test.tsx b/src/components/__tests__/Dashboard.i18n.test.tsx
index 66aa690..2a71012 100644
--- a/src/components/__tests__/Dashboard.i18n.test.tsx
+++ b/src/components/__tests__/Dashboard.i18n.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { render, screen, waitFor } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from '../../i18n/config';
@@ -8,23 +9,23 @@ import { ThemeProvider } from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => true),
})),
});
// Mock window.electron API
const mockElectronAPI = {
- getTimeEntries: jest.fn(),
- getAppUsage: jest.fn(),
- getStats: jest.fn(),
+ getTimeEntries: mock(() => Promise.resolve([])),
+ getAppUsage: mock(() => Promise.resolve([])),
+ getStats: mock(() => Promise.resolve({})),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -44,14 +45,13 @@ const renderWithI18n = (component: React.ReactElement, language = 'en') => {
describe('Dashboard i18n Integration', () => {
beforeEach(() => {
- jest.clearAllMocks();
- mockElectronAPI.getTimeEntries.mockResolvedValue([]);
- mockElectronAPI.getAppUsage.mockResolvedValue([]);
- mockElectronAPI.getStats.mockResolvedValue({
+ mockElectronAPI.getTimeEntries = mock(() => Promise.resolve([]));
+ mockElectronAPI.getAppUsage = mock(() => Promise.resolve([]));
+ mockElectronAPI.getStats = mock(() => Promise.resolve({
totalTime: 0,
tasksCompleted: 0,
activeTask: null,
- });
+ }));
});
describe('English translations', () => {
@@ -78,11 +78,11 @@ describe('Dashboard i18n Integration', () => {
});
it('should render stat cards in English', async () => {
- mockElectronAPI.getStats.mockResolvedValue({
+ mockElectronAPI.getStats = mock(() => Promise.resolve({
totalTime: 3600,
tasksCompleted: 5,
activeTask: 'Test Task',
- });
+ }));
renderWithI18n();
@@ -94,11 +94,11 @@ describe('Dashboard i18n Integration', () => {
});
it('should render "No active task" in English', async () => {
- mockElectronAPI.getStats.mockResolvedValue({
+ mockElectronAPI.getStats = mock(() => Promise.resolve({
totalTime: 3600,
tasksCompleted: 5,
activeTask: null,
- });
+ }));
renderWithI18n();
@@ -150,11 +150,11 @@ describe('Dashboard i18n Integration', () => {
});
it('should render stat cards in Arabic', async () => {
- mockElectronAPI.getStats.mockResolvedValue({
+ mockElectronAPI.getStats = mock(() => Promise.resolve({
totalTime: 3600,
tasksCompleted: 5,
activeTask: 'Test Task',
- });
+ }));
renderWithI18n(, 'ar');
@@ -166,11 +166,11 @@ describe('Dashboard i18n Integration', () => {
});
it('should render "No active task" in Arabic', async () => {
- mockElectronAPI.getStats.mockResolvedValue({
+ mockElectronAPI.getStats = mock(() => Promise.resolve({
totalTime: 3600,
tasksCompleted: 5,
activeTask: null,
- });
+ }));
renderWithI18n(, 'ar');
@@ -234,7 +234,7 @@ describe('Dashboard i18n Integration', () => {
describe('Data rendering with translations', () => {
it('should render time entries with "Active" status in English', async () => {
- mockElectronAPI.getTimeEntries.mockResolvedValue([
+ mockElectronAPI.getTimeEntries = mock(() => Promise.resolve([
{
id: 1,
taskName: 'Task 1',
@@ -243,7 +243,7 @@ describe('Dashboard i18n Integration', () => {
endTime: null,
duration: null,
},
- ]);
+ ]));
renderWithI18n();
@@ -253,7 +253,7 @@ describe('Dashboard i18n Integration', () => {
});
it('should render time entries with "Active" status in Arabic', async () => {
- mockElectronAPI.getTimeEntries.mockResolvedValue([
+ mockElectronAPI.getTimeEntries = mock(() => Promise.resolve([
{
id: 1,
taskName: 'Task 1',
@@ -262,7 +262,7 @@ describe('Dashboard i18n Integration', () => {
endTime: null,
duration: null,
},
- ]);
+ ]));
renderWithI18n(, 'ar');
@@ -272,13 +272,13 @@ describe('Dashboard i18n Integration', () => {
});
it('should render app usage with "Active" status in English', async () => {
- mockElectronAPI.getAppUsage.mockResolvedValue([
+ mockElectronAPI.getAppUsage = mock(() => Promise.resolve([
{
id: 1,
appName: 'Chrome',
duration: null,
},
- ]);
+ ]));
renderWithI18n();
@@ -288,13 +288,13 @@ describe('Dashboard i18n Integration', () => {
});
it('should render app usage with "Active" status in Arabic', async () => {
- mockElectronAPI.getAppUsage.mockResolvedValue([
+ mockElectronAPI.getAppUsage = mock(() => Promise.resolve([
{
id: 1,
appName: 'Chrome',
duration: null,
},
- ]);
+ ]));
renderWithI18n(, 'ar');
diff --git a/src/components/__tests__/ErrorBoundary.i18n.test.tsx b/src/components/__tests__/ErrorBoundary.i18n.test.tsx
index cb8eef0..197dc9b 100644
--- a/src/components/__tests__/ErrorBoundary.i18n.test.tsx
+++ b/src/components/__tests__/ErrorBoundary.i18n.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, mock, spyOn } from 'bun:test';
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
@@ -8,15 +9,15 @@ import { ThemeProvider } from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => {}),
})),
});
@@ -40,7 +41,7 @@ describe('ErrorBoundary i18n Integration', () => {
// Suppress console.error for these tests
const originalError = console.error;
beforeAll(() => {
- console.error = jest.fn();
+ console.error = mock(() => {});
});
afterAll(() => {
@@ -48,7 +49,7 @@ describe('ErrorBoundary i18n Integration', () => {
});
beforeEach(() => {
- jest.clearAllMocks();
+ // Bun handles mock cleanup differently
});
describe('English translations', () => {
@@ -168,7 +169,7 @@ describe('ErrorBoundary i18n Integration', () => {
describe('Custom fallback', () => {
it('should render custom fallback instead of default error UI', () => {
const customFallback = Custom Error Page
;
-
+
renderWithI18n(
@@ -233,10 +234,10 @@ describe('ErrorBoundary i18n Integration', () => {
describe('Edge cases', () => {
it('should handle refresh page button click', () => {
- const reloadSpy = jest.fn();
+ const reloadFn = mock(() => {});
Object.defineProperty(window, 'location', {
writable: true,
- value: { reload: reloadSpy },
+ value: { reload: reloadFn },
});
renderWithI18n(
@@ -248,12 +249,12 @@ describe('ErrorBoundary i18n Integration', () => {
const refreshButton = screen.getByText('Refresh Page');
fireEvent.click(refreshButton);
- expect(reloadSpy).toHaveBeenCalled();
+ expect(reloadFn).toHaveBeenCalled();
});
it('should handle errors during translation rendering', () => {
// This tests robustness even if i18n fails
-
+
renderWithI18n(
@@ -264,4 +265,4 @@ describe('ErrorBoundary i18n Integration', () => {
expect(screen.getByRole('button')).toBeInTheDocument();
});
});
-});
\ No newline at end of file
+});
diff --git a/src/components/__tests__/FocusMode.test.tsx b/src/components/__tests__/FocusMode.test.tsx
index f91b9d2..8b13ef1 100644
--- a/src/components/__tests__/FocusMode.test.tsx
+++ b/src/components/__tests__/FocusMode.test.tsx
@@ -1,41 +1,47 @@
+import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, mock, spyOn } from 'bun:test';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
-import FocusMode from '../pages/FocusMode';
-import * as PomodoroContext from '@/contexts/PomodoroContext';
-import { PomodoroTimerStatus, SessionType, TimerState } from '@/contexts/PomodoroContext';
-import { PomodoroSettings } from '@/types';
import { ThemeProvider } from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => {}),
})),
});
-// Mock i18next
-jest.mock('react-i18next', () => ({
+// Mock i18next - must come before component import
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
-// Mock useLanguage hook
-jest.mock('../../hooks/useLanguage', () => ({
+// Create mock function for changeLanguage
+const mockChangeLanguage = mock(() => {});
+
+// Mock useLanguage hook - must come before component import
+mock.module('../../hooks/useLanguage', () => ({
useLanguage: () => ({
language: 'en',
- changeLanguage: jest.fn(),
+ changeLanguage: mockChangeLanguage,
}),
}));
+// Import after mocks
+import FocusMode from '../pages/FocusMode';
+import * as PomodoroContext from '@/contexts/PomodoroContext';
+import { PomodoroTimerStatus, SessionType, TimerState } from '@/contexts/PomodoroContext';
+import { PomodoroSettings } from '@/types';
+
// Mock PomodoroContext
const mockPomodoroContext = {
status: {
@@ -57,20 +63,27 @@ const mockPomodoroContext = {
notificationsEnabled: true,
dailyGoal: 8,
} as PomodoroSettings,
- startSession: jest.fn(),
- pauseSession: jest.fn(),
- resumeSession: jest.fn(),
- stopSession: jest.fn(),
- skipSession: jest.fn(),
- loadSettings: jest.fn(),
- saveSettings: jest.fn(),
- refreshStatus: jest.fn(),
+ startSession: mock(() => {}),
+ pauseSession: mock(() => {}),
+ resumeSession: mock(() => {}),
+ stopSession: mock(() => {}),
+ skipSession: mock(() => {}),
+ loadSettings: mock(() => {}),
+ saveSettings: mock(() => {}),
+ refreshStatus: mock(() => {}),
};
const mockElectronAPI = {
pomodoro: {
sessions: {
- getStats: jest.fn(),
+ getStats: mock(() => Promise.resolve({
+ totalSessions: 5,
+ completedSessions: 4,
+ totalFocusTime: 6000,
+ totalBreakTime: 1200,
+ completionRate: 80,
+ currentStreak: 2,
+ })),
},
},
};
@@ -82,18 +95,26 @@ const renderWithTheme = (component: React.ReactElement) => {
describe('FocusMode Component', () => {
beforeEach(() => {
- jest.clearAllMocks();
-
- jest.spyOn(PomodoroContext, 'usePomodoro').mockReturnValue(mockPomodoroContext);
-
- mockElectronAPI.pomodoro.sessions.getStats.mockResolvedValue({
+ // Reset mock functions
+ mockPomodoroContext.startSession = mock(() => {});
+ mockPomodoroContext.pauseSession = mock(() => {});
+ mockPomodoroContext.resumeSession = mock(() => {});
+ mockPomodoroContext.stopSession = mock(() => {});
+ mockPomodoroContext.skipSession = mock(() => {});
+ mockPomodoroContext.loadSettings = mock(() => {});
+ mockPomodoroContext.saveSettings = mock(() => {});
+ mockPomodoroContext.refreshStatus = mock(() => {});
+
+ spyOn(PomodoroContext, 'usePomodoro').mockReturnValue(mockPomodoroContext);
+
+ mockElectronAPI.pomodoro.sessions.getStats = mock(() => Promise.resolve({
totalSessions: 5,
completedSessions: 4,
totalFocusTime: 6000,
totalBreakTime: 1200,
completionRate: 80,
currentStreak: 2,
- });
+ }));
(window as any).electronAPI = mockElectronAPI;
@@ -114,7 +135,7 @@ describe('FocusMode Component', () => {
describe('Component Rendering', () => {
it('should render loading state when settings is null', () => {
- jest.spyOn(PomodoroContext, 'usePomodoro').mockReturnValue({
+ spyOn(PomodoroContext, 'usePomodoro').mockReturnValue({
...mockPomodoroContext,
settings: null,
});
@@ -429,27 +450,27 @@ describe('FocusMode Component', () => {
});
it('should display zero stats when no data', async () => {
- mockElectronAPI.pomodoro.sessions.getStats.mockResolvedValue({
+ mockElectronAPI.pomodoro.sessions.getStats = mock(() => Promise.resolve({
totalSessions: 0,
completedSessions: 0,
totalFocusTime: 0,
totalBreakTime: 0,
completionRate: 0,
currentStreak: 0,
- });
+ }));
renderWithTheme();
});
it('should display extremely large stats correctly', async () => {
- mockElectronAPI.pomodoro.sessions.getStats.mockResolvedValue({
+ mockElectronAPI.pomodoro.sessions.getStats = mock(() => Promise.resolve({
totalSessions: 999999999,
completedSessions: 888888888,
totalFocusTime: 999999999, // in minutes
totalBreakTime: 888888888, // in minutes
completionRate: 99.9999,
currentStreak: 123456789,
- });
+ }));
renderWithTheme();
@@ -535,31 +556,31 @@ describe('FocusMode Component', () => {
describe('Error Handling', () => {
it('should handle missing electronAPI gracefully', async () => {
delete (window as any).electronAPI;
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
renderWithTheme();
await waitFor(() => {
- expect(consoleError).toHaveBeenCalled();
+ expect(consoleErrorSpy).toHaveBeenCalled();
});
- consoleError.mockRestore();
+ consoleErrorSpy.mockRestore();
});
it('should handle getPomodoroStats error', async () => {
- mockElectronAPI.pomodoro.sessions.getStats.mockRejectedValue(new Error('Stats failed'));
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockElectronAPI.pomodoro.sessions.getStats = mock(() => Promise.reject(new Error('Stats failed')));
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
renderWithTheme();
await waitFor(() => {
- expect(consoleError).toHaveBeenCalledWith(
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to load pomodoro stats:',
expect.any(Error)
);
});
- consoleError.mockRestore();
+ consoleErrorSpy.mockRestore();
});
});
diff --git a/src/components/__tests__/Settings.i18n.test.tsx b/src/components/__tests__/Settings.i18n.test.tsx
index 6d819b7..d708723 100644
--- a/src/components/__tests__/Settings.i18n.test.tsx
+++ b/src/components/__tests__/Settings.i18n.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, mock, spyOn } from 'bun:test';
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
@@ -8,24 +9,33 @@ import { ThemeProvider } from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => {}),
})),
});
// Mock window.electron API
const mockElectronAPI = {
- getSettings: jest.fn(),
- saveSettings: jest.fn(),
- exportData: jest.fn(),
- clearData: jest.fn(),
+ getSettings: mock(() => Promise.resolve({
+ autoTrackApps: true,
+ showNotifications: true,
+ minimizeToTray: false,
+ autoStartTracking: false,
+ defaultCategory: '',
+ trackingInterval: 60,
+ crashReporting: true,
+ dataLocation: '/path/to/data',
+ })),
+ saveSettings: mock(() => Promise.resolve(true)),
+ exportData: mock(() => {}),
+ clearData: mock(() => {}),
};
(global as any).window = {
@@ -44,8 +54,7 @@ const renderWithI18n = (component: React.ReactElement, language = 'en') => {
describe('Settings i18n Integration', () => {
beforeEach(() => {
- jest.clearAllMocks();
- mockElectronAPI.getSettings.mockResolvedValue({
+ mockElectronAPI.getSettings = mock(() => Promise.resolve({
autoTrackApps: true,
showNotifications: true,
minimizeToTray: false,
@@ -54,14 +63,14 @@ describe('Settings i18n Integration', () => {
trackingInterval: 60,
crashReporting: true,
dataLocation: '/path/to/data',
- });
- mockElectronAPI.saveSettings.mockResolvedValue(true);
+ }));
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(true));
});
describe('Language selector', () => {
it('should render language selector in English', async () => {
renderWithI18n();
-
+
await waitFor(() => {
expect(screen.getByText('Language')).toBeInTheDocument();
expect(screen.getByText('Select Language')).toBeInTheDocument();
@@ -70,7 +79,7 @@ describe('Settings i18n Integration', () => {
it('should render language selector in Arabic', async () => {
renderWithI18n(, 'ar');
-
+
await waitFor(() => {
expect(screen.getByText('ุงููุบุฉ')).toBeInTheDocument();
expect(screen.getByText('ุงุฎุชุฑ ุงููุบุฉ')).toBeInTheDocument();
@@ -79,7 +88,7 @@ describe('Settings i18n Integration', () => {
it('should display language options in English', async () => {
renderWithI18n();
-
+
await waitFor(() => {
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
@@ -92,7 +101,7 @@ describe('Settings i18n Integration', () => {
it('should display language options in Arabic', async () => {
renderWithI18n(, 'ar');
-
+
await waitFor(() => {
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
@@ -106,7 +115,7 @@ describe('Settings i18n Integration', () => {
it('should have correct selected value for English', async () => {
await i18n.changeLanguage('en');
renderWithI18n();
-
+
await waitFor(() => {
const select = screen.getByRole('combobox') as HTMLSelectElement;
expect(select.value).toBe('en');
@@ -116,7 +125,7 @@ describe('Settings i18n Integration', () => {
it('should have correct selected value for Arabic', async () => {
await i18n.changeLanguage('ar');
renderWithI18n(, 'ar');
-
+
await waitFor(() => {
const select = screen.getByRole('combobox') as HTMLSelectElement;
expect(select.value).toBe('ar');
@@ -127,7 +136,7 @@ describe('Settings i18n Integration', () => {
describe('Language switching via UI', () => {
it('should change language when selecting Arabic', async () => {
renderWithI18n(, 'en');
-
+
await waitFor(() => {
expect(screen.getByText('General Settings')).toBeInTheDocument();
});
@@ -142,7 +151,7 @@ describe('Settings i18n Integration', () => {
it('should change language when selecting English', async () => {
renderWithI18n(, 'ar');
-
+
await waitFor(() => {
expect(screen.getByText('ุงูุฅุนุฏุงุฏุงุช ุงูุนุงู
ุฉ')).toBeInTheDocument();
});
@@ -157,7 +166,7 @@ describe('Settings i18n Integration', () => {
it('should update UI when language changes through selector', async () => {
const { rerender } = renderWithI18n(, 'en');
-
+
await waitFor(() => {
expect(screen.getByText('General Settings')).toBeInTheDocument();
});
@@ -181,7 +190,7 @@ describe('Settings i18n Integration', () => {
describe('Settings sections in different languages', () => {
it('should render all section headers in English', async () => {
renderWithI18n();
-
+
await waitFor(() => {
expect(screen.getByText('General Settings')).toBeInTheDocument();
});
@@ -189,7 +198,7 @@ describe('Settings i18n Integration', () => {
it('should render all section headers in Arabic', async () => {
renderWithI18n(, 'ar');
-
+
await waitFor(() => {
expect(screen.getByText('ุงูุฅุนุฏุงุฏุงุช ุงูุนุงู
ุฉ')).toBeInTheDocument();
});
@@ -199,7 +208,7 @@ describe('Settings i18n Integration', () => {
describe('useLanguage hook integration', () => {
it('should use useLanguage hook for language management', async () => {
renderWithI18n();
-
+
await waitFor(() => {
const select = screen.getByRole('combobox') as HTMLSelectElement;
expect(select).toBeInTheDocument();
@@ -211,10 +220,10 @@ describe('Settings i18n Integration', () => {
});
it('should call changeLanguage from useLanguage hook', async () => {
- const languageChangeSpy = jest.spyOn(i18n, 'changeLanguage');
-
+ const languageChangeSpy = spyOn(i18n, 'changeLanguage');
+
renderWithI18n();
-
+
await waitFor(() => {
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
@@ -234,7 +243,7 @@ describe('Settings i18n Integration', () => {
describe('RTL support through settings', () => {
it('should reflect RTL when Arabic is selected', async () => {
renderWithI18n(, 'en');
-
+
await waitFor(() => {
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
@@ -250,7 +259,7 @@ describe('Settings i18n Integration', () => {
it('should reflect LTR when English is selected', async () => {
renderWithI18n(, 'ar');
-
+
await waitFor(() => {
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
@@ -268,7 +277,7 @@ describe('Settings i18n Integration', () => {
describe('Persistence across re-renders', () => {
it('should maintain selected language after re-render', async () => {
const { rerender } = renderWithI18n(, 'ar');
-
+
await waitFor(() => {
const select = screen.getByRole('combobox') as HTMLSelectElement;
expect(select.value).toBe('ar');
@@ -286,14 +295,14 @@ describe('Settings i18n Integration', () => {
describe('Edge cases', () => {
it('should handle rapid language changes', async () => {
renderWithI18n();
-
+
await waitFor(() => {
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
});
const select = screen.getByRole('combobox') as HTMLSelectElement;
-
+
fireEvent.change(select, { target: { value: 'ar' } });
fireEvent.change(select, { target: { value: 'en' } });
fireEvent.change(select, { target: { value: 'ar' } });
@@ -304,14 +313,14 @@ describe('Settings i18n Integration', () => {
});
it('should not break when settings fail to load', async () => {
- mockElectronAPI.getSettings.mockRejectedValue(new Error('Failed to load'));
-
+ mockElectronAPI.getSettings = mock(() => Promise.reject(new Error('Failed to load')));
+
renderWithI18n();
-
+
await waitFor(() => {
// Should still render language selector
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
});
});
-});
\ No newline at end of file
+});
diff --git a/src/components/__tests__/Settings.test.tsx b/src/components/__tests__/Settings.test.tsx
index e118bcd..acd9f53 100644
--- a/src/components/__tests__/Settings.test.tsx
+++ b/src/components/__tests__/Settings.test.tsx
@@ -1,46 +1,69 @@
+import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, mock, spyOn } from 'bun:test';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
-import Settings from '../pages/Settings';
import { ThemeProvider } from '@/contexts/ThemeContext';
import * as ThemeContext from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => {}),
})),
});
-// Mock i18next
-jest.mock('react-i18next', () => ({
+// Mock i18next - must come before component import
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
-// Mock useLanguage hook
-jest.mock('../../hooks/useLanguage', () => ({
+// Create mock function for changeLanguage
+const mockChangeLanguage = mock(() => {});
+
+// Mock useLanguage hook - must come before component import
+mock.module('../../hooks/useLanguage', () => ({
useLanguage: () => ({
language: 'en',
- changeLanguage: jest.fn(),
+ changeLanguage: mockChangeLanguage,
}),
}));
+// Import after mocks
+import Settings from '../pages/Settings';
+
// Mock window.electronAPI
const mockElectronAPI = {
- getSettings: jest.fn(),
- saveSettings: jest.fn(),
- getActivityTrackingStatus: jest.fn(),
- startActivityTracking: jest.fn(),
- stopActivityTracking: jest.fn(),
+ getSettings: mock(() => Promise.resolve({
+ autoTrackApps: true,
+ showNotifications: true,
+ minimizeToTray: false,
+ autoStartTracking: false,
+ defaultCategory: '',
+ trackingInterval: 30,
+ activityTracking: {
+ enabled: false,
+ trackingInterval: 30,
+ idleThreshold: 300,
+ trackBrowsers: true,
+ trackApplications: true,
+ blacklistedApps: [],
+ blacklistedDomains: [],
+ dataRetentionDays: 90
+ }
+ })),
+ saveSettings: mock(() => Promise.resolve(true)),
+ getActivityTrackingStatus: mock(() => Promise.resolve(false)),
+ startActivityTracking: mock(() => Promise.resolve()),
+ stopActivityTracking: mock(() => Promise.resolve()),
};
// Helper function to render with ThemeProvider
@@ -50,11 +73,8 @@ const renderWithTheme = (component: React.ReactElement) => {
describe('Settings Component', () => {
beforeEach(() => {
- // Reset mocks
- jest.clearAllMocks();
-
- // Setup default mock implementations
- mockElectronAPI.getSettings.mockResolvedValue({
+ // Reset mock functions
+ mockElectronAPI.getSettings = mock(() => Promise.resolve({
autoTrackApps: true,
showNotifications: true,
minimizeToTray: false,
@@ -71,11 +91,13 @@ describe('Settings Component', () => {
blacklistedDomains: [],
dataRetentionDays: 90
}
- });
-
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
- mockElectronAPI.saveSettings.mockResolvedValue(true);
-
+ }));
+
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(true));
+ mockElectronAPI.startActivityTracking = mock(() => Promise.resolve());
+ mockElectronAPI.stopActivityTracking = mock(() => Promise.resolve());
+
(window as any).electronAPI = mockElectronAPI;
});
@@ -119,7 +141,7 @@ describe('Settings Component', () => {
describe('Activity Tracking Toggle', () => {
it('should start tracking when toggle is clicked from stopped state', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
renderWithTheme();
@@ -137,7 +159,7 @@ describe('Settings Component', () => {
});
it('should stop tracking when toggle is clicked from active state', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(true);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(true));
renderWithTheme();
@@ -155,7 +177,7 @@ describe('Settings Component', () => {
});
it('should save settings with correct enabled state when starting tracking', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
renderWithTheme();
@@ -178,7 +200,7 @@ describe('Settings Component', () => {
});
it('should save settings with correct enabled state when stopping tracking', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(true);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(true));
renderWithTheme();
@@ -202,7 +224,7 @@ describe('Settings Component', () => {
it('should handle error when electronAPI is not available', async () => {
delete (window as any).electronAPI;
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
renderWithTheme();
@@ -215,19 +237,19 @@ describe('Settings Component', () => {
fireEvent.click(startButton);
await waitFor(() => {
- expect(consoleError).toHaveBeenCalledWith(
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to toggle activity tracking'),
expect.any(Error)
);
});
- consoleError.mockRestore();
+ consoleErrorSpy.mockRestore();
});
it('should log error when save settings fails', async () => {
- mockElectronAPI.saveSettings.mockResolvedValue(false);
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(false));
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
renderWithTheme();
@@ -239,16 +261,16 @@ describe('Settings Component', () => {
fireEvent.click(startButton);
await waitFor(() => {
- expect(consoleError).toHaveBeenCalledWith('โ Failed to save tracking state to settings');
+ expect(consoleErrorSpy).toHaveBeenCalledWith('โ Failed to save tracking state to settings');
});
- consoleError.mockRestore();
+ consoleErrorSpy.mockRestore();
});
it('should log success message when tracking is enabled and saved', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
- mockElectronAPI.saveSettings.mockResolvedValue(true);
- const consoleLog = jest.spyOn(console, 'log').mockImplementation();
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(true));
+ const consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {});
renderWithTheme();
@@ -260,10 +282,10 @@ describe('Settings Component', () => {
fireEvent.click(startButton);
await waitFor(() => {
- expect(consoleLog).toHaveBeenCalledWith('โ
Activity tracking enabled and saved to settings');
+ expect(consoleLogSpy).toHaveBeenCalledWith('โ
Activity tracking enabled and saved to settings');
});
- consoleLog.mockRestore();
+ consoleLogSpy.mockRestore();
});
});
@@ -284,7 +306,7 @@ describe('Settings Component', () => {
});
it('should show success message after saving settings', async () => {
- mockElectronAPI.saveSettings.mockResolvedValue(true);
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(true));
renderWithTheme();
@@ -301,8 +323,9 @@ describe('Settings Component', () => {
});
it('should clear success message after 3 seconds', async () => {
- jest.useFakeTimers();
- mockElectronAPI.saveSettings.mockResolvedValue(true);
+ // Note: Bun doesn't have full timer mocking like Jest
+ // This test may need adjustment or manual verification
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(true));
renderWithTheme();
@@ -317,17 +340,16 @@ describe('Settings Component', () => {
expect(screen.getByText('settings.settingsSavedSuccess')).toBeInTheDocument();
});
- jest.advanceTimersByTime(3000);
+ // Wait for the timeout (3 seconds) - this is a real wait in Bun
+ await new Promise(resolve => setTimeout(resolve, 3100));
await waitFor(() => {
expect(screen.queryByText('settings.settingsSavedSuccess')).not.toBeInTheDocument();
});
-
- jest.useRealTimers();
});
it('should show error message when save fails', async () => {
- mockElectronAPI.saveSettings.mockResolvedValue(false);
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(false));
renderWithTheme();
@@ -344,7 +366,7 @@ describe('Settings Component', () => {
});
it('should disable save button while saving', async () => {
- mockElectronAPI.saveSettings.mockImplementation(() =>
+ mockElectronAPI.saveSettings = mock(() =>
new Promise(resolve => setTimeout(() => resolve(true), 100))
);
@@ -363,7 +385,7 @@ describe('Settings Component', () => {
});
it('should only trigger saveSettings once on rapid consecutive clicks', async () => {
- mockElectronAPI.saveSettings.mockImplementation(() =>
+ mockElectronAPI.saveSettings = mock(() =>
new Promise(resolve => setTimeout(() => resolve(true), 100))
);
@@ -392,7 +414,7 @@ describe('Settings Component', () => {
describe('Activity Tracking Status Display', () => {
it('should display "Active" status when tracking is enabled', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(true);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(true));
renderWithTheme();
@@ -402,7 +424,7 @@ describe('Settings Component', () => {
});
it('should display "Stopped" status when tracking is disabled', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
renderWithTheme();
@@ -412,7 +434,7 @@ describe('Settings Component', () => {
});
it('should show correct button text based on tracking state', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
renderWithTheme();
@@ -424,35 +446,35 @@ describe('Settings Component', () => {
describe('Error Handling', () => {
it('should handle error when loading settings fails', async () => {
- mockElectronAPI.getSettings.mockRejectedValue(new Error('Failed to load'));
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockElectronAPI.getSettings = mock(() => Promise.reject(new Error('Failed to load')));
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
renderWithTheme();
await waitFor(() => {
- expect(consoleError).toHaveBeenCalledWith(
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to load settings:',
expect.any(Error)
);
});
- consoleError.mockRestore();
+ consoleErrorSpy.mockRestore();
});
it('should handle error when loading tracking status fails', async () => {
- mockElectronAPI.getActivityTrackingStatus.mockRejectedValue(new Error('Failed to load status'));
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.reject(new Error('Failed to load status')));
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
renderWithTheme();
await waitFor(() => {
- expect(consoleError).toHaveBeenCalledWith(
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to load tracking status:',
expect.any(Error)
);
});
- consoleError.mockRestore();
+ consoleErrorSpy.mockRestore();
});
it('should handle missing electronAPI gracefully', async () => {
@@ -472,11 +494,8 @@ describe('Settings Component', () => {
// Additional tests for theme functionality
describe('Theme Selection', () => {
beforeEach(() => {
- // Reset mocks
- jest.clearAllMocks();
-
- // Setup default mock implementations
- mockElectronAPI.getSettings.mockResolvedValue({
+ // Reset mock functions
+ mockElectronAPI.getSettings = mock(() => Promise.resolve({
autoTrackApps: true,
showNotifications: true,
minimizeToTray: false,
@@ -493,17 +512,16 @@ describe('Theme Selection', () => {
blacklistedDomains: [],
dataRetentionDays: 90
}
- });
-
- mockElectronAPI.getActivityTrackingStatus.mockResolvedValue(false);
- mockElectronAPI.saveSettings.mockResolvedValue(true);
-
+ }));
+
+ mockElectronAPI.getActivityTrackingStatus = mock(() => Promise.resolve(false));
+ mockElectronAPI.saveSettings = mock(() => Promise.resolve(true));
+
(window as any).electronAPI = mockElectronAPI;
});
afterEach(() => {
delete (window as any).electronAPI;
- jest.restoreAllMocks();
});
it('should render theme dropdown with all options', async () => {
@@ -549,8 +567,8 @@ describe('Theme Selection', () => {
});
it('should call useTheme hook on mount', async () => {
- const mockChangeTheme = jest.fn();
- jest.spyOn(ThemeContext, 'useTheme').mockReturnValue({
+ const mockChangeTheme = mock(() => {});
+ spyOn(ThemeContext, 'useTheme').mockReturnValue({
theme: 'system',
effectiveTheme: 'light',
changeTheme: mockChangeTheme,
@@ -568,8 +586,8 @@ describe('Theme Selection', () => {
});
it('should change theme when user selects a different option', async () => {
- const mockChangeTheme = jest.fn();
- jest.spyOn(ThemeContext, 'useTheme').mockReturnValue({
+ const mockChangeTheme = mock(() => {});
+ spyOn(ThemeContext, 'useTheme').mockReturnValue({
theme: 'light',
effectiveTheme: 'light',
changeTheme: mockChangeTheme,
@@ -597,8 +615,8 @@ describe('Theme Selection', () => {
});
it('should handle theme change from light to dark', async () => {
- const mockChangeTheme = jest.fn();
- jest.spyOn(ThemeContext, 'useTheme').mockReturnValue({
+ const mockChangeTheme = mock(() => {});
+ spyOn(ThemeContext, 'useTheme').mockReturnValue({
theme: 'light',
effectiveTheme: 'light',
changeTheme: mockChangeTheme,
@@ -623,8 +641,8 @@ describe('Theme Selection', () => {
});
it('should handle theme change to system mode', async () => {
- const mockChangeTheme = jest.fn();
- jest.spyOn(ThemeContext, 'useTheme').mockReturnValue({
+ const mockChangeTheme = mock(() => {});
+ spyOn(ThemeContext, 'useTheme').mockReturnValue({
theme: 'light',
effectiveTheme: 'light',
changeTheme: mockChangeTheme,
@@ -649,10 +667,10 @@ describe('Theme Selection', () => {
});
it('should display current theme value in select', async () => {
- jest.spyOn(ThemeContext, 'useTheme').mockReturnValue({
+ spyOn(ThemeContext, 'useTheme').mockReturnValue({
theme: 'dark',
effectiveTheme: 'dark',
- changeTheme: jest.fn(),
+ changeTheme: mock(() => {}),
isDark: true,
});
@@ -671,8 +689,8 @@ describe('Theme Selection', () => {
});
it('should integrate theme selection with other settings', async () => {
- const mockChangeTheme = jest.fn();
- jest.spyOn(ThemeContext, 'useTheme').mockReturnValue({
+ const mockChangeTheme = mock(() => {});
+ spyOn(ThemeContext, 'useTheme').mockReturnValue({
theme: 'light',
effectiveTheme: 'light',
changeTheme: mockChangeTheme,
@@ -689,4 +707,4 @@ describe('Theme Selection', () => {
const selects = screen.getAllByRole('combobox');
expect(selects.length).toBeGreaterThanOrEqual(2);
});
-});
\ No newline at end of file
+});
diff --git a/src/components/__tests__/Sidebar.i18n.test.tsx b/src/components/__tests__/Sidebar.i18n.test.tsx
index 774bdb9..572e439 100644
--- a/src/components/__tests__/Sidebar.i18n.test.tsx
+++ b/src/components/__tests__/Sidebar.i18n.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, mock, spyOn } from 'bun:test';
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
@@ -8,15 +9,15 @@ import { ThemeProvider } from '@/contexts/ThemeContext';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => {}),
})),
});
@@ -30,22 +31,22 @@ const renderWithI18n = (component: React.ReactElement, language = 'en') => {
};
describe('Sidebar i18n Integration', () => {
- const mockOnViewChange = jest.fn();
+ let mockOnViewChange = mock(() => {});
beforeEach(() => {
- jest.clearAllMocks();
+ mockOnViewChange = mock(() => {});
});
describe('English translations', () => {
it('should render app name in English', () => {
renderWithI18n();
-
+
expect(screen.getByText('Lume')).toBeInTheDocument();
});
it('should render all navigation items in English', () => {
renderWithI18n();
-
+
expect(screen.getByText('Dashboard')).toBeInTheDocument();
expect(screen.getByText('Time Tracker')).toBeInTheDocument();
expect(screen.getByText('Reports')).toBeInTheDocument();
@@ -54,7 +55,7 @@ describe('Sidebar i18n Integration', () => {
it('should render navigation with emojis', () => {
renderWithI18n();
-
+
expect(screen.getByText('๐')).toBeInTheDocument();
expect(screen.getByText('โฑ๏ธ')).toBeInTheDocument();
expect(screen.getByText('๐')).toBeInTheDocument();
@@ -65,13 +66,13 @@ describe('Sidebar i18n Integration', () => {
describe('Arabic translations', () => {
it('should render app name in Arabic', () => {
renderWithI18n(, 'ar');
-
+
expect(screen.getByText('ููู
ู')).toBeInTheDocument();
});
it('should render all navigation items in Arabic', () => {
renderWithI18n(, 'ar');
-
+
expect(screen.getByText('ููุญุฉ ุงูุชุญูู
')).toBeInTheDocument();
expect(screen.getByText('ู
ุชุชุจุน ุงูููุช')).toBeInTheDocument();
expect(screen.getByText('ุงูุชูุงุฑูุฑ')).toBeInTheDocument();
@@ -80,7 +81,7 @@ describe('Sidebar i18n Integration', () => {
it('should maintain emojis in Arabic', () => {
renderWithI18n(, 'ar');
-
+
// Emojis should remain the same
expect(screen.getByText('๐')).toBeInTheDocument();
expect(screen.getByText('โฑ๏ธ')).toBeInTheDocument();
@@ -92,26 +93,26 @@ describe('Sidebar i18n Integration', () => {
describe('Navigation functionality', () => {
it('should call onViewChange with correct view when clicking navigation items', () => {
renderWithI18n();
-
+
fireEvent.click(screen.getByText('Time Tracker'));
expect(mockOnViewChange).toHaveBeenCalledWith('tracker');
-
+
fireEvent.click(screen.getByText('Reports'));
expect(mockOnViewChange).toHaveBeenCalledWith('reports');
-
+
fireEvent.click(screen.getByText('Settings'));
expect(mockOnViewChange).toHaveBeenCalledWith('settings');
});
it('should maintain functionality in Arabic', () => {
renderWithI18n(, 'ar');
-
+
fireEvent.click(screen.getByText('ู
ุชุชุจุน ุงูููุช'));
expect(mockOnViewChange).toHaveBeenCalledWith('tracker');
-
+
fireEvent.click(screen.getByText('ุงูุชูุงุฑูุฑ'));
expect(mockOnViewChange).toHaveBeenCalledWith('reports');
-
+
fireEvent.click(screen.getByText('ุงูุฅุนุฏุงุฏุงุช'));
expect(mockOnViewChange).toHaveBeenCalledWith('settings');
});
@@ -120,21 +121,21 @@ describe('Sidebar i18n Integration', () => {
describe('Active view highlighting', () => {
it('should highlight active view in English', () => {
renderWithI18n();
-
+
const trackerButton = screen.getByText('Time Tracker').closest('button');
expect(trackerButton).toHaveClass('bg-primary-50');
});
it('should highlight active view in Arabic', () => {
renderWithI18n(, 'ar');
-
+
const trackerButton = screen.getByText('ู
ุชุชุจุน ุงูููุช').closest('button');
expect(trackerButton).toHaveClass('bg-primary-50');
});
it('should not highlight inactive views', () => {
renderWithI18n();
-
+
const reportsButton = screen.getByText('Reports').closest('button');
expect(reportsButton).not.toHaveClass('bg-primary-50');
});
@@ -146,7 +147,7 @@ describe('Sidebar i18n Integration', () => {
,
'en'
);
-
+
expect(screen.getByText('Dashboard')).toBeInTheDocument();
expect(screen.queryByText('ููุญุฉ ุงูุชุญูู
')).not.toBeInTheDocument();
@@ -168,7 +169,7 @@ describe('Sidebar i18n Integration', () => {
,
'en'
);
-
+
expect(screen.getByText('Lume')).toBeInTheDocument();
i18n.changeLanguage('ar');
@@ -189,7 +190,7 @@ describe('Sidebar i18n Integration', () => {
,
'en'
);
-
+
let reportsButton = screen.getByText('Reports').closest('button');
expect(reportsButton).toHaveClass('bg-primary-50');
@@ -210,7 +211,7 @@ describe('Sidebar i18n Integration', () => {
describe('Menu item ordering', () => {
it('should render menu items in correct order in English', () => {
renderWithI18n();
-
+
const buttons = screen.getAllByRole('button');
expect(buttons[0]).toHaveTextContent('Dashboard');
expect(buttons[1]).toHaveTextContent('Time Tracker');
@@ -220,7 +221,7 @@ describe('Sidebar i18n Integration', () => {
it('should render menu items in correct order in Arabic', () => {
renderWithI18n(, 'ar');
-
+
const buttons = screen.getAllByRole('button');
expect(buttons[0]).toHaveTextContent('ููุญุฉ ุงูุชุญูู
');
expect(buttons[1]).toHaveTextContent('ู
ุชุชุจุน ุงูููุช');
@@ -232,14 +233,14 @@ describe('Sidebar i18n Integration', () => {
describe('Accessibility', () => {
it('should have accessible button roles', () => {
renderWithI18n();
-
+
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(4);
});
it('should maintain button accessibility in Arabic', () => {
renderWithI18n(, 'ar');
-
+
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(4);
buttons.forEach(button => {
@@ -251,25 +252,25 @@ describe('Sidebar i18n Integration', () => {
describe('Edge cases', () => {
it('should handle all view types correctly', () => {
const views = ['dashboard', 'tracker', 'reports', 'settings'] as const;
-
+
views.forEach(view => {
const { unmount } = renderWithI18n(
);
-
+
// Should render without errors
expect(screen.getAllByRole('button')).toHaveLength(4);
-
+
unmount();
});
});
it('should handle missing translations gracefully', () => {
renderWithI18n();
-
+
// Should render something even if translations are incomplete
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBeGreaterThan(0);
});
});
-});
\ No newline at end of file
+});
diff --git a/src/components/features/analytics/__tests__/CalendarHeatmap.test.tsx b/src/components/features/analytics/__tests__/CalendarHeatmap.test.tsx
index 565a8ce..3236637 100644
--- a/src/components/features/analytics/__tests__/CalendarHeatmap.test.tsx
+++ b/src/components/features/analytics/__tests__/CalendarHeatmap.test.tsx
@@ -1,10 +1,9 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
-import { CalendarHeatmap } from '../CalendarHeatmap';
-import type { HeatmapDay } from '@/types';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
// Handle month translations
@@ -24,7 +23,7 @@ jest.mock('react-i18next', () => ({
}));
// Mock ChartCard
-jest.mock('../ChartCard', () => ({
+mock.module('../ChartCard', () => ({
ChartCard: ({ title, description, isLoading, isEmpty, children }: any) => (
{isLoading &&
Loading...
}
@@ -40,6 +39,9 @@ jest.mock('../ChartCard', () => ({
),
}));
+import { CalendarHeatmap } from '../CalendarHeatmap';
+import type { HeatmapDay } from '@/types';
+
describe('CalendarHeatmap', () => {
const mockData: HeatmapDay[] = [
{
diff --git a/src/components/features/analytics/__tests__/ChartCard.test.tsx b/src/components/features/analytics/__tests__/ChartCard.test.tsx
index 965d605..54c9e4e 100644
--- a/src/components/features/analytics/__tests__/ChartCard.test.tsx
+++ b/src/components/features/analytics/__tests__/ChartCard.test.tsx
@@ -1,14 +1,16 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
-import { ChartCard } from '../ChartCard';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
+import { ChartCard } from '../ChartCard';
+
describe('ChartCard', () => {
describe('Rendering', () => {
it('renders with title and children', () => {
diff --git a/src/components/features/analytics/__tests__/HourlyHeatmap.test.tsx b/src/components/features/analytics/__tests__/HourlyHeatmap.test.tsx
index 447a9b5..ef155b1 100644
--- a/src/components/features/analytics/__tests__/HourlyHeatmap.test.tsx
+++ b/src/components/features/analytics/__tests__/HourlyHeatmap.test.tsx
@@ -1,10 +1,9 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
-import { HourlyHeatmap } from '../HourlyHeatmap';
-import type { HourlyPattern } from '@/types';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key === 'analytics.avgHours') return 'Avg Hours';
@@ -18,7 +17,7 @@ jest.mock('react-i18next', () => ({
}));
// Mock ChartCard
-jest.mock('../ChartCard', () => ({
+mock.module('../ChartCard', () => ({
ChartCard: ({ title, description, isLoading, isEmpty, children }: any) => (
{isLoading &&
Loading...
}
@@ -35,7 +34,7 @@ jest.mock('../ChartCard', () => ({
}));
// Mock Recharts
-jest.mock('recharts', () => ({
+mock.module('recharts', () => ({
ResponsiveContainer: ({ children }: any) =>
{children}
,
BarChart: ({ children, data }: any) => (
@@ -54,6 +53,9 @@ jest.mock('recharts', () => ({
Tooltip: () =>
,
}));
+import { HourlyHeatmap } from '../HourlyHeatmap';
+import type { HourlyPattern } from '@/types';
+
describe('HourlyHeatmap', () => {
const mockData: HourlyPattern[] = [
{ hour: 9, avgMinutes: 120, dayCount: 5 },
diff --git a/src/components/features/analytics/__tests__/InsightCard.test.tsx b/src/components/features/analytics/__tests__/InsightCard.test.tsx
index 9abf96a..545ee1d 100644
--- a/src/components/features/analytics/__tests__/InsightCard.test.tsx
+++ b/src/components/features/analytics/__tests__/InsightCard.test.tsx
@@ -1,10 +1,9 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
-import { InsightCard } from '../InsightCard';
-import type { BehavioralInsight } from '@/types';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, params?: any) => {
// Handle translation with params
@@ -20,6 +19,9 @@ jest.mock('react-i18next', () => ({
}),
}));
+import { InsightCard } from '../InsightCard';
+import type { BehavioralInsight } from '@/types';
+
describe('InsightCard', () => {
describe('Rendering - Peak Hour Insight', () => {
it('renders peak_hour insight with all data', () => {
diff --git a/src/components/features/analytics/__tests__/ProductivityLineChart.test.tsx b/src/components/features/analytics/__tests__/ProductivityLineChart.test.tsx
index aabb6ed..3410241 100644
--- a/src/components/features/analytics/__tests__/ProductivityLineChart.test.tsx
+++ b/src/components/features/analytics/__tests__/ProductivityLineChart.test.tsx
@@ -1,10 +1,9 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
-import { ProductivityLineChart } from '../ProductivityLineChart';
-import type { ProductivityTrend } from '@/types';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key === 'analytics.hours') return 'Hours';
@@ -19,7 +18,7 @@ jest.mock('react-i18next', () => ({
}));
// Mock ChartCard
-jest.mock('../ChartCard', () => ({
+mock.module('../ChartCard', () => ({
ChartCard: ({ title, description, isLoading, isEmpty, children }: any) => (
{isLoading &&
Loading...
}
@@ -36,7 +35,7 @@ jest.mock('../ChartCard', () => ({
}));
// Mock Recharts
-jest.mock('recharts', () => ({
+mock.module('recharts', () => ({
ResponsiveContainer: ({ children }: any) =>
{children}
,
LineChart: ({ children, data }: any) => (
@@ -53,6 +52,9 @@ jest.mock('recharts', () => ({
Legend: () =>
,
}));
+import { ProductivityLineChart } from '../ProductivityLineChart';
+import type { ProductivityTrend } from '@/types';
+
describe('ProductivityLineChart', () => {
const mockData: ProductivityTrend[] = [
{ date: '2025-01-15', value: 120 },
diff --git a/src/components/features/dataQuality/__tests__/DataQualityPanel.test.tsx b/src/components/features/dataQuality/__tests__/DataQualityPanel.test.tsx
index e76bc29..ab9cffc 100644
--- a/src/components/features/dataQuality/__tests__/DataQualityPanel.test.tsx
+++ b/src/components/features/dataQuality/__tests__/DataQualityPanel.test.tsx
@@ -1,9 +1,9 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
-import DataQualityPanel from '../DataQualityPanel';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record
= {
@@ -22,7 +22,7 @@ jest.mock('react-i18next', () => ({
}));
// Mock child components
-jest.mock('../GapDetection', () => ({
+mock.module('../GapDetection', () => ({
__esModule: true,
default: ({ startDate, endDate, onCreateActivity }: any) => (
@@ -36,7 +36,7 @@ jest.mock('../GapDetection', () => ({
),
}));
-jest.mock('../DuplicateDetection', () => ({
+mock.module('../DuplicateDetection', () => ({
__esModule: true,
default: ({ startDate, endDate, onMergeActivities }: any) => (
@@ -50,7 +50,7 @@ jest.mock('../DuplicateDetection', () => ({
),
}));
-jest.mock('../DataCleanup', () => ({
+mock.module('../DataCleanup', () => ({
__esModule: true,
default: ({ startDate, endDate, onRefresh }: any) => (
@@ -63,18 +63,21 @@ jest.mock('../DataCleanup', () => ({
}));
// Mock lucide-react icons
-jest.mock('lucide-react', () => ({
+mock.module('lucide-react', () => ({
AlertTriangle: () =>
,
Copy: () =>
,
Database: () =>
,
X: () =>
,
}));
+import DataQualityPanel from '../DataQualityPanel';
+
// Mock window.electronAPI
+let mockBulkUpdate = mock(() => Promise.resolve({ success: true }));
const mockElectronAPI = {
activities: {
bulk: {
- update: jest.fn(),
+ update: mockBulkUpdate,
},
},
};
@@ -85,13 +88,16 @@ const mockElectronAPI = {
describe('DataQualityPanel', () => {
const mockStartDate = '2025-01-01T00:00:00Z';
const mockEndDate = '2025-01-31T23:59:59Z';
- const mockOnClose = jest.fn();
- const mockOnCreateActivity = jest.fn();
- const mockOnRefreshActivities = jest.fn();
+ let mockOnClose = mock(() => {});
+ let mockOnCreateActivity = mock(() => {});
+ let mockOnRefreshActivities = mock(() => {});
beforeEach(() => {
- jest.clearAllMocks();
- mockElectronAPI.activities.bulk.update.mockResolvedValue({ success: true });
+ mockBulkUpdate = mock(() => Promise.resolve({ success: true }));
+ mockElectronAPI.activities.bulk.update = mockBulkUpdate;
+ mockOnClose = mock(() => {});
+ mockOnCreateActivity = mock(() => {});
+ mockOnRefreshActivities = mock(() => {});
});
describe('Rendering', () => {
@@ -374,9 +380,11 @@ describe('DataQualityPanel', () => {
});
it('handles merge failure gracefully', async () => {
- mockElectronAPI.activities.bulk.update.mockResolvedValueOnce({ success: false });
+ mockElectronAPI.activities.bulk.update = mock(() => Promise.resolve({ success: false }));
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+ const originalConsoleError = console.error;
+ console.error = mock(() => {});
+ const consoleErrorSpy = console.error;
render(
{
expect(consoleErrorSpy).toHaveBeenCalled();
});
- consoleErrorSpy.mockRestore();
+ console.error = originalConsoleError;
});
});
diff --git a/src/components/features/dataQuality/__tests__/GapDetection.test.tsx b/src/components/features/dataQuality/__tests__/GapDetection.test.tsx
index 196cc1a..c3d32a7 100644
--- a/src/components/features/dataQuality/__tests__/GapDetection.test.tsx
+++ b/src/components/features/dataQuality/__tests__/GapDetection.test.tsx
@@ -1,10 +1,9 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
-import GapDetection from '../GapDetection';
-import type { TimeGap } from '@/types';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, fallback?: string) => {
const translations: Record = {
@@ -26,7 +25,7 @@ jest.mock('react-i18next', () => ({
}));
// Mock format utils
-jest.mock('../../../../utils/format', () => ({
+mock.module('../../../../utils/format', () => ({
formatDuration: (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@@ -37,7 +36,7 @@ jest.mock('../../../../utils/format', () => ({
}));
// Mock lucide-react icons
-jest.mock('lucide-react', () => ({
+mock.module('lucide-react', () => ({
Clock: () => ,
AlertCircle: () => ,
TrendingUp: () => ,
@@ -45,12 +44,17 @@ jest.mock('lucide-react', () => ({
Plus: () => ,
}));
+import GapDetection from '../GapDetection';
+import type { TimeGap } from '@/types';
+
// Mock window.electronAPI
+let mockDetect = mock(() => Promise.resolve([]));
+let mockGetStatistics = mock(() => Promise.resolve({}));
const mockElectronAPI = {
dataQuality: {
gaps: {
- detect: jest.fn(),
- getStatistics: jest.fn(),
+ detect: mockDetect,
+ getStatistics: mockGetStatistics,
},
},
};
@@ -61,7 +65,7 @@ const mockElectronAPI = {
describe('GapDetection', () => {
const mockStartDate = '2025-01-01T00:00:00Z';
const mockEndDate = '2025-01-31T23:59:59Z';
- const mockOnCreateActivity = jest.fn();
+ let mockOnCreateActivity = mock(() => {});
const mockGaps: TimeGap[] = [
{
@@ -108,14 +112,16 @@ describe('GapDetection', () => {
};
beforeEach(() => {
- jest.clearAllMocks();
- mockElectronAPI.dataQuality.gaps.detect.mockResolvedValue(mockGaps);
- mockElectronAPI.dataQuality.gaps.getStatistics.mockResolvedValue(mockStatistics);
+ mockOnCreateActivity = mock(() => {});
+ mockDetect = mock(() => Promise.resolve(mockGaps));
+ mockGetStatistics = mock(() => Promise.resolve(mockStatistics));
+ mockElectronAPI.dataQuality.gaps.detect = mockDetect;
+ mockElectronAPI.dataQuality.gaps.getStatistics = mockGetStatistics;
});
describe('Loading State', () => {
it('shows loading spinner initially', () => {
- mockElectronAPI.dataQuality.gaps.detect.mockImplementation(() => new Promise(() => {}));
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => new Promise(() => {}));
render(
{
describe('Error State', () => {
it('shows error message when loading fails', async () => {
- mockElectronAPI.dataQuality.gaps.detect.mockRejectedValueOnce(new Error('Failed to load'));
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => Promise.reject(new Error('Failed to load')));
render(
{
});
it('shows alert icon in error state', async () => {
- mockElectronAPI.dataQuality.gaps.detect.mockRejectedValueOnce(new Error('Failed'));
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => Promise.reject(new Error('Failed')));
render(
{
});
it('shows empty state when no gaps', async () => {
- mockElectronAPI.dataQuality.gaps.detect.mockResolvedValueOnce([]);
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => Promise.resolve([]));
render(
{
duration: 600, // 10 minutes
};
- mockElectronAPI.dataQuality.gaps.detect.mockResolvedValueOnce([shortGap]);
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => Promise.resolve([shortGap]));
const { container } = render(
{
duration: 1800, // 30 minutes
};
- mockElectronAPI.dataQuality.gaps.detect.mockResolvedValueOnce([mediumGap]);
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => Promise.resolve([mediumGap]));
const { container } = render(
{
duration: 7200, // 2 hours
};
- mockElectronAPI.dataQuality.gaps.detect.mockResolvedValueOnce([longGap]);
+ mockElectronAPI.dataQuality.gaps.detect = mock(() => Promise.resolve([longGap]));
const { container } = render(
({
+mock.module('framer-motion', () => ({
motion: {
div: ({ children, className, ...props }: any) => {children}
,
},
}));
// Mock translation
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
+import TodoCard from '../TodoCard';
+import { Todo, Category } from '@/types';
+
describe('TodoCard', () => {
const mockTodo: Todo = {
id: 1,
@@ -40,15 +42,20 @@ describe('TodoCard', () => {
},
];
- const mockCallbacks = {
- onEdit: jest.fn(),
- onDelete: jest.fn(),
- onToggleStatus: jest.fn(),
- onQuickStatusChange: jest.fn(),
+ let mockCallbacks = {
+ onEdit: mock(() => {}),
+ onDelete: mock(() => {}),
+ onToggleStatus: mock(() => {}),
+ onQuickStatusChange: mock(() => {}),
};
beforeEach(() => {
- jest.clearAllMocks();
+ mockCallbacks = {
+ onEdit: mock(() => {}),
+ onDelete: mock(() => {}),
+ onToggleStatus: mock(() => {}),
+ onQuickStatusChange: mock(() => {}),
+ };
});
it('renders todo title and description', () => {
diff --git a/src/components/layout/__tests__/Sidebar.test.tsx b/src/components/layout/__tests__/Sidebar.test.tsx
index b13ca2a..4214394 100644
--- a/src/components/layout/__tests__/Sidebar.test.tsx
+++ b/src/components/layout/__tests__/Sidebar.test.tsx
@@ -1,9 +1,9 @@
+import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
-import Sidebar from '../Sidebar';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
@@ -28,22 +28,22 @@ jest.mock('react-i18next', () => ({
}));
// Mock useLanguage hook
-jest.mock('../../../hooks/useLanguage', () => ({
+mock.module('../../../hooks/useLanguage', () => ({
useLanguage: () => ({
isRTL: false,
}),
}));
// Mock useKeyboardShortcuts hook
-const mockUseKeyboardShortcuts = jest.fn();
-jest.mock('../../../hooks/useKeyboardShortcuts', () => ({
+let mockUseKeyboardShortcuts = mock(() => {});
+mock.module('../../../hooks/useKeyboardShortcuts', () => ({
useKeyboardShortcuts: (shortcuts: any) => {
mockUseKeyboardShortcuts(shortcuts);
},
}));
// Mock lucide-react icons
-jest.mock('lucide-react', () => ({
+mock.module('lucide-react', () => ({
LayoutDashboard: () => ,
Timer: () => ,
BarChart3: () => ,
@@ -57,6 +57,8 @@ jest.mock('lucide-react', () => ({
List: () => ,
}));
+import Sidebar from '../Sidebar';
+
// Mock localStorage
const mockLocalStorage = (() => {
let store: Record = {};
@@ -82,12 +84,12 @@ Object.defineProperty(navigator, 'platform', {
});
describe('Sidebar', () => {
- const mockOnViewChange = jest.fn();
+ let mockOnViewChange = mock(() => {});
beforeEach(() => {
- jest.clearAllMocks();
+ mockOnViewChange = mock(() => {});
+ mockUseKeyboardShortcuts = mock(() => {});
mockLocalStorage.clear();
- mockUseKeyboardShortcuts.mockClear();
});
describe('Rendering', () => {
@@ -286,10 +288,11 @@ describe('Sidebar', () => {
});
it('handles localStorage errors gracefully', () => {
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+ const consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
// Mock localStorage to throw error
- jest.spyOn(window.localStorage, 'setItem').mockImplementationOnce(() => {
+ const setItemSpy = spyOn(window.localStorage, 'setItem');
+ setItemSpy.mockImplementation(() => {
throw new Error('Storage error');
});
@@ -300,6 +303,7 @@ describe('Sidebar', () => {
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
+ setItemSpy.mockRestore();
});
});
diff --git a/src/components/layout/__tests__/TitleBar.test.tsx b/src/components/layout/__tests__/TitleBar.test.tsx
index 2aa0b3f..4eb57d2 100644
--- a/src/components/layout/__tests__/TitleBar.test.tsx
+++ b/src/components/layout/__tests__/TitleBar.test.tsx
@@ -1,9 +1,9 @@
+import { describe, it, expect, mock } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
-import TitleBar from '../TitleBar';
// Mock i18n
-jest.mock('react-i18next', () => ({
+mock.module('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key === 'app.name') return 'Lume';
@@ -13,6 +13,8 @@ jest.mock('react-i18next', () => ({
}),
}));
+import TitleBar from '../TitleBar';
+
describe('TitleBar', () => {
describe('Rendering', () => {
it('renders the title bar container', () => {
diff --git a/src/components/pages/ActivityLog/__tests__/SourceTypeIcon.test.tsx b/src/components/pages/ActivityLog/__tests__/SourceTypeIcon.test.tsx
index 31e2a4b..5cf4d14 100644
--- a/src/components/pages/ActivityLog/__tests__/SourceTypeIcon.test.tsx
+++ b/src/components/pages/ActivityLog/__tests__/SourceTypeIcon.test.tsx
@@ -1,15 +1,17 @@
+import { describe, it, expect, mock } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
-import SourceTypeIcon from '../SourceTypeIcon';
-import type { ActivitySourceType } from '@/types';
// Mock lucide-react icons
-jest.mock('lucide-react', () => ({
+mock.module('lucide-react', () => ({
Timer: ({ className }: any) => ,
Monitor: ({ className }: any) => ,
Coffee: ({ className }: any) => ,
}));
+import SourceTypeIcon from '../SourceTypeIcon';
+import type { ActivitySourceType } from '@/types';
+
describe('SourceTypeIcon', () => {
describe('Manual Source Type', () => {
it('renders Timer icon for manual type', () => {
diff --git a/src/components/ui/__tests__/ActivityListCard.test.tsx b/src/components/ui/__tests__/ActivityListCard.test.tsx
index 4d50ce6..26233a2 100644
--- a/src/components/ui/__tests__/ActivityListCard.test.tsx
+++ b/src/components/ui/__tests__/ActivityListCard.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ActivityListCard, { ActivityItem } from '../ActivityListCard';
diff --git a/src/components/ui/__tests__/ProgressListCard.test.tsx b/src/components/ui/__tests__/ProgressListCard.test.tsx
index a800f33..b28d348 100644
--- a/src/components/ui/__tests__/ProgressListCard.test.tsx
+++ b/src/components/ui/__tests__/ProgressListCard.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ProgressListCard, { ProgressListItem } from '../ProgressListCard';
diff --git a/src/components/ui/__tests__/StatCard.test.tsx b/src/components/ui/__tests__/StatCard.test.tsx
index a4593e0..b12cb07 100644
--- a/src/components/ui/__tests__/StatCard.test.tsx
+++ b/src/components/ui/__tests__/StatCard.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect } from 'bun:test';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Clock, CheckCircle2, Target, Smartphone } from 'lucide-react';
diff --git a/src/contexts/__tests__/PomodoroContext.test.tsx b/src/contexts/__tests__/PomodoroContext.test.tsx
index 246ad4c..71cfcc0 100644
--- a/src/contexts/__tests__/PomodoroContext.test.tsx
+++ b/src/contexts/__tests__/PomodoroContext.test.tsx
@@ -1,22 +1,23 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { waitFor, act, renderHook } from '@testing-library/react';
import '@testing-library/jest-dom';
import { PomodoroProvider, usePomodoro, PomodoroTimerStatus } from '../PomodoroContext';
import { PomodoroSettings } from '@/types';
// Mock window.electronAPI
-const mockElectronAPI = {
+let mockElectronAPI = {
pomodoro: {
settings: {
- get: jest.fn(),
- save: jest.fn(),
+ get: mock(() => {}),
+ save: mock(() => {}),
},
timer: {
- start: jest.fn(),
- pause: jest.fn(),
- resume: jest.fn(),
- stop: jest.fn(),
- skip: jest.fn(),
- getStatus: jest.fn(),
+ start: mock(() => {}),
+ pause: mock(() => {}),
+ resume: mock(() => {}),
+ stop: mock(() => {}),
+ skip: mock(() => {}),
+ getStatus: mock(() => {}),
},
},
};
@@ -44,23 +45,27 @@ const defaultStatus: PomodoroTimerStatus = {
describe('PomodoroContext', () => {
beforeEach(() => {
- jest.clearAllMocks();
- jest.useFakeTimers();
-
- mockElectronAPI.pomodoro.settings.get.mockResolvedValue(defaultSettings);
- mockElectronAPI.pomodoro.timer.getStatus.mockResolvedValue(defaultStatus);
- mockElectronAPI.pomodoro.settings.save.mockResolvedValue(true);
- mockElectronAPI.pomodoro.timer.start.mockResolvedValue(undefined);
- mockElectronAPI.pomodoro.timer.pause.mockResolvedValue(undefined);
- mockElectronAPI.pomodoro.timer.resume.mockResolvedValue(undefined);
- mockElectronAPI.pomodoro.timer.stop.mockResolvedValue(undefined);
- mockElectronAPI.pomodoro.timer.skip.mockResolvedValue(undefined);
+ mockElectronAPI = {
+ pomodoro: {
+ settings: {
+ get: mock(() => Promise.resolve(defaultSettings)),
+ save: mock(() => Promise.resolve(true)),
+ },
+ timer: {
+ start: mock(() => Promise.resolve(undefined)),
+ pause: mock(() => Promise.resolve(undefined)),
+ resume: mock(() => Promise.resolve(undefined)),
+ stop: mock(() => Promise.resolve(undefined)),
+ skip: mock(() => Promise.resolve(undefined)),
+ getStatus: mock(() => Promise.resolve(defaultStatus)),
+ },
+ },
+ };
(window as any).electronAPI = mockElectronAPI;
});
afterEach(() => {
- jest.useRealTimers();
delete (window as any).electronAPI;
});
@@ -140,8 +145,8 @@ describe('PomodoroContext', () => {
});
it('should handle save settings error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.settings.save.mockRejectedValue(new Error('Save failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.settings.save = mock(() => Promise.reject(new Error('Save failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -160,10 +165,10 @@ describe('PomodoroContext', () => {
it('should reload settings', async () => {
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
- mockElectronAPI.pomodoro.settings.get.mockResolvedValueOnce({
+ mockElectronAPI.pomodoro.settings.get = mock(() => Promise.resolve({
...defaultSettings,
focusDuration: 35,
- });
+ }));
await act(async () => {
await result.current.loadSettings();
@@ -183,7 +188,7 @@ describe('PomodoroContext', () => {
timeRemaining: 1500,
};
- mockElectronAPI.pomodoro.timer.getStatus.mockResolvedValueOnce(newStatus);
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => Promise.resolve(newStatus));
await act(async () => {
await result.current.refreshStatus();
@@ -194,8 +199,8 @@ describe('PomodoroContext', () => {
});
it('should handle refresh status error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.timer.getStatus.mockRejectedValue(new Error('Refresh failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => Promise.reject(new Error('Refresh failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -254,8 +259,8 @@ describe('PomodoroContext', () => {
});
it('should handle start session error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.timer.start.mockRejectedValue(new Error('Start failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.timer.start = mock(() => Promise.reject(new Error('Start failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -294,8 +299,8 @@ describe('PomodoroContext', () => {
});
it('should handle pause error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.timer.pause.mockRejectedValue(new Error('Pause failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.timer.pause = mock(() => Promise.reject(new Error('Pause failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -334,8 +339,8 @@ describe('PomodoroContext', () => {
});
it('should handle resume error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.timer.resume.mockRejectedValue(new Error('Resume failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.timer.resume = mock(() => Promise.reject(new Error('Resume failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -374,8 +379,8 @@ describe('PomodoroContext', () => {
});
it('should handle stop error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.timer.stop.mockRejectedValue(new Error('Stop failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.timer.stop = mock(() => Promise.reject(new Error('Stop failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -414,8 +419,8 @@ describe('PomodoroContext', () => {
});
it('should handle skip error', async () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- mockElectronAPI.pomodoro.timer.skip.mockRejectedValue(new Error('Skip failed'));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ mockElectronAPI.pomodoro.timer.skip = mock(() => Promise.reject(new Error('Skip failed')));
const { result } = renderHook(() => usePomodoro(), { wrapper: PomodoroProvider });
@@ -442,22 +447,24 @@ describe('PomodoroContext', () => {
timeRemaining: 1500,
};
- mockElectronAPI.pomodoro.timer.getStatus.mockResolvedValue(runningStatus);
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => Promise.resolve(runningStatus));
await act(async () => {
await result.current.refreshStatus();
});
- // Clear previous calls
- mockElectronAPI.pomodoro.timer.getStatus.mockClear();
-
- // Advance timers to trigger polling
- await act(async () => {
- jest.advanceTimersByTime(1000);
+ // Reset mock for tracking new calls
+ let callCount = 0;
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => {
+ callCount++;
+ return Promise.resolve(runningStatus);
});
+ // Advance timers to trigger polling (using setTimeout instead of jest fake timers)
+ await new Promise(resolve => setTimeout(resolve, 1100));
+
await waitFor(() => {
- expect(mockElectronAPI.pomodoro.timer.getStatus).toHaveBeenCalled();
+ expect(callCount).toBeGreaterThan(0);
});
});
@@ -470,20 +477,24 @@ describe('PomodoroContext', () => {
timeRemaining: 1200,
};
- mockElectronAPI.pomodoro.timer.getStatus.mockResolvedValue(pausedStatus);
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => Promise.resolve(pausedStatus));
await act(async () => {
await result.current.refreshStatus();
});
- mockElectronAPI.pomodoro.timer.getStatus.mockClear();
-
- await act(async () => {
- jest.advanceTimersByTime(1000);
+ // Reset mock for tracking new calls
+ let callCount = 0;
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => {
+ callCount++;
+ return Promise.resolve(pausedStatus);
});
+ // Advance timers to trigger polling
+ await new Promise(resolve => setTimeout(resolve, 1100));
+
await waitFor(() => {
- expect(mockElectronAPI.pomodoro.timer.getStatus).toHaveBeenCalled();
+ expect(callCount).toBeGreaterThan(0);
});
});
@@ -494,20 +505,23 @@ describe('PomodoroContext', () => {
await result.current.refreshStatus();
});
- mockElectronAPI.pomodoro.timer.getStatus.mockClear();
-
- await act(async () => {
- jest.advanceTimersByTime(2000);
+ // Reset mock for tracking new calls
+ let callCount = 0;
+ mockElectronAPI.pomodoro.timer.getStatus = mock(() => {
+ callCount++;
+ return Promise.resolve(defaultStatus);
});
+ await new Promise(resolve => setTimeout(resolve, 2100));
+
// Should not poll when idle
- expect(mockElectronAPI.pomodoro.timer.getStatus).not.toHaveBeenCalled();
+ expect(callCount).toBe(0);
});
});
describe('usePomodoro Hook', () => {
it('should throw error when used outside provider', () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => usePomodoro());
diff --git a/src/database/__tests__/DatabaseManager.pomodoro.test.ts b/src/database/__tests__/DatabaseManager.pomodoro.test.ts
index 67db0f4..c6eec33 100644
--- a/src/database/__tests__/DatabaseManager.pomodoro.test.ts
+++ b/src/database/__tests__/DatabaseManager.pomodoro.test.ts
@@ -1,97 +1,93 @@
-import { DatabaseManager } from '../DatabaseManager';
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { PomodoroSession } from '@/types';
-// Mock the DatabaseManager to avoid native module issues in Jest
-jest.mock('../DatabaseManager', () => {
- let sessions: Map;
- let nextId: number;
+// Create mock state
+let sessions: Map;
+let nextId: number;
- const resetState = () => {
- sessions = new Map();
- nextId = 1;
- };
+const resetState = () => {
+ sessions = new Map();
+ nextId = 1;
+};
+
+resetState();
+// Mock DatabaseManager implementation
+const createMockDatabaseManager = () => {
resetState();
+ return {
+ addPomodoroSession: mock((session: PomodoroSession) => {
+ const id = nextId++;
+ sessions.set(id, { ...session, id });
+ return id;
+ }),
+ updatePomodoroSession: mock((id: number, updates: Partial) => {
+ const session = sessions.get(id);
+ if (!session) return false;
+ if (Object.keys(updates).length === 0) return false;
+ Object.assign(session, updates);
+ return true;
+ }),
+ getPomodoroSessions: mock((limit = 100) => {
+ return Array.from(sessions.values())
+ .sort((a, b) => (b.id || 0) - (a.id || 0))
+ .slice(0, limit);
+ }),
+ getPomodoroSessionsByDateRange: mock((startDate: string, endDate: string) => {
+ return Array.from(sessions.values())
+ .filter((s) => {
+ const sessionDate = s.startTime.split('T')[0];
+ return sessionDate >= startDate && sessionDate <= endDate;
+ })
+ .sort((a, b) => b.startTime.localeCompare(a.startTime));
+ }),
+ getPomodoroStats: mock((startDate?: string, endDate?: string) => {
+ let filteredSessions = Array.from(sessions.values());
+
+ if (startDate && endDate) {
+ filteredSessions = filteredSessions.filter((s) => {
+ const sessionDate = s.startTime.split('T')[0];
+ return sessionDate >= startDate && sessionDate <= endDate;
+ });
+ }
- const MockDatabaseManager = jest.fn().mockImplementation(() => {
- resetState(); // Reset state for each new instance
- return {
- addPomodoroSession: jest.fn((session: PomodoroSession) => {
- const id = nextId++;
- sessions.set(id, { ...session, id });
- return id;
- }),
- updatePomodoroSession: jest.fn((id: number, updates: Partial) => {
- const session = sessions.get(id);
- if (!session) return false;
- if (Object.keys(updates).length === 0) return false;
- Object.assign(session, updates);
- return true;
- }),
- getPomodoroSessions: jest.fn((limit = 100) => {
- return Array.from(sessions.values())
- .sort((a, b) => (b.id || 0) - (a.id || 0))
- .slice(0, limit);
- }),
- getPomodoroSessionsByDateRange: jest.fn((startDate: string, endDate: string) => {
- return Array.from(sessions.values())
- .filter((s) => {
- const sessionDate = s.startTime.split('T')[0];
- return sessionDate >= startDate && sessionDate <= endDate;
- })
- .sort((a, b) => b.startTime.localeCompare(a.startTime));
- }),
- getPomodoroStats: jest.fn((startDate?: string, endDate?: string) => {
- let filteredSessions = Array.from(sessions.values());
-
- if (startDate && endDate) {
- filteredSessions = filteredSessions.filter((s) => {
- const sessionDate = s.startTime.split('T')[0];
- return sessionDate >= startDate && sessionDate <= endDate;
- });
- }
-
- const totalSessions = filteredSessions.length;
- const completedSessions = filteredSessions.filter((s) => s.completed).length;
- const totalFocusTime = filteredSessions
- .filter((s) => s.sessionType === 'focus' && s.completed)
- .reduce((sum, s) => sum + s.duration, 0);
- const totalBreakTime = filteredSessions
- .filter(
- (s) => (s.sessionType === 'shortBreak' || s.sessionType === 'longBreak') && s.completed
- )
- .reduce((sum, s) => sum + s.duration, 0);
- const completionRate =
- totalSessions > 0 ? Math.round((completedSessions / totalSessions) * 100) : 0;
-
- return {
- totalSessions,
- completedSessions,
- totalFocusTime,
- totalBreakTime,
- completionRate,
- currentStreak: 0,
- };
- }),
- initialize: jest.fn(),
+ const totalSessions = filteredSessions.length;
+ const completedSessions = filteredSessions.filter((s) => s.completed).length;
+ const totalFocusTime = filteredSessions
+ .filter((s) => s.sessionType === 'focus' && s.completed)
+ .reduce((sum, s) => sum + s.duration, 0);
+ const totalBreakTime = filteredSessions
+ .filter(
+ (s) => (s.sessionType === 'shortBreak' || s.sessionType === 'longBreak') && s.completed
+ )
+ .reduce((sum, s) => sum + s.duration, 0);
+ const completionRate =
+ totalSessions > 0 ? Math.round((completedSessions / totalSessions) * 100) : 0;
+
+ return {
+ totalSessions,
+ completedSessions,
+ totalFocusTime,
+ totalBreakTime,
+ completionRate,
+ currentStreak: 0,
};
- });
-
- return {
- DatabaseManager: MockDatabaseManager,
- default: new MockDatabaseManager(),
+ }),
+ initialize: mock(() => {}),
};
-});
+};
+
+// Type alias for mock
+type MockDatabaseManager = ReturnType;
describe('DatabaseManager - Pomodoro Methods', () => {
- let dbManager: DatabaseManager;
- let consoleLog: jest.SpyInstance;
+ let dbManager: MockDatabaseManager;
+ let consoleLog: ReturnType;
beforeEach(() => {
- jest.clearAllMocks();
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
- dbManager = new DatabaseManager();
+ dbManager = createMockDatabaseManager();
dbManager.initialize();
});
diff --git a/src/database/__tests__/DatabaseManager.test.ts b/src/database/__tests__/DatabaseManager.test.ts
index f14de5e..a52acab 100644
--- a/src/database/__tests__/DatabaseManager.test.ts
+++ b/src/database/__tests__/DatabaseManager.test.ts
@@ -1,87 +1,100 @@
-import { DatabaseManager } from '../DatabaseManager';
-import Database from 'better-sqlite3';
-import { app } from 'electron';
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { ActivitySession } from '@/types/activity';
+// Create mock functions
+const mockRunMigrations = mock(() => {});
+const mockAppUsageInsert = mock(() => 123);
+
// Mock better-sqlite3
-jest.mock('better-sqlite3');
+mock.module('better-sqlite3', () => ({
+ default: mock(() => ({
+ prepare: mock(() => ({})),
+ exec: mock(() => {}),
+ close: mock(() => {}),
+ pragma: mock(() => {}),
+ })),
+}));
// Mock electron app
-jest.mock('electron', () => ({
+mock.module('electron', () => ({
app: {
- getPath: jest.fn(),
+ getPath: mock(() => '/test/path'),
},
}));
// Mock MigrationRunner
-jest.mock('../migrations/MigrationRunner', () => ({
- MigrationRunner: jest.fn().mockImplementation(() => ({
- runMigrations: jest.fn(),
+mock.module('../migrations/MigrationRunner', () => ({
+ MigrationRunner: mock(() => ({
+ runMigrations: mockRunMigrations,
})),
}));
// Mock Repositories
-jest.mock('../repositories/TimeEntryRepository', () => ({
- TimeEntryRepository: jest.fn().mockImplementation(() => ({})),
+mock.module('../repositories/TimeEntryRepository', () => ({
+ TimeEntryRepository: mock(() => ({})),
}));
-jest.mock('../repositories/AppUsageRepository', () => ({
- AppUsageRepository: jest.fn().mockImplementation(() => ({
- insert: jest.fn().mockReturnValue(123),
+mock.module('../repositories/AppUsageRepository', () => ({
+ AppUsageRepository: mock(() => ({
+ insert: mockAppUsageInsert,
})),
}));
-jest.mock('../repositories/CategoryRepository', () => ({
- CategoryRepository: jest.fn().mockImplementation(() => ({})),
+mock.module('../repositories/CategoryRepository', () => ({
+ CategoryRepository: mock(() => ({})),
}));
-jest.mock('../repositories/TagRepository', () => ({
- TagRepository: jest.fn().mockImplementation(() => ({})),
+mock.module('../repositories/TagRepository', () => ({
+ TagRepository: mock(() => ({})),
}));
-jest.mock('../repositories/PomodoroRepository', () => ({
- PomodoroRepository: jest.fn().mockImplementation(() => ({})),
+mock.module('../repositories/PomodoroRepository', () => ({
+ PomodoroRepository: mock(() => ({})),
}));
-jest.mock('../repositories/GoalRepository', () => ({
- GoalRepository: jest.fn().mockImplementation(() => ({})),
+mock.module('../repositories/GoalRepository', () => ({
+ GoalRepository: mock(() => ({})),
}));
-jest.mock('../repositories/MappingRepository', () => ({
- MappingRepository: jest.fn().mockImplementation(() => ({})),
+mock.module('../repositories/MappingRepository', () => ({
+ MappingRepository: mock(() => ({})),
}));
// Mock AnalyticsService
-jest.mock('../analytics/AnalyticsService', () => ({
- AnalyticsService: jest.fn().mockImplementation(() => ({})),
+mock.module('../analytics/AnalyticsService', () => ({
+ AnalyticsService: mock(() => ({})),
}));
+import { DatabaseManager } from '../DatabaseManager';
+import Database from 'better-sqlite3';
+import { app } from 'electron';
+
describe('DatabaseManager', () => {
let dbManager: DatabaseManager;
let mockDb: any;
- let mockPrepare: jest.Mock;
- let mockExec: jest.Mock;
- let consoleLog: jest.SpyInstance;
- let consoleError: jest.SpyInstance;
+ let mockPrepare: ReturnType;
+ let mockExec: ReturnType;
+ let consoleLog: ReturnType;
+ let consoleError: ReturnType;
beforeEach(() => {
// Setup console spies
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
- consoleError = jest.spyOn(console, 'error').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
+ consoleError = spyOn(console, 'error').mockImplementation(() => {});
// Mock database operations
- mockPrepare = jest.fn();
- mockExec = jest.fn();
+ mockPrepare = mock(() => ({}));
+ mockExec = mock(() => {});
mockDb = {
prepare: mockPrepare,
exec: mockExec,
- close: jest.fn(),
- pragma: jest.fn(),
+ close: mock(() => {}),
+ pragma: mock(() => {}),
};
- (Database as unknown as jest.Mock).mockReturnValue(mockDb);
- (app.getPath as jest.Mock).mockReturnValue('/test/path');
+ (Database as any) = mock(() => mockDb);
+ (app.getPath as any) = mock(() => '/test/path');
dbManager = new DatabaseManager();
// Initialize the database with test path
@@ -89,7 +102,6 @@ describe('DatabaseManager', () => {
});
afterEach(() => {
- jest.clearAllMocks();
consoleLog.mockRestore();
consoleError.mockRestore();
});
diff --git a/src/database/repositories/__tests__/TodoRepository.test.ts b/src/database/repositories/__tests__/TodoRepository.test.ts
index 5e4927b..3cc41c7 100644
--- a/src/database/repositories/__tests__/TodoRepository.test.ts
+++ b/src/database/repositories/__tests__/TodoRepository.test.ts
@@ -1,40 +1,37 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { TodoRepository } from '../TodoRepository';
import { Todo } from '../../../types';
-// Mock better-sqlite3
-jest.mock('better-sqlite3');
-
describe('TodoRepository', () => {
let repository: TodoRepository;
let mockDb: any;
- let mockPrepare: jest.Mock;
- let mockRun: jest.Mock;
- let mockGet: jest.Mock;
- let mockAll: jest.Mock;
+ let mockPrepare: ReturnType;
+ let mockRun: ReturnType;
+ let mockGet: ReturnType;
+ let mockAll: ReturnType;
beforeEach(() => {
// Setup mock database methods
- mockRun = jest.fn().mockReturnValue({ changes: 1, lastInsertRowid: 1 });
- mockGet = jest.fn();
- mockAll = jest.fn().mockReturnValue([]);
+ mockRun = mock(() => ({ changes: 1, lastInsertRowid: 1 }));
+ mockGet = mock(() => undefined);
+ mockAll = mock(() => []);
- mockPrepare = jest.fn().mockReturnValue({
+ mockPrepare = mock(() => ({
run: mockRun,
get: mockGet,
all: mockAll,
- });
+ }));
mockDb = {
prepare: mockPrepare,
- exec: jest.fn(),
- transaction: jest.fn((fn) => () => fn()),
+ exec: mock(() => {}),
+ transaction: mock((fn: any) => () => fn()),
};
repository = new TodoRepository(mockDb);
});
afterEach(() => {
- jest.clearAllMocks();
});
describe('insert', () => {
@@ -117,7 +114,9 @@ describe('TodoRepository', () => {
describe('delete', () => {
it('should delete a todo by id', () => {
- mockRun.mockReturnValue({ changes: 1 });
+ mockRun = mock(() => ({ changes: 1 }));
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.delete(1);
@@ -127,7 +126,9 @@ describe('TodoRepository', () => {
});
it('should return false when todo not found', () => {
- mockRun.mockReturnValue({ changes: 0 });
+ mockRun = mock(() => ({ changes: 0 }));
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.delete(999);
@@ -155,7 +156,9 @@ describe('TodoRepository', () => {
updated_at: '2024-01-01T00:00:00Z',
};
- mockGet.mockReturnValue(mockTodo);
+ mockGet = mock(() => mockTodo);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getById(1);
@@ -167,7 +170,9 @@ describe('TodoRepository', () => {
});
it('should return null when todo not found', () => {
- mockGet.mockReturnValue(null);
+ mockGet = mock(() => null);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getById(999);
@@ -194,7 +199,9 @@ describe('TodoRepository', () => {
},
];
- mockAll.mockReturnValue(mockTodos);
+ mockAll = mock(() => mockTodos);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAll();
@@ -203,7 +210,9 @@ describe('TodoRepository', () => {
});
it('should return empty array when no todos', () => {
- mockAll.mockReturnValue([]);
+ mockAll = mock(() => []);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAll();
@@ -222,7 +231,9 @@ describe('TodoRepository', () => {
},
];
- mockAll.mockReturnValue(mockTodos);
+ mockAll = mock(() => mockTodos);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAll({ status: 'todo' });
@@ -231,7 +242,9 @@ describe('TodoRepository', () => {
});
it('should return empty array when no todos match status', () => {
- mockAll.mockReturnValue([]);
+ mockAll = mock(() => []);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAll({ status: 'completed' });
@@ -250,7 +263,9 @@ describe('TodoRepository', () => {
},
];
- mockAll.mockReturnValue(mockTodos);
+ mockAll = mock(() => mockTodos);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAll({ priority: 'urgent' });
@@ -270,7 +285,9 @@ describe('TodoRepository', () => {
},
];
- mockAll.mockReturnValue(mockTodos);
+ mockAll = mock(() => mockTodos);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
repository.getOverdue();
@@ -281,9 +298,15 @@ describe('TodoRepository', () => {
describe('getStats', () => {
it('should return todo statistics', () => {
- mockGet.mockReturnValueOnce({ total: 10, completed: 5, inProgress: 2 });
- mockGet.mockReturnValueOnce({ overdue: 1 });
- mockGet.mockReturnValueOnce({ avgMinutes: 120 });
+ let callCount = 0;
+ mockGet = mock(() => {
+ callCount++;
+ if (callCount === 1) return { total: 10, completed: 5, inProgress: 2 };
+ if (callCount === 2) return { overdue: 1 };
+ return { avgMinutes: 120 };
+ });
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getStats();
@@ -296,9 +319,15 @@ describe('TodoRepository', () => {
});
it('should handle zero division in completion rate', () => {
- mockGet.mockReturnValueOnce({ total: 0, completed: 0, inProgress: 0 });
- mockGet.mockReturnValueOnce({ overdue: 0 });
- mockGet.mockReturnValueOnce({ avgMinutes: null });
+ let callCount = 0;
+ mockGet = mock(() => {
+ callCount++;
+ if (callCount === 1) return { total: 0, completed: 0, inProgress: 0 };
+ if (callCount === 2) return { overdue: 0 };
+ return { avgMinutes: null };
+ });
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getStats();
@@ -308,7 +337,9 @@ describe('TodoRepository', () => {
describe('linkTimeEntry', () => {
it('should link a time entry to a todo', () => {
- mockRun.mockReturnValue({ changes: 1 });
+ mockRun = mock(() => ({ changes: 1 }));
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.linkTimeEntry(1, 100);
@@ -318,7 +349,9 @@ describe('TodoRepository', () => {
});
it('should return false when todo not found', () => {
- mockRun.mockReturnValue({ changes: 0 });
+ mockRun = mock(() => ({ changes: 0 }));
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.linkTimeEntry(999, 100);
@@ -328,7 +361,9 @@ describe('TodoRepository', () => {
describe('incrementPomodoroCount', () => {
it('should increment pomodoro count', () => {
- mockRun.mockReturnValue({ changes: 1 });
+ mockRun = mock(() => ({ changes: 1 }));
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.incrementPomodoroCount(1);
@@ -338,7 +373,9 @@ describe('TodoRepository', () => {
});
it('should return false when todo not found', () => {
- mockRun.mockReturnValue({ changes: 0 });
+ mockRun = mock(() => ({ changes: 0 }));
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.incrementPomodoroCount(999);
@@ -353,7 +390,9 @@ describe('TodoRepository', () => {
{ id: 2, name: 'urgent', color: '#00FF00', createdAt: '2024-01-01T00:00:00Z' },
];
- mockAll.mockReturnValue(mockTags);
+ mockAll = mock(() => mockTags);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getTags(1);
@@ -363,7 +402,9 @@ describe('TodoRepository', () => {
});
it('should return empty array when todo has no tags', () => {
- mockAll.mockReturnValue([]);
+ mockAll = mock(() => []);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getTags(1);
@@ -417,7 +458,14 @@ describe('TodoRepository', () => {
{ id: 1, name: 'work', color: '#FF0000', createdAt: '2024-01-01T00:00:00Z' },
];
- mockAll.mockReturnValueOnce(mockTodos).mockReturnValueOnce(mockTags);
+ let callCount = 0;
+ mockAll = mock(() => {
+ callCount++;
+ if (callCount === 1) return mockTodos;
+ return mockTags;
+ });
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAllWithTags();
@@ -441,7 +489,9 @@ describe('TodoRepository', () => {
},
];
- mockAll.mockReturnValue(mockTodos);
+ mockAll = mock(() => mockTodos);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAllWithCategory();
@@ -462,7 +512,9 @@ describe('TodoRepository', () => {
},
];
- mockAll.mockReturnValue(mockTodos);
+ mockAll = mock(() => mockTodos);
+ mockPrepare = mock(() => ({ run: mockRun, get: mockGet, all: mockAll }));
+ mockDb.prepare = mockPrepare;
const result = repository.getAllWithCategory();
diff --git a/src/hooks/__tests__/useLanguage.test.tsx b/src/hooks/__tests__/useLanguage.test.tsx
index e57d656..084aeda 100644
--- a/src/hooks/__tests__/useLanguage.test.tsx
+++ b/src/hooks/__tests__/useLanguage.test.tsx
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useLanguage } from '../useLanguage';
import i18n from '../../i18n/config';
diff --git a/src/i18n/__tests__/config.test.ts b/src/i18n/__tests__/config.test.ts
index 9b27dfe..9e6abf1 100644
--- a/src/i18n/__tests__/config.test.ts
+++ b/src/i18n/__tests__/config.test.ts
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach } from 'bun:test';
import i18n, { getDirection, isRTL, resources } from '../config';
describe('i18n configuration', () => {
diff --git a/src/i18n/__tests__/locales.test.ts b/src/i18n/__tests__/locales.test.ts
index 4229dcf..79d8503 100644
--- a/src/i18n/__tests__/locales.test.ts
+++ b/src/i18n/__tests__/locales.test.ts
@@ -1,3 +1,4 @@
+import { describe, it, expect } from 'bun:test';
import en from '../locales/en.json';
import ar from '../locales/ar.json';
diff --git a/src/i18n/__tests__/locales.theme.test.ts b/src/i18n/__tests__/locales.theme.test.ts
index 3671445..fb2dd7a 100644
--- a/src/i18n/__tests__/locales.theme.test.ts
+++ b/src/i18n/__tests__/locales.theme.test.ts
@@ -1,6 +1,6 @@
-// eslint-disable-next-line @typescript-eslint/no-require-imports
+import { describe, it, expect } from 'bun:test';
+
const enLocale = require('../locales/en.json');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
const arLocale = require('../locales/ar.json');
describe('Theme Locale Keys', () => {
diff --git a/src/main/__tests__/main.test.ts b/src/main/__tests__/main.test.ts
index cfe2924..c68483b 100644
--- a/src/main/__tests__/main.test.ts
+++ b/src/main/__tests__/main.test.ts
@@ -1,6 +1,8 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
+
/**
* Tests for main.ts
- *
+ *
* Note: These tests focus on the autoStartTracking method and settings management
* as added in the diff. Full integration testing of Electron app lifecycle
* is outside the scope of unit tests.
@@ -9,23 +11,23 @@
describe('Main Process - Auto Start Tracking', () => {
let _mockDbManager: any;
let mockActivityTracker: any;
- let consoleLog: jest.SpyInstance;
- let consoleError: jest.SpyInstance;
+ let consoleLog: ReturnType;
+ let consoleError: ReturnType;
beforeEach(() => {
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
- consoleError = jest.spyOn(console, 'error').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
+ consoleError = spyOn(console, 'error').mockImplementation(() => {});
mockActivityTracker = {
- updateSettings: jest.fn(),
- start: jest.fn(),
- stop: jest.fn(),
- isTracking: jest.fn().mockReturnValue(false),
+ updateSettings: mock(() => {}),
+ start: mock(() => {}),
+ stop: mock(() => {}),
+ isTracking: mock(() => false),
};
_mockDbManager = {
- initialize: jest.fn(),
- addActivitySession: jest.fn(),
+ initialize: mock(() => {}),
+ addActivitySession: mock(() => {}),
};
});
@@ -145,7 +147,7 @@ describe('Main Process - Auto Start Tracking', () => {
});
it('should log activity tracking status after update', () => {
- mockActivityTracker.isTracking.mockReturnValue(true);
+ mockActivityTracker.isTracking = mock(() => true);
const isTracking = mockActivityTracker.isTracking();
consoleLog(`๐ Activity tracking status after settings update: ${isTracking ? 'ACTIVE' : 'STOPPED'}`);
@@ -154,7 +156,7 @@ describe('Main Process - Auto Start Tracking', () => {
});
it('should log stopped status when not tracking', () => {
- mockActivityTracker.isTracking.mockReturnValue(false);
+ mockActivityTracker.isTracking = mock(() => false);
const isTracking = mockActivityTracker.isTracking();
consoleLog(`๐ Activity tracking status after settings update: ${isTracking ? 'ACTIVE' : 'STOPPED'}`);
diff --git a/src/main/ipc/handlers/__tests__/TodosHandlers.test.ts b/src/main/ipc/handlers/__tests__/TodosHandlers.test.ts
index c24121f..296a511 100644
--- a/src/main/ipc/handlers/__tests__/TodosHandlers.test.ts
+++ b/src/main/ipc/handlers/__tests__/TodosHandlers.test.ts
@@ -1,3 +1,4 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { IpcMain } from 'electron';
import { TodosHandlers } from '../TodosHandlers';
import { IIPCHandlerContext } from '@/types';
@@ -5,44 +6,43 @@ import { Todo, TodoStats } from '../../../../types';
describe('TodosHandlers', () => {
let handlers: TodosHandlers;
- let mockIpcMain: jest.Mocked;
+ let mockIpcMain: any;
let mockContext: IIPCHandlerContext;
- let consoleLog: jest.SpyInstance;
- let consoleError: jest.SpyInstance;
+ let consoleLog: ReturnType;
+ let consoleError: ReturnType;
let handlerCallbacks: Map Promise>;
beforeEach(() => {
- jest.clearAllMocks();
handlerCallbacks = new Map();
// Mock IpcMain
mockIpcMain = {
- handle: jest.fn((channel: string, listener: any) => {
+ handle: mock((channel: string, listener: any) => {
handlerCallbacks.set(channel, listener);
}),
- removeHandler: jest.fn(),
+ removeHandler: mock(() => {}),
} as any;
// Mock DatabaseManager methods
mockContext = {
dbManager: {
- addTodo: jest.fn(),
- updateTodo: jest.fn(),
- deleteTodo: jest.fn(),
- getTodos: jest.fn(),
- getTodo: jest.fn(),
- getTodoStats: jest.fn(),
- getTodosWithCategory: jest.fn(),
- linkTodoToTimeEntry: jest.fn(),
- incrementTodoPomodoro: jest.fn(),
+ addTodo: mock(() => {}),
+ updateTodo: mock(() => {}),
+ deleteTodo: mock(() => {}),
+ getTodos: mock(() => {}),
+ getTodo: mock(() => {}),
+ getTodoStats: mock(() => {}),
+ getTodosWithCategory: mock(() => {}),
+ linkTodoToTimeEntry: mock(() => {}),
+ incrementTodoPomodoro: mock(() => {}),
} as any,
} as unknown as IIPCHandlerContext;
handlers = new TodosHandlers();
// Spy on console methods
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
- consoleError = jest.spyOn(console, 'error').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
+ consoleError = spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
@@ -80,7 +80,7 @@ describe('TodosHandlers', () => {
priority: 'high',
};
- (mockContext.dbManager?.addTodo as jest.Mock).mockReturnValue(1);
+ (mockContext.dbManager as any).addTodo = mock(() => 1);
const handler = handlerCallbacks.get('add-todo');
const result = await handler!(null, mockTodo);
@@ -101,7 +101,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return null', async () => {
- (mockContext.dbManager?.addTodo as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).addTodo = mock(() => {
throw new Error('Database error');
});
@@ -124,7 +124,7 @@ describe('TodosHandlers', () => {
status: 'completed',
};
- (mockContext.dbManager?.updateTodo as jest.Mock).mockReturnValue(true);
+ (mockContext.dbManager as any).updateTodo = mock(() => true);
const handler = handlerCallbacks.get('update-todo');
const result = await handler!(null, 1, updates);
@@ -135,7 +135,7 @@ describe('TodosHandlers', () => {
});
it('should return false when todo not found', async () => {
- (mockContext.dbManager?.updateTodo as jest.Mock).mockReturnValue(false);
+ (mockContext.dbManager as any).updateTodo = mock(() => false);
const handler = handlerCallbacks.get('update-todo');
const result = await handler!(null, 999, { title: 'Test' });
@@ -154,7 +154,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return false', async () => {
- (mockContext.dbManager?.updateTodo as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).updateTodo = mock(() => {
throw new Error('Update error');
});
@@ -172,7 +172,7 @@ describe('TodosHandlers', () => {
});
it('should delete a todo successfully', async () => {
- (mockContext.dbManager?.deleteTodo as jest.Mock).mockReturnValue(true);
+ (mockContext.dbManager as any).deleteTodo = mock(() => true);
const handler = handlerCallbacks.get('delete-todo');
const result = await handler!(null, 1);
@@ -183,7 +183,7 @@ describe('TodosHandlers', () => {
});
it('should return false when todo not found', async () => {
- (mockContext.dbManager?.deleteTodo as jest.Mock).mockReturnValue(false);
+ (mockContext.dbManager as any).deleteTodo = mock(() => false);
const handler = handlerCallbacks.get('delete-todo');
const result = await handler!(null, 999);
@@ -202,7 +202,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return false', async () => {
- (mockContext.dbManager?.deleteTodo as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).deleteTodo = mock(() => {
throw new Error('Delete error');
});
@@ -241,7 +241,7 @@ describe('TodosHandlers', () => {
},
];
- (mockContext.dbManager?.getTodos as jest.Mock).mockReturnValue(mockTodos);
+ (mockContext.dbManager as any).getTodos = mock(() => mockTodos);
const handler = handlerCallbacks.get('get-todos');
const result = await handler!(null);
@@ -263,7 +263,7 @@ describe('TodosHandlers', () => {
},
];
- (mockContext.dbManager?.getTodos as jest.Mock).mockReturnValue(mockTodos);
+ (mockContext.dbManager as any).getTodos = mock(() => mockTodos);
const handler = handlerCallbacks.get('get-todos');
const result = await handler!(null, { status: 'todo' });
@@ -285,7 +285,7 @@ describe('TodosHandlers', () => {
},
];
- (mockContext.dbManager?.getTodos as jest.Mock).mockReturnValue(mockTodos);
+ (mockContext.dbManager as any).getTodos = mock(() => mockTodos);
const handler = handlerCallbacks.get('get-todos');
const result = await handler!(null, { priority: 'urgent' });
@@ -305,7 +305,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return empty array', async () => {
- (mockContext.dbManager?.getTodos as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).getTodos = mock(() => {
throw new Error('Query error');
});
@@ -333,7 +333,7 @@ describe('TodosHandlers', () => {
updatedAt: '2024-01-01T00:00:00Z',
};
- (mockContext.dbManager?.getTodo as jest.Mock).mockReturnValue(mockTodo);
+ (mockContext.dbManager as any).getTodo = mock(() => mockTodo);
const handler = handlerCallbacks.get('get-todo-by-id');
const result = await handler!(null, 1);
@@ -343,7 +343,7 @@ describe('TodosHandlers', () => {
});
it('should return null when todo not found', async () => {
- (mockContext.dbManager?.getTodo as jest.Mock).mockReturnValue(null);
+ (mockContext.dbManager as any).getTodo = mock(() => null);
const handler = handlerCallbacks.get('get-todo-by-id');
const result = await handler!(null, 999);
@@ -362,7 +362,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return null', async () => {
- (mockContext.dbManager?.getTodo as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).getTodo = mock(() => {
throw new Error('Query error');
});
@@ -389,7 +389,7 @@ describe('TodosHandlers', () => {
avgCompletionTime: 120,
};
- (mockContext.dbManager?.getTodoStats as jest.Mock).mockReturnValue(mockStats);
+ (mockContext.dbManager as any).getTodoStats = mock(() => mockStats);
const handler = handlerCallbacks.get('get-todo-stats');
const result = await handler!(null);
@@ -416,7 +416,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return default stats', async () => {
- (mockContext.dbManager?.getTodoStats as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).getTodoStats = mock(() => {
throw new Error('Stats error');
});
@@ -455,7 +455,7 @@ describe('TodosHandlers', () => {
},
];
- (mockContext.dbManager?.getTodosWithCategory as jest.Mock).mockReturnValue(mockTodosWithCategory);
+ (mockContext.dbManager as any).getTodosWithCategory = mock(() => mockTodosWithCategory);
const handler = handlerCallbacks.get('get-todos-with-category');
const result = await handler!(null);
@@ -475,7 +475,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return empty array', async () => {
- (mockContext.dbManager?.getTodosWithCategory as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).getTodosWithCategory = mock(() => {
throw new Error('Query error');
});
@@ -493,7 +493,7 @@ describe('TodosHandlers', () => {
});
it('should link todo to time entry successfully', async () => {
- (mockContext.dbManager?.linkTodoToTimeEntry as jest.Mock).mockReturnValue(true);
+ (mockContext.dbManager as any).linkTodoToTimeEntry = mock(() => true);
const handler = handlerCallbacks.get('link-todo-time-entry');
const result = await handler!(null, 1, 100);
@@ -504,7 +504,7 @@ describe('TodosHandlers', () => {
});
it('should return false when linking fails', async () => {
- (mockContext.dbManager?.linkTodoToTimeEntry as jest.Mock).mockReturnValue(false);
+ (mockContext.dbManager as any).linkTodoToTimeEntry = mock(() => false);
const handler = handlerCallbacks.get('link-todo-time-entry');
const result = await handler!(null, 999, 100);
@@ -523,7 +523,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return false', async () => {
- (mockContext.dbManager?.linkTodoToTimeEntry as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).linkTodoToTimeEntry = mock(() => {
throw new Error('Link error');
});
@@ -541,7 +541,7 @@ describe('TodosHandlers', () => {
});
it('should increment pomodoro count successfully', async () => {
- (mockContext.dbManager?.incrementTodoPomodoro as jest.Mock).mockReturnValue(true);
+ (mockContext.dbManager as any).incrementTodoPomodoro = mock(() => true);
const handler = handlerCallbacks.get('increment-todo-pomodoro');
const result = await handler!(null, 1);
@@ -552,7 +552,7 @@ describe('TodosHandlers', () => {
});
it('should return false when increment fails', async () => {
- (mockContext.dbManager?.incrementTodoPomodoro as jest.Mock).mockReturnValue(false);
+ (mockContext.dbManager as any).incrementTodoPomodoro = mock(() => false);
const handler = handlerCallbacks.get('increment-todo-pomodoro');
const result = await handler!(null, 999);
@@ -571,7 +571,7 @@ describe('TodosHandlers', () => {
});
it('should handle errors and return false', async () => {
- (mockContext.dbManager?.incrementTodoPomodoro as jest.Mock).mockImplementation(() => {
+ (mockContext.dbManager as any).incrementTodoPomodoro = mock(() => {
throw new Error('Increment error');
});
diff --git a/src/main/services/__tests__/ServiceContainer.test.ts b/src/main/services/__tests__/ServiceContainer.test.ts
index 0df86e8..fc832c3 100644
--- a/src/main/services/__tests__/ServiceContainer.test.ts
+++ b/src/main/services/__tests__/ServiceContainer.test.ts
@@ -1,16 +1,39 @@
-import { ServiceContainer } from '../ServiceContainer';
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { Settings } from '@/types';
// Mock all service dependencies
-jest.mock('../../../database/DatabaseManager');
-jest.mock('../../../services/activity/ActivityTrackingService');
-jest.mock('../../../services/pomodoro/PomodoroService');
-jest.mock('../../../services/notifications/NotificationService');
-jest.mock('../../../services/goals/GoalsService');
-jest.mock('../../../services/categories/CategoriesService');
-jest.mock('../../../services/activity/ActivityValidationService');
-jest.mock('../../../services/activity/ActivityMergeService');
+mock.module('../../../database/DatabaseManager', () => ({
+ DatabaseManager: mock(() => ({ initialize: mock(() => {}) })),
+}));
+mock.module('../../../services/activity/ActivityTrackingService', () => ({
+ ActivityTrackingService: mock(() => ({
+ updateSettings: mock(() => {}),
+ isTracking: mock(() => false),
+ stop: mock(() => Promise.resolve()),
+ })),
+}));
+mock.module('../../../services/pomodoro/PomodoroService', () => ({
+ PomodoroService: mock(() => ({ destroy: mock(() => {}) })),
+}));
+mock.module('../../../services/notifications/NotificationService', () => ({
+ NotificationService: mock(() => ({ updateSettings: mock(() => {}) })),
+}));
+mock.module('../../../services/goals/GoalsService', () => ({
+ GoalsService: mock(() => ({ setNotificationService: mock(() => {}) })),
+}));
+mock.module('../../../services/categories/CategoriesService', () => ({
+ CategoriesService: mock(() => ({
+ initializeDefaultCategories: mock(() => Promise.resolve()),
+ })),
+}));
+mock.module('../../../services/activity/ActivityValidationService', () => ({
+ ActivityValidationService: mock(() => ({})),
+}));
+mock.module('../../../services/activity/ActivityMergeService', () => ({
+ ActivityMergeService: mock(() => ({})),
+}));
+import { ServiceContainer } from '../ServiceContainer';
import { DatabaseManager } from '@/database/DatabaseManager';
import { ActivityTrackingService } from '@/services/activity/ActivityTrackingService';
import { PomodoroService } from '@/services/pomodoro/PomodoroService';
@@ -22,9 +45,9 @@ import { ActivityMergeService } from '@/services/activity/ActivityMergeService';
describe('ServiceContainer', () => {
let container: ServiceContainer;
- let consoleLog: jest.SpyInstance;
- let consoleError: jest.SpyInstance;
- let consoleWarn: jest.SpyInstance;
+ let consoleLog: ReturnType;
+ let consoleError: ReturnType;
+ let consoleWarn: ReturnType;
const mockSettings: Settings = {
pomodoro: {
@@ -53,52 +76,49 @@ describe('ServiceContainer', () => {
};
beforeEach(() => {
- jest.clearAllMocks();
container = new ServiceContainer();
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
- consoleError = jest.spyOn(console, 'error').mockImplementation();
- consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
+ consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ consoleWarn = spyOn(console, 'warn').mockImplementation(() => {});
// Mock DatabaseManager
- (DatabaseManager as jest.MockedClass).mockImplementation(() => ({
- initialize: jest.fn(),
- } as any));
+ (DatabaseManager as any) = mock(() => ({
+ initialize: mock(() => {}),
+ }));
// Mock NotificationService
- (NotificationService as jest.MockedClass).mockImplementation(() => ({
- updateSettings: jest.fn(),
- } as any));
+ (NotificationService as any) = mock(() => ({
+ updateSettings: mock(() => {}),
+ }));
// Mock GoalsService
- (GoalsService as jest.MockedClass).mockImplementation(() => ({
- setNotificationService: jest.fn(),
- } as any));
+ (GoalsService as any) = mock(() => ({
+ setNotificationService: mock(() => {}),
+ }));
// Mock CategoriesService with async method
- (CategoriesService as jest.MockedClass).mockImplementation(() => ({
- initializeDefaultCategories: jest.fn().mockResolvedValue(undefined),
- } as any));
+ (CategoriesService as any) = mock(() => ({
+ initializeDefaultCategories: mock(() => Promise.resolve()),
+ }));
// Mock ActivityValidationService
- (ActivityValidationService as jest.MockedClass).mockImplementation(() => ({
- } as any));
+ (ActivityValidationService as any) = mock(() => ({}));
// Mock ActivityMergeService
- (ActivityMergeService as jest.MockedClass).mockImplementation(() => ({
- } as any));
+ (ActivityMergeService as any) = mock(() => ({}));
// Mock ActivityTrackingService
- (ActivityTrackingService as jest.MockedClass).mockImplementation(() => ({
- updateSettings: jest.fn(),
- isTracking: jest.fn().mockReturnValue(false),
- stop: jest.fn().mockResolvedValue(undefined),
- } as any));
+ (ActivityTrackingService as any) = mock(() => ({
+ updateSettings: mock(() => {}),
+ isTracking: mock(() => false),
+ stop: mock(() => Promise.resolve()),
+ }));
// Mock PomodoroService
- (PomodoroService as jest.MockedClass).mockImplementation(() => ({
- destroy: jest.fn(),
- } as any));
+ (PomodoroService as any) = mock(() => ({
+ destroy: mock(() => {}),
+ }));
});
afterEach(() => {
@@ -233,7 +253,7 @@ describe('ServiceContainer', () => {
describe('Error Handling', () => {
it('should handle database initialization failure', async () => {
- (DatabaseManager as jest.MockedClass).mockImplementation(() => {
+ (DatabaseManager as any) = mock(() => {
throw new Error('Database connection failed');
});
@@ -245,7 +265,7 @@ describe('ServiceContainer', () => {
});
it('should handle notification service initialization failure', async () => {
- (NotificationService as jest.MockedClass).mockImplementation(() => {
+ (NotificationService as any) = mock(() => {
throw new Error('Notification service error');
});
@@ -254,7 +274,7 @@ describe('ServiceContainer', () => {
});
it('should handle categories service initialization failure', async () => {
- (CategoriesService as jest.MockedClass).mockImplementation(() => {
+ (CategoriesService as any) = mock(() => {
throw new Error('Categories service error');
});
@@ -285,8 +305,8 @@ describe('ServiceContainer', () => {
it('should stop activity tracking if running during cleanup', async () => {
await container.initialize('/test/path', mockSettings);
- const activityTracker = container.getActivityTrackingService() as jest.Mocked;
- activityTracker.isTracking = jest.fn().mockReturnValue(true);
+ const activityTracker = container.getActivityTrackingService() as any;
+ activityTracker.isTracking = mock(() => true);
await container.cleanup();
@@ -296,7 +316,7 @@ describe('ServiceContainer', () => {
it('should destroy pomodoro service during cleanup', async () => {
await container.initialize('/test/path', mockSettings);
- const pomodoroService = container.getPomodoroService() as jest.Mocked;
+ const pomodoroService = container.getPomodoroService() as any;
await container.cleanup();
@@ -317,9 +337,9 @@ describe('ServiceContainer', () => {
it('should handle cleanup errors gracefully', async () => {
await container.initialize('/test/path', mockSettings);
- const activityTracker = container.getActivityTrackingService() as jest.Mocked;
- activityTracker.isTracking = jest.fn().mockReturnValue(true); // Make it return true
- activityTracker.stop = jest.fn().mockRejectedValue(new Error('Stop failed'));
+ const activityTracker = container.getActivityTrackingService() as any;
+ activityTracker.isTracking = mock(() => true); // Make it return true
+ activityTracker.stop = mock(() => Promise.reject(new Error('Stop failed')));
await expect(container.cleanup()).rejects.toThrow('Stop failed');
expect(consoleError).toHaveBeenCalledWith(
@@ -428,7 +448,7 @@ describe('ServiceContainer', () => {
it('should call initializeDefaultCategories asynchronously', async () => {
await container.initialize('/test/path', mockSettings);
- const categoriesService = container.getCategoriesService() as jest.Mocked;
+ const categoriesService = container.getCategoriesService() as any;
// Give async operation time to complete
await new Promise(resolve => setTimeout(resolve, 10));
@@ -437,9 +457,9 @@ describe('ServiceContainer', () => {
});
it('should handle default categories initialization failure gracefully', async () => {
- (CategoriesService as jest.MockedClass).mockImplementation(() => ({
- initializeDefaultCategories: jest.fn().mockRejectedValue(new Error('Init failed')),
- } as any));
+ (CategoriesService as any) = mock(() => ({
+ initializeDefaultCategories: mock(() => Promise.reject(new Error('Init failed'))),
+ }));
await container.initialize('/test/path', mockSettings);
diff --git a/src/services/activity/__tests__/ActivityMonitor.test.ts b/src/services/activity/__tests__/ActivityMonitor.test.ts
index 0383dfb..a7daba2 100644
--- a/src/services/activity/__tests__/ActivityMonitor.test.ts
+++ b/src/services/activity/__tests__/ActivityMonitor.test.ts
@@ -1,31 +1,28 @@
-import { ActivityMonitor } from '../ActivityMonitor';
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { exec } from 'child_process';
// Mock child_process with proper callback-style exec
-jest.mock('child_process', () => ({
- exec: jest.fn(),
+const mockExec = mock(() => {});
+mock.module('child_process', () => ({
+ exec: mockExec,
}));
+import { ActivityMonitor } from '../ActivityMonitor';
+
describe('ActivityMonitor', () => {
let monitor: ActivityMonitor;
- let consoleLog: jest.SpyInstance;
- let consoleError: jest.SpyInstance;
- let mockExec: jest.Mock;
+ let consoleLog: ReturnType;
+ let consoleError: ReturnType;
beforeEach(() => {
- jest.clearAllMocks();
- jest.useFakeTimers();
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
- consoleError = jest.spyOn(console, 'error').mockImplementation();
-
- mockExec = exec as unknown as jest.Mock;
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
+ consoleError = spyOn(console, 'error').mockImplementation(() => {});
monitor = new ActivityMonitor(5000);
});
afterEach(() => {
monitor.stop();
- jest.useRealTimers();
consoleLog.mockRestore();
consoleError.mockRestore();
});
@@ -165,14 +162,14 @@ describe('ActivityMonitor', () => {
// Helper function to mock exec with callback style
const mockExecSuccess = (stdout: string) => {
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
// Call callback synchronously for test simplicity
callback(null, { stdout, stderr: '' });
});
};
const mockExecError = (error: Error) => {
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
// Call callback synchronously for test simplicity
callback(error, { stdout: '', stderr: '' });
});
@@ -210,7 +207,7 @@ describe('ActivityMonitor', () => {
it('should detect browser and attempt URL extraction', async () => {
let callCount = 0;
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
callCount++;
if (callCount === 1) {
callback(null, { stdout: 'Google Chrome|||Example Page', stderr: '' });
@@ -248,7 +245,7 @@ describe('ActivityMonitor', () => {
it('should skip internal browser pages', async () => {
let callCount = 0;
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
callCount++;
if (callCount === 1) {
callback(null, { stdout: 'Google Chrome|||New Tab', stderr: '' });
@@ -272,7 +269,7 @@ describe('ActivityMonitor', () => {
it('should handle Chrome browser URL extraction', async () => {
let callCount = 0;
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
callCount++;
if (callCount === 1) {
callback(null, { stdout: 'Google Chrome|||Example', stderr: '' });
@@ -299,7 +296,7 @@ describe('ActivityMonitor', () => {
it('should handle Safari browser', async () => {
let callCount = 0;
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
callCount++;
if (callCount === 1) {
callback(null, { stdout: 'Safari|||Test Page', stderr: '' });
@@ -351,7 +348,7 @@ describe('ActivityMonitor', () => {
describe('Error Handling', () => {
const mockExecError = (error: Error) => {
- mockExec.mockImplementation((_cmd: string, callback: any) => {
+ (mockExec as any) = mock((_cmd: string, callback: any) => {
callback(error, { stdout: '', stderr: '' });
});
};
@@ -390,7 +387,7 @@ describe('ActivityMonitor', () => {
it('should cleanup and stop tracking when stop() is called during capture', async () => {
// Simulate a long-running capture by not calling the callback
- mockExec.mockImplementation((_cmd: string, _callback: any) => {
+ (mockExec as any) = mock((_cmd: string, _callback: any) => {
// Never call callback
});
diff --git a/src/services/activity/__tests__/ActivityTrackingService.test.ts b/src/services/activity/__tests__/ActivityTrackingService.test.ts
index ea8e70c..a685391 100644
--- a/src/services/activity/__tests__/ActivityTrackingService.test.ts
+++ b/src/services/activity/__tests__/ActivityTrackingService.test.ts
@@ -1,52 +1,56 @@
-import { ActivityTrackingService } from '../ActivityTrackingService';
-import { ActivityMonitor } from '../ActivityMonitor';
-import { DatabaseManager } from '../../../database/DatabaseManager';
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { CurrentActivity, ActivitySession } from '../../../types/activity';
+// Create mock for ActivityMonitor
+let mockMonitor: any;
+
// Mock ActivityMonitor
-jest.mock('../ActivityMonitor');
+mock.module('../ActivityMonitor', () => ({
+ ActivityMonitor: mock(() => mockMonitor),
+}));
// Mock DatabaseManager
-jest.mock('../../../database/DatabaseManager');
+mock.module('../../../database/DatabaseManager', () => ({
+ DatabaseManager: mock(() => ({})),
+}));
+
+import { ActivityTrackingService } from '../ActivityTrackingService';
+import { ActivityMonitor } from '../ActivityMonitor';
+import { DatabaseManager } from '../../../database/DatabaseManager';
describe('ActivityTrackingService', () => {
let service: ActivityTrackingService;
- let mockDbManager: jest.Mocked;
- let mockMonitor: jest.Mocked;
- let consoleLog: jest.SpyInstance;
- let consoleError: jest.SpyInstance;
+ let mockDbManager: any;
+ let consoleLog: ReturnType;
+ let consoleError: ReturnType;
beforeEach(() => {
- jest.clearAllMocks();
- jest.useFakeTimers();
-
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
- consoleError = jest.spyOn(console, 'error').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
+ consoleError = spyOn(console, 'error').mockImplementation(() => {});
// Create mock database manager
mockDbManager = {
- addActivitySession: jest.fn().mockReturnValue(123),
- getActivitySessions: jest.fn().mockResolvedValue([]),
- getTopApplications: jest.fn().mockResolvedValue([]),
- getTopWebsites: jest.fn().mockResolvedValue([]),
- } as any;
+ addActivitySession: mock(() => 123),
+ getActivitySessions: mock(() => Promise.resolve([])),
+ getTopApplications: mock(() => Promise.resolve([])),
+ getTopWebsites: mock(() => Promise.resolve([])),
+ };
// Create mock monitor
mockMonitor = {
- start: jest.fn(),
- stop: jest.fn(),
- isTracking: jest.fn().mockReturnValue(false),
- getCurrentActivity: jest.fn().mockResolvedValue(null),
- setInterval: jest.fn(),
- } as any;
+ start: mock(() => {}),
+ stop: mock(() => {}),
+ isTracking: mock(() => false),
+ getCurrentActivity: mock(() => Promise.resolve(null)),
+ setInterval: mock(() => {}),
+ };
- (ActivityMonitor as jest.Mock).mockImplementation(() => mockMonitor);
+ (ActivityMonitor as any) = mock(() => mockMonitor);
service = new ActivityTrackingService(mockDbManager);
});
afterEach(() => {
- jest.useRealTimers();
consoleLog.mockRestore();
consoleError.mockRestore();
});
@@ -86,7 +90,7 @@ describe('ActivityTrackingService', () => {
});
it('should stop tracking if enabled is set to false when currently tracking', () => {
- mockMonitor.isTracking.mockReturnValue(true);
+ mockMonitor.isTracking = mock(() => true);
service.updateSettings({ enabled: false });
@@ -94,7 +98,7 @@ describe('ActivityTrackingService', () => {
});
it('should update monitor interval if tracking is active', () => {
- mockMonitor.isTracking.mockReturnValue(true);
+ mockMonitor.isTracking = mock(() => true);
service.updateSettings({ trackingInterval: 45 });
@@ -367,7 +371,7 @@ describe('ActivityTrackingService', () => {
});
it('should handle database errors gracefully', async () => {
- mockDbManager.addActivitySession.mockImplementation(() => {
+ mockDbManager.addActivitySession = mock(() => {
throw new Error('Database error');
});
@@ -562,7 +566,7 @@ describe('ActivityTrackingService', () => {
});
it('isTracking should return monitor tracking status', () => {
- mockMonitor.isTracking.mockReturnValue(true);
+ mockMonitor.isTracking = mock(() => true);
expect(service.isTracking()).toBe(true);
expect(mockMonitor.isTracking).toHaveBeenCalled();
diff --git a/src/services/goals/__tests__/GoalsService.test.ts b/src/services/goals/__tests__/GoalsService.test.ts
index 2f9aa5b..3734a61 100644
--- a/src/services/goals/__tests__/GoalsService.test.ts
+++ b/src/services/goals/__tests__/GoalsService.test.ts
@@ -1,37 +1,33 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { GoalsService } from '../GoalsService';
import { ProductivityGoal, GoalWithProgress } from '../../../types';
-// Mock DatabaseManager
-jest.mock('../../../database/DatabaseManager');
-jest.mock('../../notifications/NotificationService');
-
describe('GoalsService', () => {
let service: GoalsService;
let mockDb: any;
let mockNotificationService: any;
- let consoleLog: jest.SpyInstance;
+ let consoleLog: ReturnType;
beforeEach(() => {
- jest.clearAllMocks();
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
mockDb = {
- addGoal: jest.fn(),
- updateGoal: jest.fn(),
- deleteGoal: jest.fn(),
- getGoals: jest.fn(),
- updateGoalProgress: jest.fn(),
- getTodayGoalsWithProgress: jest.fn(),
- getGoalProgress: jest.fn(),
- getGoalAchievementHistory: jest.fn(),
- getGoalStats: jest.fn(),
- queryTotalActiveTime: jest.fn(),
- queryCategoryTime: jest.fn(),
- queryAppTime: jest.fn(),
+ addGoal: mock(() => {}),
+ updateGoal: mock(() => {}),
+ deleteGoal: mock(() => {}),
+ getGoals: mock(() => {}),
+ updateGoalProgress: mock(() => {}),
+ getTodayGoalsWithProgress: mock(() => {}),
+ getGoalProgress: mock(() => {}),
+ getGoalAchievementHistory: mock(() => {}),
+ getGoalStats: mock(() => {}),
+ queryTotalActiveTime: mock(() => {}),
+ queryCategoryTime: mock(() => {}),
+ queryAppTime: mock(() => {}),
};
mockNotificationService = {
- showNotification: jest.fn(),
+ showNotification: mock(() => {}),
};
service = new GoalsService(mockDb, mockNotificationService);
@@ -73,7 +69,7 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.addGoal.mockResolvedValue(1);
+ mockDb.addGoal = mock(() => Promise.resolve(1));
const goalId = await service.addGoal(goal);
@@ -96,8 +92,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.addGoal.mockRejectedValue(new Error('Database error'));
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockDb.addGoal = mock(() => Promise.reject(new Error('Database error')));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
await expect(service.addGoal(goal)).rejects.toThrow('Database error');
expect(consoleError).toHaveBeenCalledWith('Failed to add goal:', expect.any(Error));
@@ -108,7 +104,7 @@ describe('GoalsService', () => {
describe('updateGoal', () => {
it('should update an existing goal', async () => {
- mockDb.updateGoal.mockResolvedValue(true);
+ mockDb.updateGoal = mock(() => Promise.resolve(true));
const success = await service.updateGoal(1, { targetMinutes: 300 });
@@ -117,7 +113,7 @@ describe('GoalsService', () => {
});
it('should handle update failure', async () => {
- mockDb.updateGoal.mockResolvedValue(false);
+ mockDb.updateGoal = mock(() => Promise.resolve(false));
const success = await service.updateGoal(999, { targetMinutes: 300 });
@@ -127,7 +123,7 @@ describe('GoalsService', () => {
describe('deleteGoal', () => {
it('should delete a goal and clear notification history', async () => {
- mockDb.deleteGoal.mockResolvedValue(true);
+ mockDb.deleteGoal = mock(() => Promise.resolve(true));
const success = await service.deleteGoal(1);
@@ -136,7 +132,7 @@ describe('GoalsService', () => {
});
it('should handle deletion failure', async () => {
- mockDb.deleteGoal.mockResolvedValue(false);
+ mockDb.deleteGoal = mock(() => Promise.resolve(false));
const success = await service.deleteGoal(999);
@@ -160,7 +156,7 @@ describe('GoalsService', () => {
},
];
- mockDb.getGoals.mockResolvedValue(mockGoals);
+ mockDb.getGoals = mock(() => Promise.resolve(mockGoals));
const goals = await service.getGoals();
@@ -183,7 +179,7 @@ describe('GoalsService', () => {
},
];
- mockDb.getGoals.mockResolvedValue(mockGoals);
+ mockDb.getGoals = mock(() => Promise.resolve(mockGoals));
const goals = await service.getGoals(true);
@@ -220,7 +216,7 @@ describe('GoalsService', () => {
},
];
- mockDb.getTodayGoalsWithProgress.mockResolvedValue(mockGoalsWithProgress);
+ mockDb.getTodayGoalsWithProgress = mock(() => Promise.resolve(mockGoalsWithProgress));
const goals = await service.getTodayGoalsWithProgress();
@@ -230,7 +226,7 @@ describe('GoalsService', () => {
describe('updateGoalProgress', () => {
it('should update progress for a goal', async () => {
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
mockDb.getGoals.mockResolvedValue([
{
id: 1,
@@ -271,11 +267,11 @@ describe('GoalsService', () => {
},
];
- mockDb.getGoals.mockResolvedValue(mockGoals);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve(mockGoals));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
// Mock the database query for calculateTotalActiveTime
- mockDb.queryTotalActiveTime.mockReturnValue(120); // 2 hours = 120 minutes
+ mockDb.queryTotalActiveTime = mock(() => 120); // 2 hours = 120 minutes
await service.recalculateAllGoalProgress();
@@ -297,7 +293,7 @@ describe('GoalsService', () => {
},
];
- mockDb.getGoals.mockResolvedValue(mockGoals);
+ mockDb.getGoals = mock(() => Promise.resolve(mockGoals));
await service.recalculateAllGoalProgress();
@@ -309,9 +305,9 @@ describe('GoalsService', () => {
describe('Progress Calculation', () => {
beforeEach(() => {
// Mock database query methods
- mockDb.queryTotalActiveTime.mockReturnValue(60); // 1 hour = 60 minutes
- mockDb.queryCategoryTime.mockReturnValue(60);
- mockDb.queryAppTime.mockReturnValue(60);
+ mockDb.queryTotalActiveTime = mock(() => 60); // 1 hour = 60 minutes
+ mockDb.queryCategoryTime = mock(() => 60);
+ mockDb.queryAppTime = mock(() => 60);
});
it('should calculate progress for daily_time goal', async () => {
@@ -327,8 +323,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.recalculateAllGoalProgress();
@@ -353,8 +349,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.recalculateAllGoalProgress();
@@ -375,8 +371,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.recalculateAllGoalProgress();
@@ -398,8 +394,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 50,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.updateGoalProgress(1, '2025-01-01', 50); // 50% progress
@@ -419,8 +415,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 50,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
// First update - should notify
await service.updateGoalProgress(1, '2025-01-01', 50);
@@ -444,8 +440,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 50,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.updateGoalProgress(1, '2025-01-01', 50);
@@ -465,8 +461,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.updateGoalProgress(1, '2025-01-01', 100);
@@ -487,8 +483,8 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.updateGoalProgress.mockResolvedValue(undefined);
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.updateGoalProgress = mock(() => Promise.resolve(undefined));
await service.updateGoalProgress(1, '2025-01-01', 40); // Exceeded limit
@@ -601,8 +597,8 @@ describe('GoalsService', () => {
describe('Error Handling', () => {
it('should handle database errors gracefully', async () => {
- mockDb.getGoals.mockRejectedValue(new Error('Database connection failed'));
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockDb.getGoals = mock(() => Promise.reject(new Error('Database connection failed')));
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
await expect(service.getGoals()).rejects.toThrow('Database connection failed');
expect(consoleError).toHaveBeenCalled();
@@ -623,12 +619,12 @@ describe('GoalsService', () => {
notifyAtPercentage: 100,
};
- mockDb.getGoals.mockResolvedValue([goal]);
- mockDb.queryTotalActiveTime.mockImplementation(() => {
+ mockDb.getGoals = mock(() => Promise.resolve([goal]));
+ mockDb.queryTotalActiveTime = mock(() => {
throw new Error('Query failed');
});
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
await service.recalculateAllGoalProgress();
diff --git a/src/services/notifications/__tests__/NotificationService.test.ts b/src/services/notifications/__tests__/NotificationService.test.ts
index e39726d..8f23b5a 100644
--- a/src/services/notifications/__tests__/NotificationService.test.ts
+++ b/src/services/notifications/__tests__/NotificationService.test.ts
@@ -1,24 +1,31 @@
-import { NotificationService } from '../NotificationService';
-import { Notification } from 'electron';
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
+
+// Track mock instances for testing
+let mockNotificationInstances: any[] = [];
+const mockShow = mock(() => {});
// Mock Electron's Notification
-jest.mock('electron', () => ({
- Notification: jest.fn().mockImplementation(function(this: any, options: any) {
+mock.module('electron', () => ({
+ Notification: function(this: any, options: any) {
this.title = options.title;
this.body = options.body;
this.silent = options.silent;
- this.show = jest.fn();
+ this.show = mockShow;
+ mockNotificationInstances.push(this);
return this;
- }),
+ },
}));
+import { NotificationService } from '../NotificationService';
+import { Notification } from 'electron';
+
describe('NotificationService', () => {
let service: NotificationService;
- let consoleLog: jest.SpyInstance;
+ let consoleLog: ReturnType;
beforeEach(() => {
- jest.clearAllMocks();
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
+ mockNotificationInstances = [];
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
@@ -127,7 +134,7 @@ describe('NotificationService', () => {
service = new NotificationService(true, true);
service.notifyFocusComplete('Test Task');
- const mockNotification = (Notification as jest.MockedClass).mock.instances[0]!;
+ const mockNotification = mockNotificationInstances[0]!;
expect(mockNotification.show).toHaveBeenCalled();
});
});
@@ -218,10 +225,12 @@ describe('NotificationService', () => {
describe('Error handling', () => {
it('should handle notification errors gracefully', () => {
- const consoleError = jest.spyOn(console, 'error').mockImplementation();
- (Notification as jest.MockedClass).mockImplementationOnce(() => {
+ const consoleError = spyOn(console, 'error').mockImplementation(() => {});
+ // Override the mock to throw an error
+ const originalNotification = Notification;
+ (Notification as any) = function() {
throw new Error('Notification failed');
- });
+ };
service = new NotificationService(true, true);
service.notifyFocusComplete('Test Task');
diff --git a/src/services/pomodoro/__tests__/PomodoroService.test.ts b/src/services/pomodoro/__tests__/PomodoroService.test.ts
index 6849fa9..ba2d5fb 100644
--- a/src/services/pomodoro/__tests__/PomodoroService.test.ts
+++ b/src/services/pomodoro/__tests__/PomodoroService.test.ts
@@ -1,17 +1,14 @@
+import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { PomodoroService } from '../PomodoroService';
import { DatabaseManager } from '../../../database/DatabaseManager';
import { NotificationService } from '../../notifications/NotificationService';
import { PomodoroSettings } from '../../../types';
-// Mock dependencies
-jest.mock('../../../database/DatabaseManager');
-jest.mock('../../notifications/NotificationService');
-
describe('PomodoroService', () => {
let service: PomodoroService;
- let mockDbManager: jest.Mocked;
- let mockNotificationService: jest.Mocked;
- let consoleLog: jest.SpyInstance;
+ let mockDbManager: any;
+ let mockNotificationService: any;
+ let consoleLog: ReturnType;
const defaultSettings: PomodoroSettings = {
focusDuration: 25,
@@ -26,28 +23,24 @@ describe('PomodoroService', () => {
};
beforeEach(() => {
- jest.clearAllMocks();
- jest.useFakeTimers();
-
- consoleLog = jest.spyOn(console, 'log').mockImplementation();
+ consoleLog = spyOn(console, 'log').mockImplementation(() => {});
let sessionIdCounter = 0;
mockDbManager = {
- addPomodoroSession: jest.fn().mockImplementation(() => ++sessionIdCounter),
- updatePomodoroSession: jest.fn().mockReturnValue(true),
- } as any;
+ addPomodoroSession: mock(() => ++sessionIdCounter),
+ updatePomodoroSession: mock(() => true),
+ };
mockNotificationService = {
- notifyFocusComplete: jest.fn(),
- notifyBreakComplete: jest.fn(),
- updateSettings: jest.fn(),
- } as any;
+ notifyFocusComplete: mock(() => {}),
+ notifyBreakComplete: mock(() => {}),
+ updateSettings: mock(() => {}),
+ };
service = new PomodoroService(mockDbManager, mockNotificationService, defaultSettings);
});
afterEach(() => {
- jest.useRealTimers();
consoleLog.mockRestore();
service.destroy();
});
@@ -168,22 +161,28 @@ describe('PomodoroService', () => {
);
});
- it('should emit start event', (done) => {
- service.on('start', (status) => {
- expect(status.state).toBe('running');
- done();
+ it('should emit start event', async () => {
+ const eventPromise = new Promise((resolve) => {
+ service.on('start', (status) => {
+ expect(status.state).toBe('running');
+ resolve();
+ });
});
service.start('Test Task', 'focus');
+ await eventPromise;
});
- it('should emit stateChange event', (done) => {
- service.on('stateChange', (status) => {
- expect(status.state).toBe('running');
- done();
+ it('should emit stateChange event', async () => {
+ const eventPromise = new Promise((resolve) => {
+ service.on('stateChange', (status) => {
+ expect(status.state).toBe('running');
+ resolve();
+ });
});
service.start('Test Task', 'focus');
+ await eventPromise;
});
it('should log session start', () => {
@@ -209,15 +208,18 @@ describe('PomodoroService', () => {
expect(service.getStatus().state).toBe('idle');
});
- it('should emit pause event', (done) => {
+ it('should emit pause event', async () => {
service.start('Test Task', 'focus');
- service.on('pause', (status) => {
- expect(status.state).toBe('paused');
- done();
+ const eventPromise = new Promise((resolve) => {
+ service.on('pause', (status) => {
+ expect(status.state).toBe('paused');
+ resolve();
+ });
});
service.pause();
+ await eventPromise;
});
});
@@ -236,16 +238,19 @@ describe('PomodoroService', () => {
expect(service.getStatus().state).toBe('idle');
});
- it('should emit resume event', (done) => {
+ it('should emit resume event', async () => {
service.start('Test Task', 'focus');
service.pause();
- service.on('resume', (status) => {
- expect(status.state).toBe('running');
- done();
+ const eventPromise = new Promise((resolve) => {
+ service.on('resume', (status) => {
+ expect(status.state).toBe('running');
+ resolve();
+ });
});
service.resume();
+ await eventPromise;
});
});
@@ -270,15 +275,18 @@ describe('PomodoroService', () => {
);
});
- it('should emit stop event', (done) => {
+ it('should emit stop event', async () => {
service.start('Test Task', 'focus');
- service.on('stop', (status) => {
- expect(status.state).toBe('idle');
- done();
+ const eventPromise = new Promise((resolve) => {
+ service.on('stop', (status) => {
+ expect(status.state).toBe('idle');
+ resolve();
+ });
});
service.stop();
+ await eventPromise;
});
it('should not stop if idle', () => {
@@ -347,14 +355,18 @@ describe('PomodoroService', () => {
expect(service.getStatus().timeRemaining).toBe(initialTime - 1);
});
- it('should emit tick event', (done) => {
+ it('should emit tick event', async () => {
service.start('Test Task', 'focus');
- service.on('tick', () => {
- done();
+ const eventPromise = new Promise((resolve) => {
+ service.on('tick', () => {
+ resolve();
+ });
});
- jest.advanceTimersByTime(1000);
+ // Wait a bit for the tick event
+ await new Promise(resolve => setTimeout(resolve, 1100));
+ await eventPromise;
});
it('should complete session when time reaches zero', () => {
@@ -478,25 +490,25 @@ describe('PomodoroService', () => {
});
describe('destroy', () => {
- it('should stop interval', () => {
+ it('should stop interval', async () => {
service.start('Test Task', 'focus');
service.destroy();
const timeBefore = service.getStatus().timeRemaining;
- jest.advanceTimersByTime(1000);
+ await new Promise(resolve => setTimeout(resolve, 1100));
const timeAfter = service.getStatus().timeRemaining;
// Time should not change after destroy
expect(timeAfter).toBe(timeBefore);
});
- it('should remove all listeners', () => {
- const listener = jest.fn();
+ it('should remove all listeners', async () => {
+ const listener = mock(() => {});
service.on('tick', listener);
service.destroy();
service.start('Test Task', 'focus');
- jest.advanceTimersByTime(1000);
+ await new Promise(resolve => setTimeout(resolve, 1100));
expect(listener).not.toHaveBeenCalled();
});
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 576f0e2..5f5c645 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -1,92 +1,97 @@
+import { mock } from 'bun:test';
+import { GlobalRegistrator } from '@happy-dom/global-registrator';
import '@testing-library/jest-dom';
+// Register happy-dom globals (window, document, etc.)
+GlobalRegistrator.register();
+
// Mock window.matchMedia (required for components that use media queries)
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: mock((query: string) => ({
matches: false,
media: query,
onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
+ addListener: mock(() => {}),
+ removeListener: mock(() => {}),
+ addEventListener: mock(() => {}),
+ removeEventListener: mock(() => {}),
+ dispatchEvent: mock(() => false),
})),
});
// Mock window.electron API (Electron IPC)
const mockElectronAPI = {
// Time entries
- getTimeEntries: jest.fn().mockResolvedValue([]),
- createTimeEntry: jest.fn().mockResolvedValue({ id: 1 }),
- updateTimeEntry: jest.fn().mockResolvedValue(true),
- deleteTimeEntry: jest.fn().mockResolvedValue(true),
+ getTimeEntries: mock(() => Promise.resolve([])),
+ createTimeEntry: mock(() => Promise.resolve({ id: 1 })),
+ updateTimeEntry: mock(() => Promise.resolve(true)),
+ deleteTimeEntry: mock(() => Promise.resolve(true)),
// App usage
- getAppUsage: jest.fn().mockResolvedValue([]),
+ getAppUsage: mock(() => Promise.resolve([])),
// Stats
- getStats: jest.fn().mockResolvedValue({
+ getStats: mock(() => Promise.resolve({
totalTime: 0,
tasksCompleted: 0,
activeTask: null,
- }),
+ })),
// Activity tracking
- startTracking: jest.fn().mockResolvedValue(true),
- stopTracking: jest.fn().mockResolvedValue(true),
- getCurrentActivity: jest.fn().mockResolvedValue(null),
+ startTracking: mock(() => Promise.resolve(true)),
+ stopTracking: mock(() => Promise.resolve(true)),
+ getCurrentActivity: mock(() => Promise.resolve(null)),
// Categories
- getCategories: jest.fn().mockResolvedValue([]),
- createCategory: jest.fn().mockResolvedValue({ id: 1 }),
- updateCategory: jest.fn().mockResolvedValue(true),
- deleteCategory: jest.fn().mockResolvedValue(true),
+ getCategories: mock(() => Promise.resolve([])),
+ createCategory: mock(() => Promise.resolve({ id: 1 })),
+ updateCategory: mock(() => Promise.resolve(true)),
+ deleteCategory: mock(() => Promise.resolve(true)),
// Tags
- getTags: jest.fn().mockResolvedValue([]),
- createTag: jest.fn().mockResolvedValue({ id: 1 }),
- updateTag: jest.fn().mockResolvedValue(true),
- deleteTag: jest.fn().mockResolvedValue(true),
+ getTags: mock(() => Promise.resolve([])),
+ createTag: mock(() => Promise.resolve({ id: 1 })),
+ updateTag: mock(() => Promise.resolve(true)),
+ deleteTag: mock(() => Promise.resolve(true)),
// Pomodoro
- getPomodoroSessions: jest.fn().mockResolvedValue([]),
- createPomodoroSession: jest.fn().mockResolvedValue({ id: 1 }),
+ getPomodoroSessions: mock(() => Promise.resolve([])),
+ createPomodoroSession: mock(() => Promise.resolve({ id: 1 })),
// Goals
- getGoals: jest.fn().mockResolvedValue([]),
- createGoal: jest.fn().mockResolvedValue({ id: 1 }),
- updateGoal: jest.fn().mockResolvedValue(true),
- deleteGoal: jest.fn().mockResolvedValue(true),
+ getGoals: mock(() => Promise.resolve([])),
+ createGoal: mock(() => Promise.resolve({ id: 1 })),
+ updateGoal: mock(() => Promise.resolve(true)),
+ deleteGoal: mock(() => Promise.resolve(true)),
// Todos
todos: {
- getAll: jest.fn().mockResolvedValue([]),
- getById: jest.fn().mockResolvedValue(null),
- add: jest.fn().mockResolvedValue(1),
- update: jest.fn().mockResolvedValue(true),
- delete: jest.fn().mockResolvedValue(true),
- getStats: jest.fn().mockResolvedValue({
+ getAll: mock(() => Promise.resolve([])),
+ getById: mock(() => Promise.resolve(null)),
+ add: mock(() => Promise.resolve(1)),
+ update: mock(() => Promise.resolve(true)),
+ delete: mock(() => Promise.resolve(true)),
+ getStats: mock(() => Promise.resolve({
totalTodos: 0,
completedTodos: 0,
inProgressTodos: 0,
overdueTodos: 0,
completionRate: 0,
avgCompletionTime: 0,
- }),
- getWithCategory: jest.fn().mockResolvedValue([]),
- linkToTimeEntry: jest.fn().mockResolvedValue(true),
- incrementPomodoro: jest.fn().mockResolvedValue(true),
+ })),
+ getWithCategory: mock(() => Promise.resolve([])),
+ linkToTimeEntry: mock(() => Promise.resolve(true)),
+ incrementPomodoro: mock(() => Promise.resolve(true)),
},
// App info
- getAppVersion: jest.fn().mockResolvedValue('2.5.4'),
+ getAppVersion: mock(() => Promise.resolve('2.5.4')),
// IPC listeners
- onTimeEntryUpdate: jest.fn(),
- onActivityUpdate: jest.fn(),
- removeAllListeners: jest.fn(),
+ onTimeEntryUpdate: mock(() => {}),
+ onActivityUpdate: mock(() => {}),
+ removeAllListeners: mock(() => {}),
};
(window as any).electron = mockElectronAPI;
@@ -107,4 +112,4 @@ global.ResizeObserver = class ResizeObserver {
disconnect() {}
observe() {}
unobserve() {}
-} as unknown as typeof ResizeObserver;
\ No newline at end of file
+} as unknown as typeof ResizeObserver;