Skip to content

Commit

Permalink
feat(component-tester): add waitForElement method and options (#32)
Browse files Browse the repository at this point in the history
* feat(component-tester): add waitForElement method and options

* doc(component-tester): fix default interval value for waitForElement

* feat(component-tester): replace ComponentTester.waitForElement by main waitFor function

* refactor(component-tester): lint wait.js
  • Loading branch information
tkhyn authored and EisenbergEffect committed Jan 21, 2017
1 parent 385422f commit 65eb382
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 1 deletion.
56 changes: 56 additions & 0 deletions doc/article/en-US/testing-components.md
Expand Up @@ -330,3 +330,59 @@ The `ComponentTester` exposes a set of properties that can be handy when doing a
* `unbind` - Manually handles `unbind`.
* `attached` - Manually handles `attached`.
* `detached` - Manually handles `detached`.
* `waitForElement` and `waitForElements` - Waits until one or several elements are present / absent. See below.

## [Testing complex components](aurelia-doc://section/6/version/1.0.0)

In some cases, the tested element is not rendered yet when the `component.create()` promise is resolved, and therefore when the actual test starts. For these situations, `aurelia-testing` and `ComponentTester` expose helper methods and functions to wait for tested elements to be present in the page.

### Waiting for element(s)

If you want to wait for elements that can be looked up in the DOM using a query passed to `querySelector` or `querySelectorAll`, you can use one of the following:

* `ComponentTester.waitForElement` or `ComponentTester.waitForElements`: to wait for one or several HTML element(s) within the tested component. The query is carried out using `querySelector` and `querySelectorAll`, respectively.
* `waitForDocumentElement` or `waitForDocumentElements` (imported from `aurelia-testing`): to wait for one or several HTML element(s) within the document, not restricted to the descendants of the tested component. This is especially useful if you want to wait for elements created by third-party libraries such as context menus, date pickers, etc.

All these methods and functions take 2 arguments:

* `selector` (mandatory): is a selector string to look up the wanted element(s). It must be compatible with `querySelector` and `querySelectorAll`
* `options` is an object that can have the following properties:
- `present`: `true` to test for presence, `false` for absence (defaults to `true`)
- `interval`: the polling interval (defaults to 50ms)
- `timeout`: the timeout (defaults to 5s)

They all return a `Promise` that resolves to an `Element` (`waitForElement`) or a `NodeList` (`waitForElements`). The `Promise` is rejected in the event of a timeout. The returned `Promise` can be used to execute some testing code only once a given element has been detected to be present or absent, either because the component was slow to be fully rendered or because the test relies on asynchronous actions such as events or animations.

### Waiting for matches to complex queries ... or anything else

If your query is complex (with non-trivial jQuery lookups for example), or you want to wait for the result of a callback to be something else than `null`, you can use the higher-level `waitFor` function imported from `aurelia-testing`.

`waitFor(getter, options)` works exactly the same way as the previously described methods and functions, but takes a callback (`getter`) as the first argument instead of a selector string. `waitFor` internally calls `getter` with no arguments at regular intervals times until the returned value is anything else than `null`, an empty `NodeList` or jQuery set. The returned `Promise` will resolve to the result of `getter()`.

### Examples

<code-listing heading="Here is how to wait for the `firstName` input control from the example above:">
<source-code lang="JavaScript">
component.waitForElement('.firstName').then((nameElement) => {
expect(nameElement.innerHTML).toBe('Bob');
done();
});
</source-code>
</code-listing>

<code-listing heading="... and here is the same using jQuery:">
<source-code lang="JavaScript">
import {waitFor} from 'aurelia-testing';

waitFor(() => $('.firstName')).then((nameElement) => {
expect(nameElement.html()).toBe('Bob');
done();
});
</source-code>
</code-listing>

<code-listing heading="Waiting for the same element to be absent but timeout after 2s is as easy as:">
<source-code lang="JavaScript">
component.waitForElement('.firstName', {present: false, timeout: 2000}).then(done);
</source-code>
</code-listing>
6 changes: 5 additions & 1 deletion src/aurelia-testing.js
@@ -1,6 +1,7 @@
import {CompileSpy} from './compile-spy';
import {ViewSpy} from './view-spy';
import {StageComponent, ComponentTester} from './component-tester';
import {waitFor, waitForDocumentElement, waitForDocumentElements} from './wait';

function configure(config) {
config.globalResources(
Expand All @@ -14,5 +15,8 @@ export {
ViewSpy,
StageComponent,
ComponentTester,
configure
configure,
waitFor,
waitForDocumentElement,
waitForDocumentElements
};
8 changes: 8 additions & 0 deletions src/component-tester.js
Expand Up @@ -118,4 +118,12 @@ export class ComponentTester {
setTimeout(() => resolve(), 0);
});
}

waitForElement(selector: string, options: any): Promise<Element> {
return waitFor(() => this.element.querySelector(selector), options);
}

waitForElements(selector: string, options: any): Promise<Element> {
return waitFor(() => this.element.querySelectorAll(selector), options);
}
}
50 changes: 50 additions & 0 deletions src/wait.js
@@ -0,0 +1,50 @@
/**
* Generic function to wait for something to happen. Uses polling
* @param getter: a getter function that returns anything else than `null` or an
* empty array or an empty jQuery object when the
* condition is met
* @param options: lookup options, defaults to
* `{present: true, interval: 50, timeout: 5000}`
*/
export function waitFor(getter: () => any, options: any): Promise<any> {
// prevents infinite recursion if the request times out
let timedOut = false;

options = Object.assign({
present: true,
interval: 50,
timeout: 5000
}, options);


function wait() {
let element = getter();
// boolean is needed here, hence the length > 0
let found = element !== null && (!(element instanceof NodeList) &&
!element.jquery || element.length > 0);

if (!options.present ^ found || timedOut) {
return Promise.resolve(element);
}

return new Promise(rs => setTimeout(rs, options.interval)).then(wait);
}

return Promise.race([
new Promise(
(rs, rj) => setTimeout(() => {
timedOut = true;
rj(options.present ? 'Element not found' : 'Element not removed');
}, options.timeout)
),
wait()
]);
}

export function waitForDocumentElement(selector: string, options: any): Promise<Element> {
return waitFor(() => document.querySelector(selector), options);
}

export function waitForDocumentElements(selector: string, options: any): Promise<Element> {
return waitFor(() => document.querySelectorAll(selector), options);
}

0 comments on commit 65eb382

Please sign in to comment.