Skip to content

Commit

Permalink
tests(Scheduler): cover macro_task_array utils by jest tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wdevfx committed Feb 21, 2025
1 parent 0ad8f9e commit e1e2b3d
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
describe, expect, it, jest,
} from '@jest/globals';

import dispatcher, { macroTaskIdSet } from '../dispatcher';

jest.useFakeTimers();

describe('Scheduler', () => {
describe('MacroTaskArray', () => {
describe('Dispatcher', () => {
describe('schedule', () => {
it('should add timeout ids to timeout ids set', () => {
dispatcher.schedule(jest.fn(), 0).finally(() => {});
dispatcher.schedule(jest.fn(), 0).finally(() => {});

expect(macroTaskIdSet.size).toBe(2);
});

it('should remove timeout id from timeout ids set after macro task execution', async () => {
const p1 = dispatcher.schedule(jest.fn(), 0);
const p2 = dispatcher.schedule(jest.fn(), 0);

jest.advanceTimersByTime(0);

await Promise.all([p1, p2]);

expect(macroTaskIdSet.size).toBe(0);
});

it('should call callback as macro task', () => {
const callbackMock = jest.fn();
dispatcher.schedule(callbackMock, 0).finally(() => {});

expect(callbackMock).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(0);

expect(callbackMock).toHaveBeenCalledTimes(1);
});

it('should use macroTaskTimeoutMs form macro task delay', () => {
const callbackMock = jest.fn();
const macroTaskDelayMs = 1000;
dispatcher.schedule(callbackMock, macroTaskDelayMs).finally(() => {});

expect(callbackMock).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(macroTaskDelayMs / 2);

expect(callbackMock).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(macroTaskDelayMs / 2);

expect(callbackMock).toHaveBeenCalledTimes(1);
});
});

describe('dispose', () => {
it('should clear scheduled macro tasks', () => {
const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout');

dispatcher.schedule(jest.fn(), 0).finally(() => {});
dispatcher.schedule(jest.fn(), 0).finally(() => {});

const [firstId, secondId] = Array.from(macroTaskIdSet);

dispatcher.dispose();

expect(clearTimeoutSpy).toHaveBeenCalledTimes(2);
expect(clearTimeoutSpy.mock.calls).toEqual([
[firstId],
[secondId],
]);
});

it('should clear timeout ids set', () => {
dispatcher.schedule(jest.fn(), 0).finally(() => {});
dispatcher.schedule(jest.fn(), 0).finally(() => {});

expect(macroTaskIdSet.size).toBe(2);

dispatcher.dispose();

expect(macroTaskIdSet.size).toBe(0);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
beforeEach,
describe, expect, it, jest,
} from '@jest/globals';

import dispatcher from '../dispatcher';
import { macroTaskArrayForEach, macroTaskArrayMap } from '../methods';

jest.mock('../dispatcher', () => {
const actualModule = jest.requireActual<any>('../dispatcher');
return {
...actualModule.default,
schedule: jest.fn(),
};
});

const scheduleFnMock = jest.fn();

jest.useFakeTimers();

describe('Scheduler', () => {
describe('MacroTaskArray', () => {
describe('Methods', () => {
beforeEach(() => {
scheduleFnMock.mockReset();
dispatcher.schedule = scheduleFnMock as any;
});

describe('macroTaskArrayForEach', () => {
it.each<{ arraySize: number; step: number; expectedCalls: number }>([
{ arraySize: 10, step: 1, expectedCalls: 10 },
{ arraySize: 10, step: 2, expectedCalls: 5 },
{ arraySize: 10, step: 3, expectedCalls: 4 },
{ arraySize: 3, step: 4, expectedCalls: 1 },
{ arraySize: 0, step: 10, expectedCalls: 0 },
])('should split array into batches (arraySize = $arraySize | step = $step)', async ({ arraySize, step, expectedCalls }) => {
await macroTaskArrayForEach(new Array(arraySize), jest.fn(), step);

expect(scheduleFnMock).toHaveBeenCalledTimes(expectedCalls);
});

it.each<{ arraySize: number; step: number }>([
{ arraySize: 10, step: 1 },
{ arraySize: 10, step: 2 },
{ arraySize: 10, step: 3 },
{ arraySize: 3, step: 4 },
{ arraySize: 0, step: 10 },
])('should call callback for each array item (arraySize = $arraySize | step = $step)', async ({ arraySize, step }) => {
const callbackMock = jest.fn();
scheduleFnMock.mockImplementation((callback: any) => {
callback();
return Promise.resolve();
});

const array = new Array(arraySize).fill(0).map((_, idx) => idx);
await macroTaskArrayForEach(array, callbackMock, step);

expect(callbackMock).toHaveBeenCalledTimes(arraySize);
expect(callbackMock.mock.calls).toEqual(array.map((item) => [item]));
});

it('should pass macroTaskTimeoutMs to dispatcher', async () => {
const testDelayMs = 12345;

await macroTaskArrayForEach(new Array(10), jest.fn(), 100, testDelayMs);

expect(scheduleFnMock).toHaveBeenCalledTimes(1);
expect(scheduleFnMock).toHaveBeenCalledWith(expect.anything(), testDelayMs);
});
});

describe('macroTaskArrayMap', () => {
it.each<{ arraySize: number; step: number; expectedCalls: number }>([
{ arraySize: 10, step: 1, expectedCalls: 10 },
{ arraySize: 10, step: 2, expectedCalls: 5 },
{ arraySize: 10, step: 3, expectedCalls: 4 },
{ arraySize: 3, step: 4, expectedCalls: 1 },
{ arraySize: 0, step: 10, expectedCalls: 0 },
])('should split array into batches (arraySize = $arraySize | step = $step)', async ({ arraySize, step, expectedCalls }) => {
await macroTaskArrayMap(new Array(arraySize), jest.fn(), step);

expect(scheduleFnMock).toHaveBeenCalledTimes(expectedCalls);
});

it.each<{ arraySize: number; step: number }>([
{ arraySize: 10, step: 1 },
{ arraySize: 10, step: 2 },
{ arraySize: 10, step: 3 },
{ arraySize: 3, step: 4 },
{ arraySize: 0, step: 10 },
])('should call callback for each array item (arraySize = $arraySize | step = $step)', async ({ arraySize, step }) => {
const callbackMock = jest.fn();
scheduleFnMock.mockImplementation((callback: any) => {
callback();
return Promise.resolve();
});

const array = new Array(arraySize).fill(0).map((_, idx) => idx);
await macroTaskArrayMap(array, callbackMock, step);

expect(callbackMock).toHaveBeenCalledTimes(arraySize);
expect(callbackMock.mock.calls).toEqual(array.map((item) => [item]));
});

it('should return mapped result', async () => {
const callbackFn = (item: number): string => `${item}_processed`;
scheduleFnMock.mockImplementation((callback: any) => {
callback();
return Promise.resolve();
});

const array = new Array(10).fill(0).map((_, idx) => idx);
const result = await macroTaskArrayMap(array, callbackFn, 10);

expect(result).toEqual(array.map(callbackFn));
});

it('should pass macroTaskTimeoutMs to dispatcher', async () => {
const testDelayMs = 12345;

await macroTaskArrayMap(new Array(10), jest.fn(), 100, testDelayMs);

expect(scheduleFnMock).toHaveBeenCalledTimes(1);
expect(scheduleFnMock).toHaveBeenCalledWith(expect.anything(), testDelayMs);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// eslint-disable-next-line no-restricted-globals
const macroTaskIdSet = new Set<ReturnType<typeof setTimeout>>();
export const macroTaskIdSet = new Set<ReturnType<typeof setTimeout>>();

export const schedule = async (
const schedule = async (
callback: () => void,
macroTaskTimeoutMs: number,
): Promise<void> => new Promise<void>((resolve) => {
Expand All @@ -16,8 +16,11 @@ export const schedule = async (
macroTaskIdSet.add(taskId);
});

export const dispose = (): void => {
macroTaskIdSet.forEach((id) => clearTimeout(id));
const dispose = (): void => {
Array.from(macroTaskIdSet).forEach((id) => {
clearTimeout(id);
macroTaskIdSet.delete(id);
});
};

export default {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ export const macroTaskArrayForEach = async <TItem>(
macroTaskTimeoutMs = DEFAULT_MACRO_TASK_TIMEOUT,
): Promise<void> => {
const promises: Promise<void>[] = [];
const maxBatchIdx = array.length + step - 1;
const batchesCount = Math.ceil(array.length / step);

for (let batchIdx = 0; batchIdx < maxBatchIdx; batchIdx += step) {
promises.push(macroTaskDispatcher.schedule(() => {
const maxIdx = batchIdx + step - 1;
for (let batchIdx = 0; batchIdx < batchesCount; batchIdx += 1) {
const scheduledTask = macroTaskDispatcher.schedule(() => {
const startIdx = batchIdx * step;
const maxIdx = startIdx + step;

for (let idx = batchIdx; idx < maxIdx && !!array[idx]; idx += 1) {
for (let idx = startIdx; idx < maxIdx && array[idx] !== undefined; idx += 1) {
callback(array[idx]);
}
}, macroTaskTimeoutMs));
}, macroTaskTimeoutMs);

promises.push(scheduledTask);
}

await Promise.all(promises);
Expand Down

0 comments on commit e1e2b3d

Please sign in to comment.