test(electron): add sqlite ipc contract coverage

This commit is contained in:
4gray committed 2026-05-22 03:42:01 +03:00
1 parent ff9c13058c
commit 6cf730ddf6
2 files changed
+929

No files matched your search

@@ -0,0 +1,467 @@
import {
DB_WORKER_OPERATIONS,
DbWorkerOperation,
} from '../workers/database-worker.types';
type ExposedElectronApi = Record<string, (...args: unknown[]) => unknown>;
type MockIpcRenderer = {
invoke: jest.Mock;
on: jest.Mock;
off: jest.Mock;
send: jest.Mock;
};
type PreloadInvokeCase = {
method: string;
args: unknown[];
channel: string;
forwardedArgs: unknown[];
};
let mockExposedApi: ExposedElectronApi | null;
let mockIpcRenderer: MockIpcRenderer;
let mockGetPathForFile: jest.Mock;
const playlistId = 'playlist-1';
const operationId = 'operation-1';
const playlist = { id: playlistId, name: 'Playlist', type: 'xtream' };
const playlists = [playlist];
const playlistUpdates = { name: 'Updated playlist' };
const categories = [{ category_id: '10', category_name: 'Live' }];
const streams = [{ stream_id: 42, name: 'Channel' }];
const favorites = [{ contentId: 1, playlistId }];
const recentlyViewed = [{ contentId: 2, playlistId }];
const categoryIds = [10, 11];
const reorderUpdates = [{ content_id: 12, position: 1 }];
const recentItemsBatch = [{ contentId: 13, playlistId }];
const playbackData = {
contentXtreamId: 42,
contentType: 'vod',
positionSeconds: 120,
};
const dbPreloadCases: PreloadInvokeCase[] = [
{
method: 'dbCreatePlaylist',
args: [playlist],
channel: 'DB_CREATE_PLAYLIST',
forwardedArgs: [playlist],
},
{
method: 'dbGetPlaylist',
args: [playlistId],
channel: 'DB_GET_PLAYLIST',
forwardedArgs: [playlistId],
},
{
method: 'dbUpsertAppPlaylist',
args: [playlist],
channel: 'DB_UPSERT_APP_PLAYLIST',
forwardedArgs: [playlist],
},
{
method: 'dbUpsertAppPlaylists',
args: [playlists],
channel: 'DB_UPSERT_APP_PLAYLISTS',
forwardedArgs: [playlists],
},
{
method: 'dbGetAppPlaylists',
args: [],
channel: 'DB_GET_APP_PLAYLISTS',
forwardedArgs: [],
},
{
method: 'dbGetAppPlaylist',
args: [playlistId],
channel: 'DB_GET_APP_PLAYLIST',
forwardedArgs: [playlistId],
},
{
method: 'dbUpdatePlaylist',
args: [playlistId, playlistUpdates],
channel: 'DB_UPDATE_PLAYLIST',
forwardedArgs: [playlistId, playlistUpdates],
},
{
method: 'dbDeletePlaylist',
args: [playlistId, operationId],
channel: 'DB_DELETE_PLAYLIST',
forwardedArgs: [playlistId, operationId],
},
{
method: 'dbDeleteXtreamContent',
args: [playlistId, operationId],
channel: 'DB_DELETE_XTREAM_CONTENT',
forwardedArgs: [playlistId, operationId],
},
{
method: 'dbRestoreXtreamUserData',
args: [playlistId, favorites, recentlyViewed, operationId],
channel: 'DB_RESTORE_XTREAM_USER_DATA',
forwardedArgs: [playlistId, favorites, recentlyViewed, operationId],
},
{
method: 'dbHasCategories',
args: [playlistId, 'live'],
channel: 'DB_HAS_CATEGORIES',
forwardedArgs: [playlistId, 'live'],
},
{
method: 'dbGetCategories',
args: [playlistId, 'live'],
channel: 'DB_GET_CATEGORIES',
forwardedArgs: [playlistId, 'live'],
},
{
method: 'dbSaveCategories',
args: [playlistId, categories, 'live', categoryIds],
channel: 'DB_SAVE_CATEGORIES',
forwardedArgs: [playlistId, categories, 'live', categoryIds],
},
{
method: 'dbGetAllCategories',
args: [playlistId, 'live'],
channel: 'DB_GET_ALL_CATEGORIES',
forwardedArgs: [playlistId, 'live'],
},
{
method: 'dbUpdateCategoryVisibility',
args: [categoryIds, true],
channel: 'DB_UPDATE_CATEGORY_VISIBILITY',
forwardedArgs: [categoryIds, true],
},
{
method: 'dbHasContent',
args: [playlistId, 'movie'],
channel: 'DB_HAS_CONTENT',
forwardedArgs: [playlistId, 'movie'],
},
{
method: 'dbGetContent',
args: [playlistId, 'movie'],
channel: 'DB_GET_CONTENT',
forwardedArgs: [playlistId, 'movie'],
},
{
method: 'dbSaveContent',
args: [playlistId, streams, 'movie', operationId],
channel: 'DB_SAVE_CONTENT',
forwardedArgs: [playlistId, streams, 'movie', operationId],
},
{
method: 'dbClearXtreamImportCache',
args: [playlistId, 'movie'],
channel: 'DB_CLEAR_XTREAM_IMPORT_CACHE',
forwardedArgs: [playlistId, 'movie'],
},
{
method: 'dbSearchContent',
args: [playlistId, 'matrix', ['movie'], true],
channel: 'DB_SEARCH_CONTENT',
forwardedArgs: [playlistId, 'matrix', ['movie'], true],
},
{
method: 'dbGlobalSearch',
args: ['matrix', ['movie'], true],
channel: 'DB_GLOBAL_SEARCH',
forwardedArgs: ['matrix', ['movie'], true],
},
{
method: 'dbGetGlobalRecentlyAdded',
args: ['vod', 50, 'xtream'],
channel: 'DB_GET_GLOBAL_RECENTLY_ADDED',
forwardedArgs: ['vod', 50, 'xtream'],
},
{
method: 'dbGetRecentlyViewed',
args: [],
channel: 'DB_GET_RECENTLY_VIEWED',
forwardedArgs: [],
},
{
method: 'dbClearRecentlyViewed',
args: [],
channel: 'DB_CLEAR_RECENTLY_VIEWED',
forwardedArgs: [],
},
{
method: 'dbAddFavorite',
args: [12, playlistId, 'https://image.example/backdrop.jpg'],
channel: 'DB_ADD_FAVORITE',
forwardedArgs: [12, playlistId, 'https://image.example/backdrop.jpg'],
},
{
method: 'dbRemoveFavorite',
args: [12, playlistId],
channel: 'DB_REMOVE_FAVORITE',
forwardedArgs: [12, playlistId],
},
{
method: 'dbIsFavorite',
args: [12, playlistId],
channel: 'DB_IS_FAVORITE',
forwardedArgs: [12, playlistId],
},
{
method: 'dbGetFavorites',
args: [playlistId],
channel: 'DB_GET_FAVORITES',
forwardedArgs: [playlistId],
},
{
method: 'dbGetGlobalFavorites',
args: [],
channel: 'DB_GET_GLOBAL_FAVORITES',
forwardedArgs: [],
},
{
method: 'dbGetAllGlobalFavorites',
args: [],
channel: 'DB_GET_ALL_GLOBAL_FAVORITES',
forwardedArgs: [],
},
{
method: 'dbReorderGlobalFavorites',
args: [reorderUpdates],
channel: 'DB_REORDER_GLOBAL_FAVORITES',
forwardedArgs: [reorderUpdates],
},
{
method: 'dbGetRecentItems',
args: [playlistId],
channel: 'DB_GET_RECENT_ITEMS',
forwardedArgs: [playlistId],
},
{
method: 'dbAddRecentItem',
args: [13, playlistId, 'https://image.example/recent.jpg'],
channel: 'DB_ADD_RECENT_ITEM',
forwardedArgs: [13, playlistId, 'https://image.example/recent.jpg'],
},
{
method: 'dbClearPlaylistRecentItems',
args: [playlistId],
channel: 'DB_CLEAR_PLAYLIST_RECENT_ITEMS',
forwardedArgs: [playlistId],
},
{
method: 'dbRemoveRecentItem',
args: [13, playlistId],
channel: 'DB_REMOVE_RECENT_ITEM',
forwardedArgs: [13, playlistId],
},
{
method: 'dbRemoveRecentItemsBatch',
args: [recentItemsBatch],
channel: 'DB_REMOVE_RECENT_ITEMS_BATCH',
forwardedArgs: [recentItemsBatch],
},
{
method: 'dbGetContentByXtreamId',
args: [42, playlistId, 'movie'],
channel: 'DB_GET_CONTENT_BY_XTREAM_ID',
forwardedArgs: [42, playlistId, 'movie'],
},
{
method: 'dbSetContentBackdropIfMissing',
args: [12, 'https://image.example/backdrop.jpg'],
channel: 'DB_SET_CONTENT_BACKDROP_IF_MISSING',
forwardedArgs: [12, 'https://image.example/backdrop.jpg'],
},
{
method: 'dbDeleteAllPlaylists',
args: [operationId],
channel: 'DB_DELETE_ALL_PLAYLISTS',
forwardedArgs: [operationId],
},
{
method: 'dbCancelOperation',
args: [operationId],
channel: 'DB_CANCEL_OPERATION',
forwardedArgs: [operationId],
},
{
method: 'dbGetAppState',
args: ['workspace:last-route'],
channel: 'DB_GET_APP_STATE',
forwardedArgs: ['workspace:last-route'],
},
{
method: 'dbSetAppState',
args: ['workspace:last-route', '/workspace'],
channel: 'DB_SET_APP_STATE',
forwardedArgs: ['workspace:last-route', '/workspace'],
},
{
method: 'dbSavePlaybackPosition',
args: [playlistId, playbackData],
channel: 'DB_SAVE_PLAYBACK_POSITION',
forwardedArgs: [playlistId, playbackData],
},
{
method: 'dbGetPlaybackPosition',
args: [playlistId, 42, 'vod'],
channel: 'DB_GET_PLAYBACK_POSITION',
forwardedArgs: [playlistId, 42, 'vod'],
},
{
method: 'dbGetSeriesPlaybackPositions',
args: [playlistId, 88],
channel: 'DB_GET_SERIES_PLAYBACK_POSITIONS',
forwardedArgs: [playlistId, 88],
},
{
method: 'dbGetRecentPlaybackPositions',
args: [playlistId, 20],
channel: 'DB_GET_RECENT_PLAYBACK_POSITIONS',
forwardedArgs: [playlistId, 20],
},
{
method: 'dbGetAllPlaybackPositions',
args: [playlistId],
channel: 'DB_GET_ALL_PLAYBACK_POSITIONS',
forwardedArgs: [playlistId],
},
{
method: 'dbClearAllPlaybackPositions',
args: [playlistId],
channel: 'DB_CLEAR_ALL_PLAYBACK_POSITIONS',
forwardedArgs: [playlistId],
},
{
method: 'dbClearPlaybackPosition',
args: [playlistId, 42, 'vod'],
channel: 'DB_CLEAR_PLAYBACK_POSITION',
forwardedArgs: [playlistId, 42, 'vod'],
},
];
function getExposedApi(): ExposedElectronApi {
if (!mockExposedApi) {
throw new Error('Expected preload API to be exposed');
}
return mockExposedApi;
}
describe('main preload DB IPC contract', () => {
beforeEach(async () => {
jest.resetModules();
mockExposedApi = null;
mockGetPathForFile = jest.fn();
mockIpcRenderer = {
invoke: jest.fn().mockResolvedValue({ success: true }),
on: jest.fn(),
off: jest.fn(),
send: jest.fn(),
};
jest.doMock('electron', () => ({
contextBridge: {
exposeInMainWorld: jest.fn(
(_name: string, api: ExposedElectronApi) => {
mockExposedApi = api;
}
),
},
ipcRenderer: mockIpcRenderer,
webUtils: {
getPathForFile: mockGetPathForFile,
},
}));
await import('./main.preload');
});
afterEach(() => {
jest.dontMock('electron');
});
it('covers every worker-backed DB operation exposed by the preload bridge', () => {
const workerChannels = dbPreloadCases
.map((contractCase) => contractCase.channel)
.filter(
(channel): channel is DbWorkerOperation =>
channel !== 'DB_CANCEL_OPERATION' &&
DB_WORKER_OPERATIONS.includes(channel as DbWorkerOperation)
);
expect(new Set(workerChannels)).toEqual(new Set(DB_WORKER_OPERATIONS));
});
it.each(dbPreloadCases)(
'$method invokes $channel with the expected arguments',
async ({ method, args, channel, forwardedArgs }) => {
const api = getExposedApi();
await api[method](...args);
expect(mockIpcRenderer.invoke).toHaveBeenLastCalledWith(
channel,
...forwardedArgs
);
}
);
it('forwards request-scoped DB operation events and unregisters the listener', () => {
const api = getExposedApi();
const callback = jest.fn();
const event = {
operationId,
operation: 'save-content',
status: 'progress',
current: 5,
total: 10,
};
const unsubscribe = api.onDbOperationEvent(callback) as () => void;
const handler = mockIpcRenderer.on.mock.calls[0][1];
handler({}, event);
expect(mockIpcRenderer.on).toHaveBeenCalledWith(
'DB_OPERATION_EVENT',
handler
);
expect(callback).toHaveBeenCalledWith(event);
unsubscribe();
expect(mockIpcRenderer.off).toHaveBeenCalledWith(
'DB_OPERATION_EVENT',
handler
);
});
it('keeps the legacy save-content progress bridge scoped to progress events', () => {
const api = getExposedApi();
const callback = jest.fn();
api.onDbSaveContentProgress(callback);
const handler = mockIpcRenderer.on.mock.calls[0][1];
handler({}, { operation: 'delete-playlist', status: 'progress' });
handler({}, { operation: 'save-content', status: 'started' });
handler(
{},
{
operation: 'save-content',
status: 'progress',
current: 25,
increment: 5,
}
);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(5);
api.removeDbSaveContentProgress();
expect(mockIpcRenderer.off).toHaveBeenCalledWith(
'DB_OPERATION_EVENT',
handler
);
});
});
@@ -0,0 +1,462 @@
import {
DB_WORKER_OPERATIONS,
DbOperationEvent,
DbWorkerOperation,
} from '../../workers/database-worker.types';
type IpcHandler = (event: MockIpcEvent, ...args: unknown[]) => Promise<unknown>;
type MockIpcEvent = {
sender: {
isDestroyed: jest.Mock<boolean, []>;
send: jest.Mock;
};
};
type WorkerIpcContractCase = {
operation: DbWorkerOperation;
args: unknown[];
payload: unknown;
forwardsEvents?: boolean;
};
const mockRegisteredHandlers = new Map<string, IpcHandler>();
const mockWorkerRequest = jest.fn();
const mockWorkerCancel = jest.fn();
jest.mock('electron', () => ({
ipcMain: {
handle: jest.fn((channel: string, handler: IpcHandler) => {
mockRegisteredHandlers.set(channel, handler);
}),
},
}));
jest.mock('../../services/database-worker-client', () => ({
databaseWorkerClient: {
request: (...args: unknown[]) => mockWorkerRequest(...args),
cancel: (...args: unknown[]) => mockWorkerCancel(...args),
},
}));
const playlistId = 'playlist-1';
const operationId = 'operation-1';
const playlist = { id: playlistId, name: 'Playlist', type: 'xtream' };
const playlists = [playlist];
const playlistUpdates = { name: 'Updated playlist' };
const categories = [{ category_id: '10', category_name: 'Live' }];
const streams = [{ stream_id: 42, name: 'Channel' }];
const favorites = [{ contentId: 1, playlistId }];
const recentlyViewed = [{ contentId: 2, playlistId }];
const categoryIds = [10, 11];
const reorderUpdates = [{ content_id: 12, position: 1 }];
const recentItemsBatch = [{ contentId: 13, playlistId }];
const playbackData = {
contentXtreamId: 42,
contentType: 'vod',
positionSeconds: 120,
};
const workerIpcContractCases: WorkerIpcContractCase[] = [
{
operation: 'DB_CREATE_PLAYLIST',
args: [playlist],
payload: playlist,
},
{
operation: 'DB_UPSERT_APP_PLAYLIST',
args: [playlist],
payload: playlist,
},
{
operation: 'DB_UPSERT_APP_PLAYLISTS',
args: [playlists],
payload: playlists,
},
{
operation: 'DB_GET_APP_PLAYLISTS',
args: [],
payload: {},
},
{
operation: 'DB_GET_APP_PLAYLIST',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_GET_PLAYLIST',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_UPDATE_PLAYLIST',
args: [playlistId, playlistUpdates],
payload: { playlistId, updates: playlistUpdates },
},
{
operation: 'DB_GET_APP_STATE',
args: ['workspace:last-route'],
payload: { key: 'workspace:last-route' },
},
{
operation: 'DB_SET_APP_STATE',
args: ['workspace:last-route', '/workspace'],
payload: { key: 'workspace:last-route', value: '/workspace' },
},
{
operation: 'DB_HAS_CATEGORIES',
args: [playlistId, 'live'],
payload: { playlistId, type: 'live' },
},
{
operation: 'DB_GET_CATEGORIES',
args: [playlistId, 'live'],
payload: { playlistId, type: 'live' },
},
{
operation: 'DB_SAVE_CATEGORIES',
args: [playlistId, categories, 'live', categoryIds],
payload: {
playlistId,
categories,
type: 'live',
hiddenCategoryXtreamIds: categoryIds,
},
},
{
operation: 'DB_GET_ALL_CATEGORIES',
args: [playlistId, 'live'],
payload: { playlistId, type: 'live' },
},
{
operation: 'DB_UPDATE_CATEGORY_VISIBILITY',
args: [categoryIds, true],
payload: { categoryIds, hidden: true },
},
{
operation: 'DB_HAS_CONTENT',
args: [playlistId, 'movie'],
payload: { playlistId, type: 'movie' },
},
{
operation: 'DB_GET_CONTENT',
args: [playlistId, 'movie'],
payload: { playlistId, type: 'movie' },
},
{
operation: 'DB_GET_GLOBAL_RECENTLY_ADDED',
args: ['vod', 50, 'xtream'],
payload: { kind: 'vod', limit: 50, playlistType: 'xtream' },
},
{
operation: 'DB_SAVE_CONTENT',
args: [playlistId, streams, 'movie', operationId],
payload: { playlistId, streams, type: 'movie', operationId },
forwardsEvents: true,
},
{
operation: 'DB_CLEAR_XTREAM_IMPORT_CACHE',
args: [playlistId, 'movie'],
payload: { playlistId, type: 'movie' },
},
{
operation: 'DB_GET_CONTENT_BY_XTREAM_ID',
args: [42, playlistId, 'movie'],
payload: { xtreamId: 42, playlistId, contentType: 'movie' },
},
{
operation: 'DB_SET_CONTENT_BACKDROP_IF_MISSING',
args: [12, 'https://image.example/backdrop.jpg'],
payload: {
contentId: 12,
backdropUrl: 'https://image.example/backdrop.jpg',
},
},
{
operation: 'DB_SEARCH_CONTENT',
args: [playlistId, 'matrix', ['movie'], true],
payload: {
playlistId,
searchTerm: 'matrix',
types: ['movie'],
excludeHidden: true,
},
},
{
operation: 'DB_GLOBAL_SEARCH',
args: ['matrix', ['movie'], true],
payload: {
searchTerm: 'matrix',
types: ['movie'],
excludeHidden: true,
},
},
{
operation: 'DB_DELETE_PLAYLIST',
args: [playlistId, operationId],
payload: { playlistId, operationId },
forwardsEvents: true,
},
{
operation: 'DB_DELETE_ALL_PLAYLISTS',
args: [operationId],
payload: { operationId },
forwardsEvents: true,
},
{
operation: 'DB_DELETE_XTREAM_CONTENT',
args: [playlistId, operationId],
payload: { playlistId, operationId },
forwardsEvents: true,
},
{
operation: 'DB_RESTORE_XTREAM_USER_DATA',
args: [playlistId, favorites, recentlyViewed, operationId],
payload: { playlistId, favorites, recentlyViewed, operationId },
forwardsEvents: true,
},
{
operation: 'DB_ADD_FAVORITE',
args: [12, playlistId, 'https://image.example/backdrop.jpg'],
payload: {
contentId: 12,
playlistId,
backdropUrl: 'https://image.example/backdrop.jpg',
},
},
{
operation: 'DB_REMOVE_FAVORITE',
args: [12, playlistId],
payload: { contentId: 12, playlistId },
},
{
operation: 'DB_IS_FAVORITE',
args: [12, playlistId],
payload: { contentId: 12, playlistId },
},
{
operation: 'DB_GET_FAVORITES',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_GET_GLOBAL_FAVORITES',
args: [],
payload: {},
},
{
operation: 'DB_GET_ALL_GLOBAL_FAVORITES',
args: [],
payload: {},
},
{
operation: 'DB_REORDER_GLOBAL_FAVORITES',
args: [reorderUpdates],
payload: { updates: reorderUpdates },
},
{
operation: 'DB_GET_RECENTLY_VIEWED',
args: [],
payload: {},
},
{
operation: 'DB_CLEAR_RECENTLY_VIEWED',
args: [],
payload: {},
},
{
operation: 'DB_GET_RECENT_ITEMS',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_ADD_RECENT_ITEM',
args: [13, playlistId, 'https://image.example/recent.jpg'],
payload: {
contentId: 13,
playlistId,
backdropUrl: 'https://image.example/recent.jpg',
},
},
{
operation: 'DB_CLEAR_PLAYLIST_RECENT_ITEMS',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_REMOVE_RECENT_ITEM',
args: [13, playlistId],
payload: { contentId: 13, playlistId },
},
{
operation: 'DB_REMOVE_RECENT_ITEMS_BATCH',
args: [recentItemsBatch],
payload: { items: recentItemsBatch },
},
{
operation: 'DB_SAVE_PLAYBACK_POSITION',
args: [playlistId, playbackData],
payload: { playlistId, data: playbackData },
},
{
operation: 'DB_GET_PLAYBACK_POSITION',
args: [playlistId, 42, 'vod'],
payload: { playlistId, contentXtreamId: 42, contentType: 'vod' },
},
{
operation: 'DB_GET_SERIES_PLAYBACK_POSITIONS',
args: [playlistId, 88],
payload: { playlistId, seriesXtreamId: 88 },
},
{
operation: 'DB_GET_RECENT_PLAYBACK_POSITIONS',
args: [playlistId, 20],
payload: { playlistId, limit: 20 },
},
{
operation: 'DB_GET_ALL_PLAYBACK_POSITIONS',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_CLEAR_ALL_PLAYBACK_POSITIONS',
args: [playlistId],
payload: { playlistId },
},
{
operation: 'DB_CLEAR_PLAYBACK_POSITION',
args: [playlistId, 42, 'vod'],
payload: { playlistId, contentXtreamId: 42, contentType: 'vod' },
},
];
async function importDatabaseEventModules(): Promise<void> {
await import('./category.events');
await import('./content.events');
await import('./favorites.events');
await import('./playback-position.events');
await import('./playlist.events');
await import('./recently-viewed.events');
await import('./xtream.events');
}
function createIpcEvent(): MockIpcEvent {
return {
sender: {
isDestroyed: jest.fn(() => false),
send: jest.fn(),
},
};
}
function getHandler(channel: string): IpcHandler {
const handler = mockRegisteredHandlers.get(channel);
if (!handler) {
throw new Error(`Expected IPC handler for ${channel}`);
}
return handler;
}
function getLastWorkerRequestCall(): unknown[] {
return mockWorkerRequest.mock.calls[
mockWorkerRequest.mock.calls.length - 1
];
}
describe('database worker IPC contract', () => {
beforeEach(async () => {
jest.resetModules();
mockRegisteredHandlers.clear();
mockWorkerRequest.mockReset().mockResolvedValue({ success: true });
mockWorkerCancel.mockReset().mockResolvedValue({ success: true });
await importDatabaseEventModules();
});
it('registers an IPC handler for every worker operation', () => {
const registeredDbChannels = [...mockRegisteredHandlers.keys()]
.filter((channel) => channel !== 'DB_CANCEL_OPERATION')
.sort();
expect(registeredDbChannels).toEqual([...DB_WORKER_OPERATIONS].sort());
expect(mockRegisteredHandlers.has('DB_CANCEL_OPERATION')).toBe(true);
});
it.each(workerIpcContractCases)(
'$operation builds the expected worker request payload',
async ({ operation, args, payload, forwardsEvents }) => {
const ipcEvent = createIpcEvent();
const handler = getHandler(operation);
await handler(ipcEvent, ...args);
if (forwardsEvents) {
expect(mockWorkerRequest).toHaveBeenLastCalledWith(
operation,
payload,
{ onEvent: expect.any(Function) }
);
} else {
expect(mockWorkerRequest).toHaveBeenLastCalledWith(
operation,
payload
);
}
}
);
it('covers every worker operation in the payload contract cases', () => {
expect(
new Set(workerIpcContractCases.map(({ operation }) => operation))
).toEqual(new Set(DB_WORKER_OPERATIONS));
});
it('forwards request-scoped worker events only while the renderer exists', async () => {
const ipcEvent = createIpcEvent();
const workerEvent: DbOperationEvent = {
operationId,
operation: 'save-content',
status: 'progress',
current: 5,
total: 10,
};
await getHandler('DB_SAVE_CONTENT')(
ipcEvent,
playlistId,
streams,
'movie',
operationId
);
const options = getLastWorkerRequestCall()[2] as {
onEvent: (event: DbOperationEvent) => void;
};
options.onEvent(workerEvent);
expect(ipcEvent.sender.send).toHaveBeenCalledWith(
'DB_OPERATION_EVENT',
workerEvent
);
ipcEvent.sender.send.mockClear();
ipcEvent.sender.isDestroyed.mockReturnValue(true);
options.onEvent(workerEvent);
expect(ipcEvent.sender.send).not.toHaveBeenCalled();
});
it('routes DB cancellation through the worker client cancel channel', async () => {
const result = await getHandler('DB_CANCEL_OPERATION')(
createIpcEvent(),
operationId
);
expect(mockWorkerCancel).toHaveBeenCalledWith(operationId);
expect(result).toEqual({ success: true });
});
});