/* eslint-disable vitest/no-conditional-expect */ import {colorText} from '../../colorText'; import {delay} from '../../delay'; import {TimeoutError} from '../../errors/TimeoutError'; import {waitFor} from '../waitFor'; describe('waitFor', () => { describe('using real timers', () => { it('should pass with Promise', async () => { // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression const result = await waitFor(async () => true, {timeout: 10}); expect(result).toBe(undefined); }); it('should pass with sync', async () => { // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression const result = await waitFor(() => true, {timeout: 10}); expect(result).toBe(undefined); }); it('should not pass', async () => { try { await waitFor( async () => { // @ts-expect-error - condition that will throw // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return 1 === '1'; }, {timeout: 10}, ); } catch (e) { expect(e).toBeInstanceOf(TimeoutError); expect((e as Error).message).toContain('waitFor [timeout=10]'); expect((e as Error).message).toContain( colorText.default('Expected: ') + colorText.green('true') + '\n' + colorText.default('Received: ') + colorText.red('false'), ); } }); it('should be able to work with shorthand timeout param', async () => { try { await waitFor(async () => { return false; }, 10); } catch (e) { expect(e).toBeInstanceOf(TimeoutError); expect((e as Error).message).toContain('waitFor [timeout=10]'); } }); it('should give type hint in case error of primitive values comparison', async () => { try { // @ts-expect-error - condition that will throw await waitFor(() => 'true', {timeout: 10}); } catch (e) { expect(e).toBeInstanceOf(TimeoutError); expect((e as Error).message).toContain('waitFor [timeout=10]'); expect((e as Error).message).toContain( colorText.default('Expected: ') + colorText.grey('') + colorText.green('true') + '\n' + colorText.default('Received: ') + colorText.grey('') + colorText.red('true'), ); } }); }); describe('using fake timers', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.runAllTimers(); vi.useRealTimers(); }); // The default amount value covers 5 microtasks. If the implementation creates more than that - need to increase the amount const waitForMicrotasks = async (amount = 5) => { let i = amount; while (i > 0) { // eslint-disable-next-line @typescript-eslint/await-thenable await (() => Promise.resolve()); i -= 1; } }; const advanceTime = (ms: number) => { vi.advanceTimersByTime(ms); return waitForMicrotasks(); }; const completeWaitFor = async () => { for (let i = 0; i < defaultParams.timeout / defaultParams.interval; i++) { await advanceTime(defaultParams.interval); } }; const defaultParams = { timeout: 1000, interval: 50, }; const timeout = 500; const interval = 50; const defaultTimeoutErrorMessage = (timeoutMs = timeout) => `waitFor [timeout=${String(timeoutMs === 1000 ? '1_000' : timeoutMs)}]`; it(`should call async predicate until it returns true`, async () => { let isWaitFinished = false; let predicateReturnValue = false; const predicate = vi.fn(async () => predicateReturnValue); void waitFor(predicate, {timeout, interval}).then(() => { isWaitFinished = true; }); await waitForMicrotasks(); expect(predicate).toBeCalledTimes(1); expect(isWaitFinished).toBe(false); await advanceTime(interval); expect(predicate).toBeCalledTimes(2); expect(isWaitFinished).toBe(false); predicateReturnValue = true; await advanceTime(interval); expect(predicate).toBeCalledTimes(3); expect(isWaitFinished).toBe(true); }); it(`should throw error if predicate didn't return true in time`, async () => { let error: Error | undefined; const predicate = vi.fn(async () => false); waitFor(predicate, {timeout, interval}).catch((e: unknown) => { error = e as Error; }); await completeWaitFor(); expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch(defaultTimeoutErrorMessage()); }); it('should handle long-running predicate fn', async () => { let isWaitFinished = false; let predicateReturnValue = false; const predicateDuration = 10; const predicate = vi.fn(async () => { await delay(predicateDuration); return predicateReturnValue; }); void waitFor(predicate, {timeout, interval}).then(() => { isWaitFinished = true; }); expect(predicate).toBeCalledTimes(1); await advanceTime(predicateDuration); expect(isWaitFinished).toBe(false); await advanceTime(interval); expect(predicate).toBeCalledTimes(2); await advanceTime(predicateDuration); expect(isWaitFinished).toBe(false); predicateReturnValue = true; await advanceTime(interval); expect(predicate).toBeCalledTimes(3); await advanceTime(predicateDuration); expect(isWaitFinished).toBe(true); }); it('should account for predicate execution time in overall waiting duration (predicate duration 100, timeout 500)', async () => { let error: Error | undefined; const predicateDuration = 100; const predicate = vi.fn(async () => { await delay(predicateDuration); return false; }); waitFor(predicate, {timeout, interval}).catch((e: unknown) => { error = e as Error; }); expect(predicate).toBeCalledTimes(1); // time passed: 0 await advanceTime(predicateDuration); await advanceTime(interval); // time passed: 150 expect(predicate).toBeCalledTimes(2); await advanceTime(predicateDuration); await advanceTime(interval); // time passed: 300 expect(predicate).toBeCalledTimes(3); await advanceTime(predicateDuration); await advanceTime(interval); // time passed: 450 expect(predicate).toBeCalledTimes(4); await advanceTime(predicateDuration); await advanceTime(interval); expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch(defaultTimeoutErrorMessage()); }); it(`should account for predicate execution time in overall waiting duration (predicate duration higher than timeout)`, async () => { let error: Error | undefined; const predicateDuration = timeout + 100; const predicate = vi.fn(async () => { await delay(predicateDuration); return false; }); waitFor(predicate, {timeout, interval}).catch((e: unknown) => { error = e as Error; }); expect(predicate).toBeCalledTimes(1); // time passed: 0 await advanceTime(predicateDuration); await advanceTime(interval); // time passed: 1100 expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch(defaultTimeoutErrorMessage()); }); it(`should not call predicate and throw error immediately if timeout is 0`, async () => { let error: Error | undefined; const predicate = vi.fn(async () => false); try { await waitFor(predicate, {timeout: 0, interval}); } catch (e) { error = e as Error; } expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch(`waitFor [timeout=0]`); }); it(`should use default timeout and interval`, async () => { let error: Error | undefined; const predicate = vi.fn(async () => false); waitFor(predicate).catch((e: unknown) => { error = e as Error; }); await completeWaitFor(); expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch(defaultTimeoutErrorMessage(1_000)); }); it(`should use default params if passed "undefined"s`, async () => { let error: Error | undefined; const predicate = vi.fn(async () => false); waitFor(predicate, {timeout: undefined, interval: undefined}).catch((e: unknown) => { error = e as Error; }); await completeWaitFor(); expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch(defaultTimeoutErrorMessage(1_000)); }); it(`should throw custom error message`, async () => { let error: Error | undefined; const predicate = vi.fn(async () => false); const message = 'custom error message'; waitFor(predicate, {timeout, interval, message}).catch((e: unknown) => { error = e as Error; }); await completeWaitFor(); expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch('Error: custom error message'); }); it(`should handle sync predicate`, async () => { let isWaitFinished = false; let predicateReturnValue = false; const predicate = vi.fn(() => predicateReturnValue); void waitFor(predicate, {timeout, interval}).then(() => { isWaitFinished = true; }); await waitForMicrotasks(); expect(predicate).toBeCalledTimes(1); expect(isWaitFinished).toBe(false); await advanceTime(interval); expect(predicate).toBeCalledTimes(2); expect(isWaitFinished).toBe(false); predicateReturnValue = true; await advanceTime(interval); expect(predicate).toBeCalledTimes(3); expect(isWaitFinished).toBe(true); }); it(`should throw if predicate throws error`, async () => { const predicate = vi.fn(async () => { throw new Error('thrown error'); }); let error: Error | undefined; waitFor(predicate, {timeout, interval}).catch((e: unknown) => { error = e as Error; }); await completeWaitFor(); expect(error).toBeInstanceOf(TimeoutError); expect(error?.message).toMatch('Error: thrown error'); }); }); });