Skip to content

Commit

Permalink
Converting signature in TestWaiter.beginAsync to take a Token
Browse files Browse the repository at this point in the history
  • Loading branch information
scalvert committed May 14, 2019
1 parent d7f3968 commit a104081
Show file tree
Hide file tree
Showing 15 changed files with 350 additions and 128 deletions.
6 changes: 0 additions & 6 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,5 @@ module.exports = {
// add your custom rules and overrides for node files here
}),
},
{
files: ['tests/**/*.[jt]s'],
env: {
qunit: true,
},
},
],
};
90 changes: 86 additions & 4 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
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,97 @@ let waiter = buildWaiter('friend-waiter');

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

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

#### Custom Tokens

The `beginAsync` method also can receive a custom token, if you need to specifically define a token to represent your async operation. _Note_ - due to the nature of
async operations, it's impossible to guarantee which order the `endAsync` calls will run. If you are making multiple `beginAsync`/`endAsync` calls wiithin a single
class or function, _you need to ensure the uniqueness of each async operation by ensuring the uniqueness of the tokens you provide_.

The following code _will not_ guarantee uniqueness, as the two `endAsync` calls _may_ be called in succession:

```js
import Component from '@ember/component';
import { buildWaiter } from 'ember-test-waiters';

let waiter = buildWaiter('friend-waiter');

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

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

action: {
someAction() {
let token = waiter.beginAsync(this);

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

The following code _will_ guarantee uniqueness, as the two `endAsync` calls _may_ be called in succession, but have unique tokens:

```js
import Component from '@ember/component';
import { buildWaiter } from 'ember-test-waiters';

let waiter = buildWaiter('friend-waiter');

export default class Friendz extends Component {
didInsertElement() {
let token = 'fist async';
waiter.beginAsync(token); // if a token is provided, `beginAsync` wil simply return it

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

action: {
someAction() {
let token = 'second async';
waiter.beginAsync(token);

someOtherAsync()
.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
35 changes: 25 additions & 10 deletions addon/test-waiter.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
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;
public nextToken: () => T;
private isRegistered = false;

items = new Map<T, ITestWaiterDebugInfo>();
items = new Map<Token, ITestWaiterDebugInfo>();

/**
* @public
* @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
17 changes: 8 additions & 9 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

module.exports = function(defaults) {
let app = new EmberAddon(defaults, {});

app.import('node_modules/qunit/qunit/qunit.js', {
type: 'test',
});

app.import('node_modules/qunit/qunit/qunit.css', {
type: 'test',
let app = new EmberAddon(defaults, {
// Add options here
});

app.import('vendor/shims/qunit.js', { type: 'test' });
/*
This build file specifies the options for the dummy test app of this
addon, located in `/tests/dummy`
This build file does *not* influence how the addon or the app using it
behave. You most likely want to be modifying `./index.js` or app's build file
*/

return app.toTree();
};
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@
"@ember/optional-features": "^0.6.3",
"@glimmer/env": "^0.1.7",
"@types/ember": "^3.0.29",
"@types/ember-qunit": "^3.4.5",
"@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/qunit": "^2.5.4",
"@types/rsvp": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.2",
Expand All @@ -45,13 +46,13 @@
"ember-cli-htmlbars-inline-precompile": "^1.0.3",
"ember-cli-inject-live-reload": "^1.8.2",
"ember-cli-sri": "^2.1.1",
"ember-cli-test-loader": "^2.2.0",
"ember-cli-typescript": "^1.5.0",
"ember-cli-uglify": "^2.1.0",
"ember-disable-prototype-extensions": "^1.1.3",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.1.0",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-qunit": "^3.4.1",
"ember-resolver": "^5.0.1",
"ember-source": "~3.7.0",
"ember-source-channel-url": "^1.1.0",
Expand All @@ -63,7 +64,7 @@
"eslint-plugin-prettier": "^3.0.1",
"loader.js": "^4.7.0",
"prettier": "^1.16.4",
"qunit": "^2.9.2",
"qunit-dom": "^0.8.0",
"typescript": "^3.3.3333"
},
"engines": {
Expand Down
7 changes: 0 additions & 7 deletions tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
{{content-for "body"}}
{{content-for "test-body"}}

<div id="qunit"></div>
<div id="qunit-fixture"></div>

<div id="ember-testing-container">
<div id="ember-testing"></div>
</div>

<script src="/testem.js" integrity=""></script>
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/test-support.js"></script>
Expand Down
33 changes: 6 additions & 27 deletions tests/test-helper.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,8 @@
import QUnit from 'qunit';
import AbstractTestLoader from 'ember-cli-test-loader/test-support/index';
import Application from '../app';
import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-qunit';

let moduleLoadFailures = [];
setApplication(Application.create(config.APP));

QUnit.done(function() {
if (moduleLoadFailures.length) {
throw new Error('\n' + moduleLoadFailures.join('\n'));
}
});

class TestLoader extends AbstractTestLoader {
moduleLoadFailure(moduleName, error) {
moduleLoadFailures.push(error);

QUnit.module('TestLoader Failures');
QUnit.test(moduleName + ': could not be loaded', function() {
throw error;
});
}
}

new TestLoader().loadModules();

QUnit.testDone(function() {
let testElementContainer = document.getElementById('ember-testing-container');
let testElementReset = testElementContainer.outerHTML;
testElementContainer.innerHTML = testElementReset;
});
start();
Loading

0 comments on commit a104081

Please sign in to comment.