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, + }; +}