Skip to content

Commit

Permalink
Implementation ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
BinaryMuse committed Jan 7, 2017
1 parent 299fe1f commit 14648c2
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules/
/npm-debug.log*
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# test-until

A utility that returns a promise that resolves when the passed function returns true. It works like Jasmine's `waitsFor`, but Promise based.

## Installation

Node.js:

`npm install --save[-dev] test-until`

## Requirements

test-until requires a global variable `Promise` which needs to be a A+ compliant promise implementation (such as is available by default in [most modern browsers](http://caniuse.com/#feat=promises)). If you need to polyfill `Promise` for any reason (such as supporting IE 11), I recommend [promise-polyfill](https://github.com/taylorhakes/promise-polyfill).

## Usage

```javascript
var promise = until(checkFunc, message, timeout)
```

* `checkFunc` - A function that returns a truthy value once the promise should be resolved. `until` will call this function repeatedly until it returns `true` or the timeout elapses.
* `message` *(optional)* - A message to help identify failing cases. For example, setting this to `"value == 42"` will reject with an error of `"timed out waiting until value == 42"` if it times out. Defaults to `"something happens"`.
* `timeout` *(optional)* - The number of milliseconds to wait before timing out and rejecting the promise. Defaults to `1000`.

The three arguments can be supplied in any order depending on your preferences. For example, putting the message first can make the line read a little more like English:

```javascript
until('we know the answer to life, the universe, and everything', function () { return val === 42 }, 500)
```

## Example with Test Framework

Here's an example using the [Mocha](https://mochajs.org/) testing framework.

```javascript
var until = require('test-until')

describe('something', function () {
it('tests things', function (done) {
var val = 0
setTimeout(function () { val = 42 }, 100)
var promise = until(function () { val === 42 })
promise.then(function() {
// after 100ms, `val` will be set to `42`
// and the promise returned from `until` will resolve
done()
})
})
})
```

test-until reads and works even better with access to `async`/`await` in your tests, allowing you to wait for multiple async conditions with no callbacks in a manner that reads much like English:

```javascript
import until from 'test-until'

describe('something', function () {
it('tests things', async function () {
let val = 0
let otherVal = 0
setTimeout(() => val = 42, 100)
setTimeout(() => otherVal = 2048, 200)
// Awaiting a rejected promise will cause a synchronous `throw`,
// which will reject the promise returned from the async function
// and will fail the test.
await until('val is 42', () => val === 42)
await until('otherVal is 2048', () => otherVal === 2048)
})
})
```

## Advanced Usage

### Setting the Default Timeout

Use `until.setDefaultTimeout(ms)` to set the default timeout. You can easily set this to different values in different parts of your test suite by setting it in a `beforeEach`

```javascript
import until from 'test-until'

// Set a global default timeout
beforeEach(function () {
until.setDefaultTimeout(500)
})

describe('slow stuff', function () {
beforeEach(function () {
// Make it a bit longer for these tests
until.setDefaultTimeout(1000)
})
// ...
})
```

Passing a falsy `ms` resets to the default of `1000`.

### Setting the Error Message from Inside the Check Function

The check function gets called with a special argument called `setError` which allows the check function to specify an error to return if the `until` call times out. This can be useful when integrating `until` with other test assertions; for example, here's a snippet that will wait for `val` to be `42` and will reject with an actual Chai assertion error if it fails.

```javascript
import until from 'test-until'
import {assert} from 'chai'

describe('something', function () {
it('tests things', async function () {
let val = 0
setTimeout(() => val = 42, 100)
await until(setError => {
try {
assert.equal(val, 42)
return true
} catch (err) {
return setError(err) // explicitly set the error
}
})
})
})
```

## Development

The test suite uses newer JavaScript features, so you need Node.js 6+ in order to run it. Run `npm test` to run the suite.
69 changes: 69 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
let defaultTimeout = 1000;

function until(_latch, _message, _timeout) {
var start = new Date().getTime();

var latchFunction = null;
var message = null;
var timeout = null;

if (arguments.length > 3) {
throw new Error('until only takes up to 3 args');
}

for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case 'function':
latchFunction = arguments[i];
break;
case 'string':
message = arguments[i];
break;
case 'number':
timeout = arguments[i];
break;
}
}

message = message || 'something happens';
timeout = timeout || defaultTimeout;
var error;

var setError = function(err) {
if (typeof err === 'string') {
error = new Error(err);
} else {
error = err;
}
return false;
};

return new Promise(function(resolve, reject) {
var checker = function() {
var result = latchFunction(setError);
if (result) { return resolve(result); }

var now = new Date().getTime();
var delta = now - start;
if (delta > timeout) {
if (!error) {
error = new Error(`timed out waiting until ${message}`);
}
error.message = 'async(' + timeout + 'ms): ' + error.message;
return reject(error);
} else {
return setTimeout(checker);
}
};
checker();
});
}

until.setDefaultTimeout = function(ms) {
if (!ms) {
ms = 1000;
}
defaultTimeout = ms;
};

module.exports = until;
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Utility method that returns a promise that resolves when a condition returns true",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "mocha test"
},
"keywords": [
"unit testing",
Expand All @@ -14,5 +14,9 @@
"promise"
],
"author": "Michelle Tilley <michelle@michelletilley.net>",
"license": "MIT"
"license": "MIT",
"devDependencies": {
"chai": "^3.5.0",
"mocha": "^3.2.0"
}
}
71 changes: 71 additions & 0 deletions test/until.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const assert = require('chai').assert;

const until = require('../');

describe('until', function() {
it('returns a promise that resolves when the latch function returns a truthy value', function(done) {
let val = null;
const promise = until(() => val === 42);
setTimeout(() => val = 42, 20);
promise.then(() => {
done();
});
});

it('returns a promise that rejects if the latch function never returns true with the timeout', function(done) {
let val = null;
const promise = until(() => val === 42, 20);
setTimeout(() => val = 42, 50);
promise.catch(err => {
assert.match(err.message, /async.*20.*timed out waiting until something happens/);
done();
});
});

it('allows mixing the order of the arguments', function(done) {
const val = null;
const promise = until(10, 'value equals 42', () => val === 42);
promise.catch(err => {
assert.match(err.message, /async.*10.*timed out waiting until value equals 42/);
done();
});
});

it('allows setting the error explicitly', function(done) {
let val = 0;
until(15, setError => {
val++;
return setError(new Error('Failure in pass ' + val));
}).catch(err => {
assert.equal(err.message, 'async(15ms): Failure in pass ' + val);
assert.operator(val, '>', 0);
done();
});
});

describe('the default timeout', function() {
beforeEach(function() {
until.setDefaultTimeout();
});

it('starts at 1000ms', function(done) {
until(() => false).catch(err => {
assert.match(err.message, /async.*1000.*timed out/);
done();
});
});

describe('it can be set', function() {
beforeEach(function() {
until.setDefaultTimeout(20);
});

it('and is sticky', function(done) {
until(() => false).catch(err => {
assert.match(err.message, /async.*20.*timed out/);
done();
});
});
});
});
});

0 comments on commit 14648c2

Please sign in to comment.