mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
test(electron): add sqlite ipc contract coverage
This commit is contained in:
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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user