Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion src/helpers/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,63 @@ describe('Helper tests', () => {
expect(countListeners()).toBe(0);
});

test('waiter is finished', async () => {
test('waiter is resolved', async () => {
const myWaiter = waiter();
myWaiter.resolve();
await myWaiter;
expect(myWaiter.isFinished).toBe(true);
expect(myWaiter.isRejected).toBe(false);
expect(myWaiter.isResolved).toBe(true);
});

test('waiter is resolved with value', async () => {
const myWaiter = waiter<string>();
const value = 'my resolve result';
myWaiter.resolve(value);
const result = await myWaiter;
expect(result).toBe(value);
expect(myWaiter.isFinished).toBe(true);
expect(myWaiter.isRejected).toBe(false);
expect(myWaiter.isResolved).toBe(true);
});

test('waiter is finished (ensure finish alias works)', async () => {
const myWaiter = waiter();
myWaiter.finish();
await myWaiter;
expect(myWaiter.isFinished).toBe(true);
expect(myWaiter.isRejected).toBe(false);
expect(myWaiter.isResolved).toBe(true);
});

test('waiter is rejected', async () => {
const myWaiter = waiter();
const error = new Error('Waiter was rejected');
myWaiter.reject(error);
await expect(myWaiter).rejects.toThrow(error);
expect(myWaiter.isFinished).toBe(true);
expect(myWaiter.isRejected).toBe(true);
expect(myWaiter.isResolved).toBe(false);
});

test('waiter is rejected with error type', async () => {
class MyError extends Error {
readonly name = 'MyError';
}
const myWaiter = waiter<void, MyError>();
const error = new MyError('MyError test instance');
myWaiter.reject(error);
await expect(myWaiter).rejects.toThrow(error);
expect(myWaiter.isFinished).toBe(true);
expect(myWaiter.isRejected).toBe(true);
expect(myWaiter.isResolved).toBe(false);

// Expect other error types to cause a typescript error
class OtherError extends Error {
readonly name = 'OtherError';
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
myWaiter.reject(new OtherError('OtherError test instance'));
});
});
29 changes: 23 additions & 6 deletions src/helpers/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,26 +105,43 @@ export function stopwatch(): Stopwatch {
return result;
}

export type Waiter<T> = Promise<T> & {
export type Waiter<T = void, E = Error> = Promise<T> & {
/** Alias for `resolve` */
finish: (result: T) => void;
resolve: (result: T) => void;
reject: (error: E) => void;
/** True if the promise is resolved or rejected */
isFinished: boolean;
/** True only if the promise is resolved */
isResolved: boolean;
/** True only if the promise is rejected */
isRejected: boolean;
};

/**
* Creates a `Waiter` promise that can be resolved at a later time with a return value.
* Creates a `Waiter` promise that can be resolved or rejected at a later time.
* @returns Waiter
*/
export function waiter<T = void>(): Waiter<T> {
export function waiter<T = void, E = Error>(): Waiter<T, E> {
let resolveFn: (result: T) => void;
const promise = new Promise<T>(resolve => {
let rejectFn: (error: E) => void;
const promise = new Promise<T>((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
});
const completer = {
finish: (result: T) => {
void Object.assign(promise, { isFinished: true });
finish: (result: T) => completer.resolve(result),
resolve: (result: T) => {
void Object.assign(promise, { isFinished: true, isResolved: true });
resolveFn(result);
},
reject: (error: E) => {
void Object.assign(promise, { isFinished: true, isRejected: true });
rejectFn(error);
},
isFinished: false,
isResolved: false,
isRejected: false,
};
return Object.assign(promise, completer);
}