@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));