From 36f08055909f371fd9cbe3480ea813b963aea22b Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Tue, 4 May 2021 19:10:10 +0200 Subject: [PATCH] unify usage of sleepAsync and add tests The tests mock JS setTimeout API. However promise.resolve() is not working without flushing the promise queue (which could be done just by awaiting Promise.resolve()), similar issue has been discussed in facebook/jest#2157. --- src/infrastructure/Threading/AsyncSleep.ts | 5 ++ .../SelectableTree/SelectableTree.vue | 6 +- .../{ => Threading}/AsyncLazy.spec.ts | 2 +- .../Threading/AsyncSleep.spec.ts | 79 +++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/infrastructure/Threading/AsyncSleep.ts rename tests/unit/infrastructure/{ => Threading}/AsyncLazy.spec.ts (94%) create mode 100644 tests/unit/infrastructure/Threading/AsyncSleep.spec.ts diff --git a/src/infrastructure/Threading/AsyncSleep.ts b/src/infrastructure/Threading/AsyncSleep.ts new file mode 100644 index 00000000..ff0388af --- /dev/null +++ b/src/infrastructure/Threading/AsyncSleep.ts @@ -0,0 +1,5 @@ +export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void; + +export function sleepAsync(time: number, scheduler: SchedulerType = setTimeout) { + return new Promise((resolve) => scheduler(() => resolve(undefined), time)); +} diff --git a/src/presentation/components/Scripts/ScriptsTree/SelectableTree/SelectableTree.vue b/src/presentation/components/Scripts/ScriptsTree/SelectableTree/SelectableTree.vue index c4e36095..8746c433 100644 --- a/src/presentation/components/Scripts/ScriptsTree/SelectableTree/SelectableTree.vue +++ b/src/presentation/components/Scripts/ScriptsTree/SelectableTree/SelectableTree.vue @@ -22,7 +22,8 @@ import LiquorTree from 'liquor-tree'; import Node from './Node/Node.vue'; import { INode } from './Node/INode'; import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator'; -import { INodeSelectedEvent } from './/INodeSelectedEvent'; +import { INodeSelectedEvent } from './INodeSelectedEvent'; +import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep'; import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater'; import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions'; import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter'; @@ -121,7 +122,6 @@ function recurseDown( async function tryUntilDefinedAsync( accessor: () => T | undefined, delayInMs: number, maxTries: number): Promise { - const sleepAsync = () => new Promise(((resolve) => setTimeout(resolve, delayInMs))); let triesLeft = maxTries; let value: T; while (triesLeft !== 0) { @@ -130,7 +130,7 @@ async function tryUntilDefinedAsync( return value; } triesLeft--; - await sleepAsync(); + await sleepAsync(delayInMs); } return value; } diff --git a/tests/unit/infrastructure/AsyncLazy.spec.ts b/tests/unit/infrastructure/Threading/AsyncLazy.spec.ts similarity index 94% rename from tests/unit/infrastructure/AsyncLazy.spec.ts rename to tests/unit/infrastructure/Threading/AsyncLazy.spec.ts index 003f5edf..45ca1d15 100644 --- a/tests/unit/infrastructure/AsyncLazy.spec.ts +++ b/tests/unit/infrastructure/Threading/AsyncLazy.spec.ts @@ -1,6 +1,7 @@ import 'mocha'; import { expect } from 'chai'; import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; +import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep'; describe('AsyncLazy', () => { it('returns value from lambda', async () => { @@ -33,7 +34,6 @@ describe('AsyncLazy', () => { }); it('when running long-running task in parallel', async () => { // act - const sleepAsync = (time: number) => new Promise(((resolve) => setTimeout(resolve, time))); const sut = new AsyncLazy(async () => { await sleepAsync(100); totalExecuted++; diff --git a/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts b/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts new file mode 100644 index 00000000..9172bd71 --- /dev/null +++ b/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts @@ -0,0 +1,79 @@ +import 'mocha'; +import { expect } from 'chai'; +import { sleepAsync, SchedulerType } from '@/infrastructure/Threading/AsyncSleep'; + +describe('AsyncSleep', () => { + it('fulfills after delay', async () => { + // arrange + const delayInMs = 10; + const scheduler = new SchedulerMock(); + // act + const sleep = sleepAsync(delayInMs, scheduler.mock); + const promiseState = watchPromiseState(sleep); + scheduler.tickNext(delayInMs); + await flushPromiseResolutionQueue(); + // assert + const actual = promiseState.isFulfilled(); + expect(actual).to.equal(true); + }); + it('pending before delay', async () => { + // arrange + const delayInMs = 10; + const scheduler = new SchedulerMock(); + // act + const sleep = sleepAsync(delayInMs, scheduler.mock); + const promiseState = watchPromiseState(sleep); + scheduler.tickNext(delayInMs / 5); + await flushPromiseResolutionQueue(); + // assert + const actual = promiseState.isPending(); + expect(actual).to.equal(true); + }); +}); + +function flushPromiseResolutionQueue() { + return Promise.resolve(); +} + +class SchedulerMock { + public readonly mock: SchedulerType; + private currentTime = 0; + private scheduledActions = new Array<{time: number, action: (...args: any[]) => void}>(); + constructor() { + this.mock = (callback: (...args: any[]) => void, ms: number) => { + this.scheduledActions.push({ time: this.currentTime + ms, action: callback }); + }; + } + public tickNext(ms: number) { + const newTime = this.currentTime + ms; + let newActions = this.scheduledActions; + for (const action of this.scheduledActions) { + if (newTime >= action.time) { + newActions = newActions.filter((a) => a !== action); + action.action(); + } + } + this.scheduledActions = newActions; + } +} + +function watchPromiseState(promise: Promise) { + let isPending = true; + let isRejected = false; + let isFulfilled = false; + promise.then( + () => { + isFulfilled = true; + isPending = false; + }, + () => { + isRejected = true; + isPending = false; + }, + ); + return { + isFulfilled: () => isFulfilled, + isPending: () => isPending, + isRejected: () => isRejected, + }; +}