Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converts the system to a token based system rather than arbitrary objects #8

Merged
merged 7 commits into from
May 14, 2019
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
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ember-test-waiters

This addon provides APIs to allow [@ember/test-helpers](https://github.com/emberjs/ember-test-helpers/) to play nicely
with other asynchronous events, such as an application that is waiting for a CSS transition or an IndexDB transaction.
with other asynchronous operations, such as an application that is waiting for a CSS transition or an IndexDB transaction.
The async helpers inside `@ember/test-helpers` return promises (i.e. `click`, `andThen`, `visit`, etc). Waiters run periodically
after each helper has executed until a predetermined condition is met. After the waiters finish, the next async helper
is executed and the process repeats.
Expand Down Expand Up @@ -29,7 +29,10 @@ ember install ember-test-waiters

### buildWaiter function

The `buildWaiter` function is, in most cases, all you will need to wait for async operations to complete before continuing tests.
The `buildWaiter` function is, in most cases, all you will need to wait for async operations to complete before continuing tests. It returns a waiter instance
scalvert marked this conversation as resolved.
Show resolved Hide resolved
that provides a number of methods. The key methods that allow you to control async behavior are `beginAsync` and `endAsync`, which are expected to be called as
a pair to _begin_ waiting and _end_ waiting respectively. The `beginAsync` method returns a `token`, which uniquely identifies that async operation. To mark the
async operation as complete, call `endAsync`, passing in the `token` that was returned from the prior `beginAsync` call.

```js
import Component from '@ember/component';
Expand All @@ -39,18 +42,23 @@ let waiter = buildWaiter('friend-waiter');

export default class Friendz extends Component {
didInsertElement() {
waiter.beginAsync(this);

someAsyncWork().then(() => {
waiter.endAsync(this);
});
let token = waiter.beginAsync();

someAsyncWork()
.then(() => {
//... some work
})
.finally(() => {
waiter.endAsync(token);
});
}
}
```

### waitForPromise function

This addon also provides a `waitForPromise` function, which can be used to wrap a promise to register it with the test waiter system.
This addon also provides a `waitForPromise` function, which can be used to wrap a promise to register it with the test waiter system. _Note_: the
`waitForPromise` function will ensure that `endAsync` is called correctly in the `finally` call of your promise.

```js
import Component from '@ember/component';
Expand Down
2 changes: 1 addition & 1 deletion addon/build-waiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import NoopTestWaiter from './noop-test-waiter';
* }
* }
*/
export default function buildWaiter(name: string): ITestWaiter<unknown> {
export default function buildWaiter(name: string): ITestWaiter {
if (DEBUG) {
return new TestWaiter(name);
}
Expand Down
8 changes: 5 additions & 3 deletions addon/noop-test-waiter.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { ITestWaiter } from './types';
import { ITestWaiter, Token } from './types';

/**
* A class providing a production, noop replacement for the {TestWaiter<T>} class.
*
* @public
* @class TestWaiter<T>
*/
export default class NoopTestWaiter<T> implements ITestWaiter<T> {
export default class NoopTestWaiter implements ITestWaiter {
name: string;
constructor(name: string) {
this.name = name;
}

beginAsync(): void {}
beginAsync(): Token {
return this;
}

endAsync(): void {}

Expand Down
33 changes: 24 additions & 9 deletions addon/test-waiter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { ITestWaiter, WaiterName, ITestWaiterDebugInfo } from './types';
import { ITestWaiter, WaiterName, ITestWaiterDebugInfo, Token } from './types';
import { register } from './waiter-manager';

let token: number = 0;

function getNextToken(): number {
return token++;
}

/**
* A class providing creation, registration and async waiting functionality.
*
* @public
* @class TestWaiter<T>
*/
export default class TestWaiter<T> implements ITestWaiter<T> {
export default class TestWaiter<T = Token> implements ITestWaiter<T> {
public name: WaiterName;
private nextToken: () => T;
private isRegistered = false;

items = new Map<T, ITestWaiterDebugInfo>();
Expand All @@ -18,8 +25,10 @@ export default class TestWaiter<T> implements ITestWaiter<T> {
* @constructor
* @param name {WaiterName} the name of the test waiter
*/
constructor(name: WaiterName) {
constructor(name: WaiterName, nextToken?: () => T) {
this.name = name;
// @ts-ignore
this.nextToken = nextToken || getNextToken;
}

/**
Expand Down Expand Up @@ -50,17 +59,23 @@ export default class TestWaiter<T> implements ITestWaiter<T> {
* @param item {T} The item to register for waiting
* @param label {string} An optional label to identify the item
*/
beginAsync(item: T, label?: string) {
beginAsync(token: T = this.nextToken(), label?: string) {
this.register();

if (this.items.has(token)) {
throw new Error(`beginAsync called for ${token} but it is already pending.`);
}

let error = new Error();

this.items.set(item, {
this.items.set(token, {
get stack() {
return error.stack;
},
label,
});

return token;
}

/**
Expand All @@ -72,12 +87,12 @@ export default class TestWaiter<T> implements ITestWaiter<T> {
* @method endAsync
* @param item {T} The item to that was registered for waiting
*/
endAsync(item: T) {
if (!this.items.has(item)) {
throw new Error(`endAsync called for ${item} but item is not currently pending.`);
endAsync(token: T) {
if (!this.items.has(token)) {
throw new Error(`endAsync called for ${token} but it is not currently pending.`);
}

this.items.delete(item);
this.items.delete(token);
}

/**
Expand Down
7 changes: 4 additions & 3 deletions addon/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
export type WaiterName = string;
export type Token = unknown;

export interface IWaiter {
name: WaiterName;
waitUntil(): boolean;
debugInfo(): unknown;
}

export interface ITestWaiter<T> extends IWaiter {
beginAsync(item: T, label?: string): void;
endAsync(item: T): void;
export interface ITestWaiter<T = Token> extends IWaiter {
beginAsync(token?: T, label?: string): T;
endAsync(token: T): void;
}

export interface ITestWaiterDebugInfo {
Expand Down
2 changes: 1 addition & 1 deletion addon/wait-for-promise.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DEBUG } from '@glimmer/env';
import TestWaiter from './test-waiter';

const PROMISE_WAITER = new TestWaiter('promise-waiter');
const PROMISE_WAITER = new TestWaiter<Promise<unknown>>('promise-waiter');

/**
A convenient utility function to simplify waiting for a promise.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"@types/ember-test-helpers": "^1.0.4",
"@types/ember-testing-helpers": "^0.0.3",
"@types/ember__test-helpers": "^0.7.7",
"@types/qunit": "^2.9.0",
"@types/rsvp": "^4.0.2",
"@types/qunit": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.2",
"documentation": "^9.3.1",
Expand Down
112 changes: 108 additions & 4 deletions tests/unit/test-waiter-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,30 @@ module('test-waiter', function(hooks) {
assert.equal(waiter.name, name);
});

test('test waiters automatically register when beginAsync is invoked', function(assert) {
test('test waiters return a token from beginAsync when no token provided', function(assert) {
let waiter = new TestWaiter('my-waiter');

let token = waiter.beginAsync();

assert.ok(typeof token === 'number', 'A token was returned from beginAsync');
});

test('test waiters automatically register when beginAsync is invoked when no token provied', function(assert) {
let waiter = new TestWaiter('my-waiter');

let token = waiter.beginAsync();

let registeredWaiters = getWaiters();

assert.equal(registeredWaiters[0], waiter, 'The waiter is registered');
assert.deepEqual(
(<TestWaiter>registeredWaiters[0]).items.keys().next().value,
token,
'Waiter item is in items'
);
});

test('test waiters automatically register when beginAsync is invoked using a custom token', function(assert) {
let waiter = new TestWaiter('my-waiter');
let waiterItem = {};

Expand All @@ -26,27 +49,108 @@ module('test-waiter', function(hooks) {

assert.equal(registeredWaiters[0], waiter, 'The waiter is registered');
assert.deepEqual(
(<TestWaiter<object>>registeredWaiters[0]).items.keys().next().value,
(<TestWaiter>registeredWaiters[0]).items.keys().next().value,
{},
'Waiter item is in items'
);
});

test('test waiters removes item from items map when endAsync is invoked', function(assert) {
let waiter = new TestWaiter('my-waiter');

let token = waiter.beginAsync();
waiter.endAsync(token);
let registeredWaiters = getWaiters();

assert.equal((<TestWaiter>registeredWaiters[0]).items.size, 0);
});

test('test waiters removes item from items map when endAsync is invoked using a custom token', function(assert) {
let waiter = new TestWaiter('my-waiter');
let waiterItem = {};

waiter.beginAsync(waiterItem);
waiter.endAsync(waiterItem);
let registeredWaiters = getWaiters();

assert.equal((<TestWaiter<object>>registeredWaiters[0]).items.size, 0);
assert.equal((<TestWaiter>registeredWaiters[0]).items.size, 0);
});

test('beginAsync will throw if a prior call to beginAsync with the same token occurred', function(assert) {
let waiter = new TestWaiter('my-waiter');

assert.throws(
() => {
let token = waiter.beginAsync();
waiter.beginAsync(token);
},
Error,
/beginAsync called for [object Object] but item already pending./
);
});

test('beginAsync will throw if a prior call to beginAsync with the same token occurred', function(assert) {
let waiter = new TestWaiter('my-waiter');
let token = {};

assert.throws(
() => {
waiter.beginAsync(token);
waiter.beginAsync(token);
},
Error,
/beginAsync called for [object Object] but item already pending./
);
});

test('endAsync will throw if a prior call to beginAsync with the same token did not occur', function(assert) {
let waiter = new TestWaiter('my-waiter');
let token = 0;

assert.throws(
() => {
waiter.endAsync(token);
},
Error,
/endAsync called for [object Object] but item is not currently pending./
);
});

test('endAsync will throw if a prior call to beginAsync with the same token did not occur using custom token', function(assert) {
let waiter = new TestWaiter('my-waiter');
let waiterItem = {};

assert.throws(
() => {
waiter.endAsync(waiterItem);
},
Error,
/endAsync called for [object Object] but item is not currently pending./
);
});

test('endAsync will throw if endAsync called twice in a row with the same token', function(assert) {
let waiter = new TestWaiter('my-waiter');
let token = waiter.beginAsync();

waiter.endAsync(token);

assert.throws(
() => {
waiter.endAsync(token);
},
Error,
/endAsync called for [object Object] but item is not currently pending./
);
});

test('endAsync will throw if a prior call to beginAsync with the same waiter item did not occur', function(assert) {
test('endAsync will throw if endAsync called twice in a row with the same token using custom token', function(assert) {
let waiter = new TestWaiter('my-waiter');
let waiterItem = {};

waiter.beginAsync(waiterItem);
waiter.endAsync(waiterItem);

assert.throws(
() => {
waiter.endAsync(waiterItem);
Expand Down
14 changes: 12 additions & 2 deletions tests/unit/waiter-manager-noop-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,24 @@ if (!DEBUG) {
assert.deepEqual(waiters, ['first']);
});

test('a NoopTestWaiter always returns true from waitUntil', function(assert) {
let waiter = buildWaiter('first');

assert.ok(waiter.waitUntil(), 'waitUntil returns true');
let token = waiter.beginAsync();
scalvert marked this conversation as resolved.
Show resolved Hide resolved
assert.ok(waiter.waitUntil(), 'waitUntil returns true');
waiter.endAsync(token);
assert.ok(waiter.waitUntil(), 'waitUntil returns true');
});

test('a NoopTestWaiter always returns true from waitUntil', function(assert) {
let waiter = buildWaiter('first');
let waiterItem = {};

assert.ok(waiter.waitUntil(), 'waitUntil returns true');
waiter.beginAsync(waiterItem);
let token = waiter.beginAsync(waiterItem);
assert.ok(waiter.waitUntil(), 'waitUntil returns true');
waiter.endAsync(waiterItem);
waiter.endAsync(token);
assert.ok(waiter.waitUntil(), 'waitUntil returns true');
});
});
Expand Down