Proyecto App Linux
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import type { DatabaseWorkerClient as DatabaseWorkerClientType } from './database-worker-client';
|
||||
|
||||
const mockWorkerInstances: any[] = [];
|
||||
const resolveWorkerRuntimeBootstrap = jest.fn();
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
app: {
|
||||
isPackaged: false,
|
||||
getAppPath: () => '/mock/app.asar',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('worker_threads', () => {
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class MockWorker extends EventEmitter {
|
||||
postMessage = jest.fn();
|
||||
terminate = jest.fn().mockResolvedValue(0);
|
||||
}
|
||||
|
||||
return {
|
||||
Worker: jest.fn().mockImplementation(() => {
|
||||
const worker = new MockWorker();
|
||||
mockWorkerInstances.push(worker);
|
||||
return worker;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../workers/worker-runtime-paths', () => ({
|
||||
resolveWorkerRuntimeBootstrap: (...args: unknown[]) =>
|
||||
resolveWorkerRuntimeBootstrap(...args),
|
||||
}));
|
||||
|
||||
describe('DatabaseWorkerClient', () => {
|
||||
let DatabaseWorkerClient: typeof DatabaseWorkerClientType;
|
||||
let clients: DatabaseWorkerClientType[];
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
mockWorkerInstances.length = 0;
|
||||
resolveWorkerRuntimeBootstrap.mockReset();
|
||||
resolveWorkerRuntimeBootstrap.mockReturnValue({
|
||||
workerPath: '/mock/workers/database.worker.js',
|
||||
workerPathCandidates: ['/mock/workers/database.worker.js'],
|
||||
nativeModuleSearchPaths: [
|
||||
'/mock/resources/app.asar.unpacked/node_modules',
|
||||
],
|
||||
});
|
||||
clients = [];
|
||||
({ DatabaseWorkerClient } = await import('./database-worker-client'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
clients.map((client) => client.shutdown().catch(() => undefined))
|
||||
);
|
||||
});
|
||||
|
||||
function createClient(): DatabaseWorkerClientType {
|
||||
const client = new DatabaseWorkerClient();
|
||||
clients.push(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
async function flushPromises(): Promise<void> {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
it('resolves requests after the worker reports ready and returns a response', async () => {
|
||||
const client = createClient();
|
||||
const requestPromise = client.request('DB_GLOBAL_SEARCH', {
|
||||
searchTerm: 'matrix',
|
||||
});
|
||||
const worker = mockWorkerInstances[0];
|
||||
const { Worker } = jest.requireMock('worker_threads');
|
||||
|
||||
expect(resolveWorkerRuntimeBootstrap).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workerFilename: 'database.worker.js',
|
||||
developmentWorkerDir: expect.stringContaining('workers'),
|
||||
})
|
||||
);
|
||||
expect(Worker).toHaveBeenCalledWith(expect.any(URL), {
|
||||
workerData: {
|
||||
nativeModuleSearchPaths: [
|
||||
'/mock/resources/app.asar.unpacked/node_modules',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
worker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
|
||||
expect(worker.postMessage).toHaveBeenCalledTimes(1);
|
||||
const request = worker.postMessage.mock.calls[0][0];
|
||||
|
||||
worker.emit('message', {
|
||||
type: 'response',
|
||||
requestId: request.requestId,
|
||||
success: true,
|
||||
result: [{ id: 1 }],
|
||||
});
|
||||
|
||||
await expect(requestPromise).resolves.toEqual([{ id: 1 }]);
|
||||
});
|
||||
|
||||
it('forwards worker events to the pending request callback', async () => {
|
||||
const client = createClient();
|
||||
const onEvent = jest.fn();
|
||||
const requestPromise = client.request(
|
||||
'DB_SAVE_CONTENT',
|
||||
{ playlistId: 'xtream-1', streams: [], type: 'movie' },
|
||||
{ onEvent }
|
||||
);
|
||||
const worker = mockWorkerInstances[0];
|
||||
|
||||
worker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
|
||||
const request = worker.postMessage.mock.calls[0][0];
|
||||
const event = {
|
||||
operationId: 'op-1',
|
||||
operation: 'save-content',
|
||||
status: 'progress',
|
||||
current: 10,
|
||||
total: 100,
|
||||
};
|
||||
|
||||
worker.emit('message', {
|
||||
type: 'event',
|
||||
requestId: request.requestId,
|
||||
event,
|
||||
});
|
||||
|
||||
expect(onEvent).toHaveBeenCalledWith(event);
|
||||
|
||||
worker.emit('message', {
|
||||
type: 'response',
|
||||
requestId: request.requestId,
|
||||
success: true,
|
||||
result: { success: true, count: 10 },
|
||||
});
|
||||
|
||||
await expect(requestPromise).resolves.toEqual({
|
||||
success: true,
|
||||
count: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('turns serialized worker errors into rejected Error instances', async () => {
|
||||
const client = createClient();
|
||||
const requestPromise = client.request('DB_DELETE_PLAYLIST', {
|
||||
playlistId: 'xtream-1',
|
||||
});
|
||||
const worker = mockWorkerInstances[0];
|
||||
|
||||
worker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
|
||||
const request = worker.postMessage.mock.calls[0][0];
|
||||
worker.emit('message', {
|
||||
type: 'response',
|
||||
requestId: request.requestId,
|
||||
success: false,
|
||||
error: {
|
||||
name: 'SqliteBusyError',
|
||||
message: 'database is locked',
|
||||
stack: 'stack-trace',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(requestPromise).rejects.toMatchObject({
|
||||
name: 'SqliteBusyError',
|
||||
message: 'database is locked',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves AbortError responses from the worker', async () => {
|
||||
const client = createClient();
|
||||
const requestPromise = client.request('DB_SAVE_CONTENT', {
|
||||
playlistId: 'xtream-1',
|
||||
streams: [],
|
||||
type: 'movie',
|
||||
});
|
||||
const worker = mockWorkerInstances[0];
|
||||
|
||||
worker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
|
||||
const request = worker.postMessage.mock.calls[0][0];
|
||||
worker.emit('message', {
|
||||
type: 'response',
|
||||
requestId: request.requestId,
|
||||
success: false,
|
||||
error: {
|
||||
name: 'AbortError',
|
||||
message: 'Operation "save-content" was cancelled',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(requestPromise).rejects.toMatchObject({
|
||||
name: 'AbortError',
|
||||
message: 'Operation "save-content" was cancelled',
|
||||
});
|
||||
});
|
||||
|
||||
it('posts cancel messages to the current worker', async () => {
|
||||
const client = createClient();
|
||||
const requestPromise = client.request('DB_DELETE_PLAYLIST', {
|
||||
playlistId: 'xtream-1',
|
||||
});
|
||||
const worker = mockWorkerInstances[0];
|
||||
|
||||
worker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
|
||||
await expect(client.cancel('operation-123')).resolves.toEqual({
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect(worker.postMessage).toHaveBeenCalledWith({
|
||||
type: 'cancel',
|
||||
operationId: 'operation-123',
|
||||
});
|
||||
|
||||
const request = worker.postMessage.mock.calls[0][0];
|
||||
worker.emit('message', {
|
||||
type: 'response',
|
||||
requestId: request.requestId,
|
||||
success: true,
|
||||
result: { success: true },
|
||||
});
|
||||
|
||||
await expect(requestPromise).resolves.toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('rejects pending work on worker exit and starts a fresh worker for the next request', async () => {
|
||||
const client = createClient();
|
||||
const firstRequest = client.request('DB_SEARCH_CONTENT', {
|
||||
playlistId: 'xtream-1',
|
||||
searchTerm: 'matrix',
|
||||
types: ['movie'],
|
||||
});
|
||||
const firstWorker = mockWorkerInstances[0];
|
||||
|
||||
firstWorker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
firstWorker.emit('exit', 1);
|
||||
|
||||
await expect(firstRequest).rejects.toThrow(
|
||||
'Database worker stopped with exit code 1'
|
||||
);
|
||||
|
||||
const secondRequest = client.request('DB_SEARCH_CONTENT', {
|
||||
playlistId: 'xtream-1',
|
||||
searchTerm: 'neo',
|
||||
types: ['movie'],
|
||||
});
|
||||
const secondWorker = mockWorkerInstances[1];
|
||||
|
||||
secondWorker.emit('message', { type: 'ready' });
|
||||
await flushPromises();
|
||||
|
||||
const request = secondWorker.postMessage.mock.calls[0][0];
|
||||
secondWorker.emit('message', {
|
||||
type: 'response',
|
||||
requestId: request.requestId,
|
||||
success: true,
|
||||
result: [],
|
||||
});
|
||||
|
||||
await expect(secondRequest).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects requests with actionable worker path errors', async () => {
|
||||
const client = createClient();
|
||||
resolveWorkerRuntimeBootstrap.mockImplementation(() => {
|
||||
const error = new Error(
|
||||
'Unable to resolve worker "database.worker.js".\nTried:\n- /missing/database.worker.js'
|
||||
);
|
||||
error.name = 'WorkerPathResolutionError';
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.request('DB_GLOBAL_SEARCH', { searchTerm: 'matrix' })
|
||||
).rejects.toMatchObject({
|
||||
name: 'WorkerPathResolutionError',
|
||||
message: expect.stringContaining('database.worker.js'),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user