feat: Add user statuses and cards
This commit is contained in:
124
electron/idle/idle-monitor.spec.ts
Normal file
124
electron/idle/idle-monitor.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'vitest';
|
||||
|
||||
// Mock Electron modules before importing the module under test
|
||||
const mockGetSystemIdleTime = vi.fn(() => 0);
|
||||
const mockSend = vi.fn();
|
||||
const mockGetMainWindow = vi.fn(() => ({
|
||||
isDestroyed: () => false,
|
||||
webContents: { send: mockSend }
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
powerMonitor: {
|
||||
getSystemIdleTime: mockGetSystemIdleTime
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../window/create-window', () => ({
|
||||
getMainWindow: mockGetMainWindow
|
||||
}));
|
||||
|
||||
import {
|
||||
startIdleMonitor,
|
||||
stopIdleMonitor,
|
||||
getIdleState
|
||||
} from './idle-monitor';
|
||||
|
||||
describe('idle-monitor', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockGetSystemIdleTime.mockReturnValue(0);
|
||||
mockSend.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stopIdleMonitor();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns active when idle time is below threshold', () => {
|
||||
mockGetSystemIdleTime.mockReturnValue(0);
|
||||
expect(getIdleState()).toBe('active');
|
||||
});
|
||||
|
||||
it('returns idle when idle time exceeds 15 minutes', () => {
|
||||
mockGetSystemIdleTime.mockReturnValue(15 * 60);
|
||||
expect(getIdleState()).toBe('idle');
|
||||
});
|
||||
|
||||
it('sends idle-state-changed to renderer when transitioning to idle', () => {
|
||||
startIdleMonitor();
|
||||
|
||||
mockGetSystemIdleTime.mockReturnValue(15 * 60);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith('idle-state-changed', 'idle');
|
||||
});
|
||||
|
||||
it('sends idle-state-changed to renderer when transitioning back to active', () => {
|
||||
startIdleMonitor();
|
||||
|
||||
// Go idle
|
||||
mockGetSystemIdleTime.mockReturnValue(15 * 60);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
mockSend.mockClear();
|
||||
|
||||
// Go active
|
||||
mockGetSystemIdleTime.mockReturnValue(5);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith('idle-state-changed', 'active');
|
||||
});
|
||||
|
||||
it('does not fire duplicates when state stays the same', () => {
|
||||
startIdleMonitor();
|
||||
|
||||
mockGetSystemIdleTime.mockReturnValue(15 * 60);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
|
||||
// Only one transition, so only one call
|
||||
const idleCalls = mockSend.mock.calls.filter(
|
||||
([channel, state]: [string, string]) => channel === 'idle-state-changed' && state === 'idle'
|
||||
);
|
||||
|
||||
expect(idleCalls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('stops polling after stopIdleMonitor', () => {
|
||||
startIdleMonitor();
|
||||
|
||||
mockGetSystemIdleTime.mockReturnValue(15 * 60);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
mockSend.mockClear();
|
||||
|
||||
stopIdleMonitor();
|
||||
|
||||
mockGetSystemIdleTime.mockReturnValue(0);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not notify when main window is null', () => {
|
||||
mockGetMainWindow.mockReturnValue(null);
|
||||
startIdleMonitor();
|
||||
|
||||
mockGetSystemIdleTime.mockReturnValue(15 * 60);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
mockGetMainWindow.mockReturnValue({
|
||||
isDestroyed: () => false,
|
||||
webContents: { send: mockSend }
|
||||
});
|
||||
});
|
||||
});
|
||||
49
electron/idle/idle-monitor.ts
Normal file
49
electron/idle/idle-monitor.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { powerMonitor } from 'electron';
|
||||
import { getMainWindow } from '../window/create-window';
|
||||
|
||||
const IDLE_THRESHOLD_SECONDS = 15 * 60; // 15 minutes
|
||||
const POLL_INTERVAL_MS = 10_000; // Check every 10 seconds
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let wasIdle = false;
|
||||
|
||||
const IDLE_STATE_CHANGED_CHANNEL = 'idle-state-changed';
|
||||
|
||||
export type IdleState = 'active' | 'idle';
|
||||
|
||||
/**
|
||||
* Starts polling `powerMonitor.getSystemIdleTime()` and notifies the
|
||||
* renderer whenever the user transitions between active and idle.
|
||||
*/
|
||||
export function startIdleMonitor(): void {
|
||||
if (pollTimer)
|
||||
return;
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
const idleSeconds = powerMonitor.getSystemIdleTime();
|
||||
const isIdle = idleSeconds >= IDLE_THRESHOLD_SECONDS;
|
||||
|
||||
if (isIdle !== wasIdle) {
|
||||
wasIdle = isIdle;
|
||||
const state: IdleState = isIdle ? 'idle' : 'active';
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IDLE_STATE_CHANGED_CHANNEL, state);
|
||||
}
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
export function stopIdleMonitor(): void {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getIdleState(): IdleState {
|
||||
const idleSeconds = powerMonitor.getSystemIdleTime();
|
||||
|
||||
return idleSeconds >= IDLE_THRESHOLD_SECONDS ? 'idle' : 'active';
|
||||
}
|
||||
Reference in New Issue
Block a user