@bigtest/convergence
new Convergence (timeout)
import Convergence from '@bigtest/convergence'
Convergences are powerful, immutable, reusable, and composable assertions that allow you to know immediately when a desired state is achieved.
setTimeout(() => foo = 'bar', 100)
await new Convergence().when(() => foo === 'bar')
console.log(foo) // => "bar"
By default, a convergence will converge before or after 2000ms
depending on the type of assertions defined. This can be configured
by providing a timeout when initializing the convergence, or by
using the #timeout()
method.
new Convergence(100)
new Convergence().timeout(5000)
Using #when()
, the assertions will run multiple times
until they pass. Similarly, #always()
ensures that
assertions keep passing for a period of time.
// converges when `foo` is equal to `'bar'` within 100ms
new Convergence(100).when(() => foo === 'bar')
// converges after `foo` is equal to `'bar'` for at least 100ms
new Convergence(100).always(() => foo === 'bar')
Convergences are immutable, and as such, it’s methods return new
instances. This allows you to compose multiple convergences and
start each of them separately using their respective
#run()
methods.
let converge = new Convergence(300)
let convergeFoo = converge.when(() => foo === 'foo')
let convergeFooBar = convergeFoo.when(() => foo === 'bar')
let convergeFooBarBaz = convergeFooBar.when(() => foo === 'baz')
setTimeout(() => foo = 'foo', 100)
setTimeout(() => foo = 'bar', 200)
setTimeout(() => foo = 'baz', 150)
// resolves after 100ms
convergeFoo.run()
// resolves after 200ms
convergeFooBar.run()
// rejects after 300ms since it wasn't `baz` _after_ `bar`
convergeFooBarBaz.run()
Convergences are also thennable, which immediately invokes
#run()
. This allows them to be able to be used anywhere
Promises can be used in most cases.
async function onceBarAlwaysBar() {
await new Convergence()
.when(() => foo === 'bar')
.always(() => foo === 'bar')
}
Promise.race([
onceBarAlwaysBar(),
new Convergence().when(() => foo === 'baz')
])
Methods
isConvergence obj Boolean
-
obj
Object
A possible convergence object -
returns
Boolean
Returns true
if the object has common convergence properties of
the correct type.
let result = maybeConvergence()
if (isConvergence(result)) {
await result.do(something).timeout(100)
} else {
something(result)
}
always assertion, timeout Convergence
-
assertion
Function
The assertion to converge on -
timeout
Number
The timeout to use, capped at the remaining timeout. -
returns
Convergence
A new convergence instance
Returns a new convergence instance with an additional assertion. This assertion is run repeatedly to ensure it passes throughout the timeout. If the assertion fails at any point during the timeout, the convergence will fail.
// would converge after `foo` remains `'foo'` for at least 100ms
new Convergence(100).always(() => foo === 'foo')
When an always assertion is encountered at the end of a
convergence, the timeout defaults to the remaining time for the
current running instance; minumum 20ms
. When not at the ned of
a convergence, it defaults to one-tenth of the total timeout.
let convergeFooThenBar = new Convergence(1000)
// would continue after `foo` remains `'foo'` for at least 100ms
.always(() => foo === 'foo')
// then have any time remaining to converge on `foo` being `'bar'`
.when(() => foo === 'bar')
Given a timeout, it is capped at the remaining timeout for the current running instance.
let convergeFooThenBar = new Convergence(100)
// would continue after `foo` remains `'foo'` for at least 50ms
.always(() => foo === 'foo', 50)
// then have 50ms remaining to converge on `foo` being `'bar'`
.when(() => foo === 'bar')
// and a maximum of ~50ms to converge on it remaining `bar`
.always(() => foo === 'bar', 100)
append convergence Convergence
-
convergence
Convergence
A convergence instance -
returns
Convergence
A new convergence instance
Appends another convergence’s queue to this convergence’s queue to allow composing different convergences together.
// would converge when `foo` equals `'bar'`
let convergeBar = new Convergence().when(() => foo === 'bar')
// would converge when `foo` equals `'baz'`
let convergeBaz = new Convergence().when(() => foo === 'baz')
// would converge when `foo` equals `'bar'` and then `'baz'`
let convergeBarBaz = convergeBar.append(convergeBaz)
do callback Convergence
-
callback
Function
The callback to execute -
returns
Convergence
A new convergence instance
Returns a new convergence instance with a callback added to its queue. When a running convergence instance encounters a callback, it will be invoked with the value returned from the last function in the queue. The resulting return value will also be provided to the following function in the queue. If the return value is undefined, the previous return value will be retained.
new Convergence()
// continues after finding a random even number
.when(() => {
let n = Math.ceil(Math.random() * 100)
return !(n % 2) && n
})
// multiplies the even number by another random number
.do((even) => {
return even * Math.ceil(Math.random() * 100)
})
// does not return, the previous value will be retained
.do((rand) => {
console.log('even number times random number = ', rand);
})
// asserts that any number times an even number is even
.always((rand) => {
let rand = Math.ceil(Math.random() * 100);
return !(rand % 2)
}, 100)
When a promise is returned from a callback, the convergence will wait for the promise to resolve before continuing.
new Convergence()
.when(() => foo === 'bar')
.do(() => doSomethingAsync())
.do((baz) => console.log('resolved with', baz))
Returning other convergences from a callback is also supported. The returned convergence will be run with the current remaining timeout. This is useful when computing convergences after converging on another state.
new Convergence()
// continue when `num` is less than 100
.when(() => num < 100)
.do(() => {
// if odd, wait until it is even
if (num % 2) {
return new Convergence()
.when(() => !(num % 2) && num)
} else {
return num;
}
})
run Promise
-
returns
Promise
Runs the current convergence instance, returning a promise that will resolve after all assertions have converged, or reject when any of them fail.
let convergence = new Convergence().when(() => foo === 'bar');
// will converge within the timeout or fail afterwards
convegence.timeout(100).run()
.then(() => console.log('foo is bar!'))
.catch(() => console.log('foo is not bar'))
When an assertion fails and the convergence rejects, it is rejected with the last error thrown from the assertion.
let convergence = new Convergence().when(() => {
expect(foo).to.equal('bar')
})
// will fail after 100ms if `foo` does not equal `'bar'`
convegence.timeout(100).run()
.catch((e) => console.error(e)) // expected '' to equal 'bar'
When the convergence is successful and the promise resolves, it will resolve with a stats object containing useful information about how the convergence and it’s assertions ran.
let convergence = new Convergence()
.when(() => foo === 'bar')
.always(() => foo === 'bar')
convergence.run().then((stats) => {
stats.start // timestamp of the convergence start time
stats.end // timestamp of the convergence end time
stats.elapsed // amount of milliseconds the convergence took
stats.timeout // the timeout this convergence used
stats.runs // total times this convergence ran an assertion
stats.value // last returned value from the queue
stats.queue // array of other stats for each assertion
})
then Promise
-
returns
Promise
By being thennable we can enable the usage of async/await syntax
with convergences. This allows us to naturally chain convergences
without calling #run()
.
async function click(selector) {
// will resolve when the element exists and gets clicked
await new Convergence()
.when(() => {
let $node = document.querySelector('.element')
return !!$node && $node
})
.do(($node) => {
$node.click()
})
}
The convergence thennable method immediately invokes #run()
and
resolves with the last returned value from the convergence’s
queue. This allows us to await for values from a convergence.
let find = (selector) => new Convergence().when(() => {
let $node = document.querySelector('.element')
return !!$node && $node
})
async function fill(selector, value) {
let $node = await find(selector)
$node.value = value
}
timeout timeout Number|Convergence
-
timeout
Number
Timeout for the next convergence -
returns
Number|Convergence
The current instance timeout or a new convergence instance
Returns a new convergence instance with the given timeout, inheriting the current instance’s assertions. If no timeout is given, returns the current timeout for this instance.
let quick = new Convergence(100)
let long = quick.timeout(5000)
quick.timeout() // => 100
long.timeout() // => 5000
when assertion Convergence
-
assertion
Function
The assertion to converge on -
returns
Convergence
A new convergence instance
Returns a new convergence instance with an additional assertion. This assertion is run repeatedly until it passes within the timeout. If the assertion does not pass within the timeout, the convergence will fail.
// would converge when `foo` equals `'bar'`
let convergeFoo = new Convergence().when(() => foo === 'bar')
// would converge when `foo` equals `'bar'` and then `'baz'`
let convergeFooBar = convergeFoo.when(() => foo === 'baz')
always assertion, timeout function
-
assertion
Function
Assertion to converge with -
timeout
Number
Timeout in milliseconds -
returns
function
thennable function that resolves when the assertion converges
Converges on an assertion by resolving when the given assertion passes throughout the timeout period. The assertion will run once every 10ms and is considered to be passing when it does not error or return false. If the assertion does not pass consistently throughout the entire timeout period, it will reject the very first time it encounters a failure.
// simple boolean test
await always(() => total !== 100)
// with chai assertions
await always(() => {
expect(total).to.not.equal(100)
expect(add(total, 1)).to.equal(101)
})
The timeout
argument controls how long it will take for the
assertion to converge. By default, this is 200ms
.
// will pass if `num` is less than `100` for 2 seconds
await always(() => num < 100, 2000)
Returns a thennable function that can be used as a callback where libraries support async functions.
// mocha's `it` supports async tests
it('stays foo for one second', always(() => {
expect(foo).to.equal('foo');
}, 1000));
when assertion, timeout function
-
assertion
Function
Assertion to converge on -
timeout
Number
Timeout in milliseconds -
returns
function
thennable function that resolves when the assertion converges
Converges on an assertion by resolving when the given assertion passes within the timeout period. The assertion will run once every 10ms and is considered to be passing when it does not error or return false. If the assertion never passes within the timeout period, then the promise will reject as soon as it can with the last error it recieved.
// simple boolean test
await when(() => total === 100)
// with chai assertions
await when(() => {
expect(total).to.equal(100)
expect(add(total, 1)).to.equal(101)
})
The timeout
argument controls how long the assertion is given to
converge within. By default, this is 2000ms
.
// will fail if `num` is not `1` within 100ms
await when(() => num === 1, 100)
Returns a thennable function that can be used as a callback where libraries support async functions.
// mocha's `it` supports async tests
it('becomes bar within one second', when(() => {
expect(foo).to.equal('bar');
}, 1000));