UNPKG

@stackbit/utils

Version:
627 lines (524 loc) 27.6 kB
import { jest, describe, expect, test, beforeEach, afterEach } from '@jest/globals'; import './test-utils/toBeWithinRange'; import { deferredPromise, deferWhileRunning } from '../src'; type MockFunction = (arg1: string, arg2: string, arg3: string) => Promise<string>; function mockHelper(func?: MockFunction) { return jest.fn(func); } function sleep(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } describe('deferredPromise', () => { test('resolves when resolve is called', async () => { const deferred = deferredPromise(); deferred.resolve('test'); await expect(deferred.promise).resolves.toEqual('test'); }); test('rejects when reject is called', async () => { const deferred = deferredPromise(); deferred.reject('error'); await expect(deferred.promise).rejects.toEqual('error'); }); }); describe('deferWhileRunning', () => { beforeEach(() => { jest.useFakeTimers(); jest.spyOn(global, 'setTimeout'); }); afterEach(() => { (setTimeout as jest.MockedFunction<typeof setTimeout>).mockClear(); jest.useRealTimers(); }); test('invokes the function immediately if no previous calls were made and resolves with the resolved value', async () => { const mockFn = mockHelper((arg1, arg2, arg3) => Promise.resolve(arg1 + arg2 + arg3)); const deferred = deferWhileRunning(mockFn); const result = deferred('a', 'b', 'c'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); await expect(result).resolves.toEqual('abc'); }); test('invokes the function immediately if no previous calls were made and rejects with the rejected value', async () => { const mockFn = mockHelper((arg1, arg2, arg3) => Promise.reject({ error: arg1 + arg2 + arg3 })); const deferred = deferWhileRunning(mockFn); const result = deferred('a', 'b', 'c'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); await expect(result).rejects.toEqual({ error: 'abc' }); }); test('invokes the function immediately if no previous calls were made and rejects with the thrown error', async () => { const mockFn = mockHelper(async (arg1, arg2, arg3) => { throw new Error(arg1 + arg2 + arg3); }); const deferred = deferWhileRunning(mockFn); const result = deferred('a', 'b', 'c'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); try { await result; } catch (e: any) { expect(e.message).toEqual('abc'); } }); test('invokes the next function immediately if the previous invocation was resolved', async () => { const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => arg1 + arg2 + arg3); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); await expect(result1).resolves.toEqual('abc'); mockFn.mockClear(); const result2 = await deferred('d', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']); expect(result2).toEqual('def'); }); test('invokes the next function immediately if the previous invocation was rejected', async () => { const mockFn = mockHelper() .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { return Promise.reject({ error: arg1 + arg2 + arg3 }); }) .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); await expect(result1).rejects.toEqual({ error: 'abc' }); mockFn.mockClear(); const result2 = await deferred('d', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']); expect(result2).toEqual('def'); }); test('debounce the function call and resolves with the resolved value after debounce delay', async () => { const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn, { debounceDelay: 100 }); const result = deferred('a', 'b', 'c'); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(100); await expect(result).resolves.toEqual('abc'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); }); test('debounce the function call and rejects with the rejected value after debounce delay', async () => { const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { return Promise.reject({ error: arg1 + arg2 + arg3 }); }); const deferred = deferWhileRunning(mockFn, { debounceDelay: 100 }); const result = deferred('a', 'b', 'c'); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(100); await expect(result).rejects.toEqual({ error: 'abc' }); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); }); test('debounce the function call and rejects with the thrown error after debounce delay', async () => { const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { throw new Error(arg1 + arg2 + arg3); }); const deferred = deferWhileRunning(mockFn, { debounceDelay: 100 }); const result = deferred('a', 'b', 'c'); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(100); await expect(result).rejects.toThrow('abc'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); }); test('debounce the function three times then invokes the function with the last provided arguments and resolves all calls after debounce delay', async () => { const mockFn = jest.fn(async (arg1: string) => arg1); const deferred = deferWhileRunning(mockFn, { debounceDelay: 150 }); // first call is debounced for 150ms const result1 = deferred('a'); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(100); // second call is made after 100ms, debounced for 150ms, and scheduled for 250ms (from start) const result2 = deferred('b'); expect(setTimeout).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(100); // third call is made after 200ms (from start), debounced for 150ms, and scheduled for 350ms (from start) const result3 = deferred('c'); expect(setTimeout).toHaveBeenCalledTimes(3); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(150); await expect(result1).resolves.toEqual('c'); await expect(result2).resolves.toEqual('c'); await expect(result3).resolves.toEqual('c'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.calls[0]).toEqual(['c']); }); test('debounce the function twice then invokes the function due to debounceMaxDelay, then defers the third call until the previous invocation is resolved', async () => { const mockFn = jest.fn(async (arg1: string) => { await sleep(100); return arg1; }); const deferred = deferWhileRunning(mockFn, { debounceDelay: 200, debounceMaxDelay: 300 }); // const deferred = deferWhileRunning(mockFn); // // mockFn('b') mockFn('c') // ↓ ↓ // o------ debounce delay -------●o========●o=========● // o---------------------------------------● // ↑ o------------------------● // ↑ ↑ o---------------● // ↑ ↑ ↑ // time:--0---------100--150--200-------300--350--400---------------> // ↑ ↑ ↑ // deferred('a') deferred('b') deferred('c') // first call is debounced for 300ms const result1 = deferred('a'); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200); expect(mockFn).not.toBeCalled(); // second call is made after 150ms, debounced for 150ms because max debounce delay is 300ms, // and scheduled for 300ms (from start) and runs for 100ms jest.advanceTimersByTime(150); const result2 = deferred('b'); expect(setTimeout).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(150); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenLastCalledWith('b'); // this setTimeout called from within the mocked function expect(setTimeout).toHaveBeenCalledTimes(3); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100); // third call is made after 350ms (from start) while the second call is running, // so this call will be deferred until after the second call invocation completes // which will be at 400ms from the start jest.advanceTimersByTime(50); const result3 = deferred('c'); expect(mockFn).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenCalledTimes(3); jest.advanceTimersByTime(50); await expect(result1).resolves.toEqual('b'); await expect(result2).resolves.toEqual('b'); expect(setTimeout).toHaveBeenCalledTimes(4); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100); jest.advanceTimersByTime(100); expect(mockFn).toHaveBeenCalledTimes(2); expect(mockFn).toHaveBeenLastCalledWith('c'); await expect(result3).resolves.toEqual('c'); }); test('debounce the function three times then invokes the function with arguments as defined by the argsResolver then resolves with the resolved value after debounce delay', async () => { const mockFn = jest.fn(async (arg1: string, arg2: number) => { return [arg1, arg2]; }); const deferred = deferWhileRunning(mockFn, { debounceDelay: 200, argsResolver: ({ nextArgs, prevArgs }) => { return prevArgs ? ([prevArgs[0] + nextArgs[0], prevArgs[1] + nextArgs[1]] as [string, number]) : nextArgs; } }); // const deferred = deferWhileRunning(mockFn); // // mockFn('abc', 6) // ↓ // o------ debounce delay -------●o===● // o-----------------------------● // ↑ o-------------------● // ↑ ↑ o---------● // ↑ ↑ ↑ // time:--0---------100-------200-------300-----> // ↑ ↑ ↑ // ↑ ↑ deferred('c', 3) // ↑ deferred('a', 2) // deferred('a', 1) // first call is debounced for 200ms const result1 = deferred('a', 1); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200); expect(mockFn).not.toBeCalled(); // second call is made after 100ms, debounced for 200ms, and scheduled for 300ms (from start) jest.advanceTimersByTime(100); const result2 = deferred('b', 2); expect(setTimeout).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200); expect(mockFn).not.toBeCalled(); // third call is made after 200ms (from start), debounced for 200ms, and scheduled for 400ms (from start) jest.advanceTimersByTime(100); const result3 = deferred('c', 3); expect(setTimeout).toHaveBeenCalledTimes(3); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200); expect(mockFn).not.toBeCalled(); jest.advanceTimersByTime(200); await expect(result1).resolves.toEqual(['abc', 6]); await expect(result2).resolves.toEqual(['abc', 6]); await expect(result3).resolves.toEqual(['abc', 6]); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.calls[0]).toEqual(['abc', 6]); }); test('defers the next function call until the previous invocation is resolved', async () => { const wait = deferredPromise<void>(); const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); const result2 = deferred('d', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); mockFn.mockClear(); wait.resolve(); await expect(result1).resolves.toEqual('abc'); await expect(result2).resolves.toEqual('def'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']); }); test('defers the next function call until the previous invocation is rejected', async () => { const wait = deferredPromise<void>(); const mockFn = mockHelper() .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return Promise.reject({ error: arg1 + arg2 + arg3 }); }) .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); const result2 = deferred('d', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); mockFn.mockClear(); wait.resolve(); await expect(result1).rejects.toEqual({ error: 'abc' }); await expect(result2).resolves.toEqual('def'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']); }); test('defers the next function call until the previous invocation throws', async () => { const wait = deferredPromise<void>(); const mockFn = mockHelper() .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; throw new Error(arg1 + arg2 + arg3); }) .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); const result2 = deferred('d', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); mockFn.mockClear(); wait.resolve(); try { await result1; } catch (e: any) { expect(e.message).toEqual('abc'); } await expect(result2).resolves.toEqual('def'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']); }); test( 'defers multiple function calls until the previous invocation is resolved,' + 'then invokes the function with the last provided arguments and resolves all calls', async () => { const wait = deferredPromise<void>(); const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); const result2 = deferred('d', 'e', 'f'); const result3 = deferred('g', 'h', 'i'); const result4 = deferred('j', 'k', 'l'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); mockFn.mockClear(); wait.resolve(); await expect(result1).resolves.toEqual('abc'); await expect(result2).resolves.toEqual('jkl'); await expect(result3).resolves.toEqual('jkl'); await expect(result4).resolves.toEqual('jkl'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['j', 'k', 'l']); } ); test( 'defers multiple function calls until the previous invocation is resolved,' + 'then invokes the function with the last provided arguments and rejects all calls', async () => { const wait = deferredPromise<void>(); const mockFn = mockHelper() .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return arg1 + arg2 + arg3; }) .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { return Promise.reject({ error: arg1 + arg2 + arg3 }); }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); const result2 = deferred('d', 'e', 'f'); const result3 = deferred('g', 'h', 'i'); const result4 = deferred('j', 'k', 'l'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); mockFn.mockClear(); wait.resolve(); await expect(result1).resolves.toEqual('abc'); await expect(result2).rejects.toEqual({ error: 'jkl' }); await expect(result3).rejects.toEqual({ error: 'jkl' }); await expect(result4).rejects.toEqual({ error: 'jkl' }); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['j', 'k', 'l']); } ); test( 'defers multiple function calls until the previous invocation is rejected, ' + 'then invokes the function with the last provided arguments and resolves all calls', async () => { const wait = deferredPromise<void>(); const mockFn = mockHelper() .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return Promise.reject({ error: arg1 + arg2 + arg3 }); }) .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => { return arg1 + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn); const result1 = deferred('a', 'b', 'c'); const result2 = deferred('d', 'e', 'f'); const result3 = deferred('g', 'h', 'i'); const result4 = deferred('j', 'k', 'l'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']); mockFn.mockClear(); wait.resolve(); await expect(result1).rejects.toEqual({ error: 'abc' }); await expect(result2).resolves.toEqual('jkl'); await expect(result3).resolves.toEqual('jkl'); await expect(result4).resolves.toEqual('jkl'); expect(mockFn.mock.calls.length).toEqual(1); expect(mockFn.mock.lastCall).toEqual(['j', 'k', 'l']); } ); test('invokes the function immediately if no previous calls in the same group made and resolves with the resolved value', async () => { const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { return arg1 + '-' + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn, { groupResolver: (group: string) => group }); const result1 = deferred('group1', 'a', 'b'); const result2 = deferred('group2', 'c', 'd'); const result3 = deferred('group3', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(3); expect(mockFn.mock.calls[0]).toEqual(['group1', 'a', 'b']); expect(mockFn.mock.calls[1]).toEqual(['group2', 'c', 'd']); expect(mockFn.mock.calls[2]).toEqual(['group3', 'e', 'f']); await expect(result1).resolves.toEqual('group1-ab'); await expect(result2).resolves.toEqual('group2-cd'); await expect(result3).resolves.toEqual('group3-ef'); }); test( 'defers multiple function calls in a group until the previous invocation in the same group is resolved,' + 'then invokes the function with the last provided arguments to the group and resolves all group calls', async () => { const wait = deferredPromise<void>(); const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return arg1 + '-' + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn, { groupResolver: (group: string) => group }); const result1a = deferred('group1', 'a', 'b'); const result1b = deferred('group1', 'c', 'd'); const result1c = deferred('group1', 'e', 'f'); const result2a = deferred('group2', 'a', 'b'); const result2b = deferred('group2', 'c', 'd'); const result2c = deferred('group2', 'e', 'f'); const result3a = deferred('group3', 'a', 'b'); const result3b = deferred('group3', 'c', 'd'); const result3c = deferred('group3', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(3); expect(mockFn.mock.calls[0]).toEqual(['group1', 'a', 'b']); expect(mockFn.mock.calls[1]).toEqual(['group2', 'a', 'b']); expect(mockFn.mock.calls[2]).toEqual(['group3', 'a', 'b']); wait.resolve(); await expect(result1a).resolves.toEqual('group1-ab'); await expect(result2a).resolves.toEqual('group2-ab'); await expect(result3a).resolves.toEqual('group3-ab'); await expect(result1b).resolves.toEqual('group1-ef'); await expect(result1c).resolves.toEqual('group1-ef'); await expect(result2b).resolves.toEqual('group2-ef'); await expect(result2c).resolves.toEqual('group2-ef'); await expect(result3b).resolves.toEqual('group3-ef'); await expect(result3c).resolves.toEqual('group3-ef'); expect(mockFn.mock.calls.length).toEqual(6); expect(mockFn.mock.calls[3]).toEqual(['group1', 'e', 'f']); expect(mockFn.mock.calls[4]).toEqual(['group2', 'e', 'f']); expect(mockFn.mock.calls[5]).toEqual(['group3', 'e', 'f']); } ); test( 'defers multiple function calls in a group until the previous invocation in the same group is resolved,' + 'then invokes the function with arguments as defined by argsResolver to the group and resolves all group calls', async () => { const wait = deferredPromise<void>(); const mockFn = mockHelper(async (arg1: string, arg2: string, arg3: string) => { await wait.promise; return arg1 + '-' + arg2 + arg3; }); const deferred = deferWhileRunning(mockFn, { groupResolver: (group: string) => group, argsResolver: ({ nextArgs, prevArgs }) => { return [nextArgs[0], 'A', 'B'] as [string, string, string]; } }); const result1a = deferred('group1', 'a', 'b'); const result1b = deferred('group1', 'c', 'd'); const result1c = deferred('group1', 'e', 'f'); const result2a = deferred('group2', 'a', 'b'); const result2b = deferred('group2', 'c', 'd'); const result2c = deferred('group2', 'e', 'f'); const result3a = deferred('group3', 'a', 'b'); const result3b = deferred('group3', 'c', 'd'); const result3c = deferred('group3', 'e', 'f'); expect(mockFn.mock.calls.length).toEqual(3); expect(mockFn.mock.calls[0]).toEqual(['group1', 'a', 'b']); expect(mockFn.mock.calls[1]).toEqual(['group2', 'a', 'b']); expect(mockFn.mock.calls[2]).toEqual(['group3', 'a', 'b']); wait.resolve(); await expect(result1a).resolves.toEqual('group1-ab'); await expect(result2a).resolves.toEqual('group2-ab'); await expect(result3a).resolves.toEqual('group3-ab'); await expect(result1b).resolves.toEqual('group1-AB'); await expect(result1c).resolves.toEqual('group1-AB'); await expect(result2b).resolves.toEqual('group2-AB'); await expect(result2c).resolves.toEqual('group2-AB'); await expect(result3b).resolves.toEqual('group3-AB'); await expect(result3c).resolves.toEqual('group3-AB'); expect(mockFn.mock.calls.length).toEqual(6); expect(mockFn.mock.calls[3]).toEqual(['group1', 'A', 'B']); expect(mockFn.mock.calls[4]).toEqual(['group2', 'A', 'B']); expect(mockFn.mock.calls[5]).toEqual(['group3', 'A', 'B']); } ); });