🔜☎️ Get a Promise that resolves when a function has been called.
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
src
.babelrc
.gitignore
.travis.yml
LICENSE.md
README.md
package-lock.json
package.json

README.md

anticipated-call

🔜 ☎️ Get a Promise that resolves when your function is called.

Build Status GitHub last commit semantic-release license npm

Example usage

anticipated-call is intended for use in tests, though of course you may use it anywhere it's useful!

const assert = require('assert');
const anticipatedCall = require('..');

class ExampleClass {
    runDelayedUpdate(value) {
        setTimeout(
            () => this.performUpdate(value),
            1000
        );
    }

    performUpdate(value) {
        this.value = value;
    }
}

describe('Example test suite', function () {
    it('should perform the update', async function () {
        const ex = new ExampleClass();
        ex.performUpdate = anticipatedCall(ex.performUpdate);

        ex.runDelayedUpdate(37);
        await ex.performUpdate.nextCall;
        assert(ex.value === 37, 'ex.value should equal 37');
    });
});

If you find a novel use case, create an issue on Github and it might get added to the README.

Requirements

anticipated-call intercepts function calls with Proxy, and returns a Promise. To do this, the Proxy and Promise constructors must be available as globals.

If you're running Node 8, these are included in core, so you don't have to do anything. Similarly, if you're using babel-polyfill or similar, this is handled for you.

API

import anticipatedCall from 'anticipated-call';

anticipatedCall(fn)

Wrap the given function to provide the anticipated-call methods.

function foo(a, b) {
    return a + b;
}

const wrappedFoo = anticipatedCall(foo);

wrappedFoo.nextCall.then(() => console.log('foo was called!'));

If you wish, you may call it with no arguments if you simply need a hook for resolving a promise.

const hook = anticipatedCall();

hook.nextCall.then(() => console.log('hook was called!'));

hook();

nextCall

Wait for the next call of the given function.

function foo(a, b) {
    return a + b;
}

const wrappedFoo = anticipatedCall(foo);

wrappedFoo.nextCall.then(() => console.log('foo was called!'));

nthNextCall(n)

Like nextCall, but wait for the function to be called n times.

let counter = 0;

const increment = anticipatedCall(() => {
    counter = counter + 1;
});

increment.nthNextCall(3).then(() => console.log(`counter value is ${counter}`));

increment();
increment();
increment();

// Prints `counter value is 3` since it waits for the 3rd call before resolving the Promise.

nextCallDuring(fn)

Wait for the function to be called from a callback.

let counter = 0;

const increment = anticipatedCall(() => {
    counter = counter + 1;
});

increment.nextCallDuring(() => {
    counter = 5;
    increment();
}).then(() => console.log(`counter value is ${counter}`));
// Prints `counter value is 6`

nthCallDuring(n, fn)

Like nextCallDuring(), but wait for the function to be called n times.

let counter = 0;

const increment = anticipatedCall(() => {
    counter = counter + 1;
});

increment.nthCallDuring(3, () => {
    counter = 5;
    increment();
    increment();
    increment();
}).then(() => console.log(`counter value is ${counter}`));
// Prints `counter value is 8`

Usage note

In some cases, it's important to keep in mind that the returned Promise will resolve at the end of the call frame. Resolving a Promise is an asynchronous operation. Since JavaScript does one thing at a time (for the most part), when the Promise is resolved, it waits for the currently running function to stop executing (for a regular function, this happens on return; for an async function, this happens on await). If you're tracking state -- for instance, using a variable in a closure -- you might get unexpected results.

Here's an example:

let counter = 0;

const increment = anticipatedCall(() => {
    counter = counter + 1;
});

increment.nextCallDuring(() => {
    increment();
    increment();
    increment();
}).then(() => console.log(`counter value is ${counter}`));
// Prints `counter value is 3`... but why?

anticipated-call was told to wait for the next invocation, but it didn't return until increment() had been called three times! This is because the callback inside nextCallDuring needed to complete execution before the Promise could resolve.

If the callback were an asynchronous function that yielded execution after the call, it would behave as might be expected:

let counter = 0;

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const increment = anticipatedCall(() => {
    counter = counter + 1;
});

increment.nextCallDuring(async () => {
    increment();
    await delay(0);
    increment();
    await delay(0);
    increment();
}).then(() => console.log(`counter value is ${counter}`));
// Prints `counter value is 1`

The purpose of introducing delay(0) is to interrupt the call frame to allow the anticipated-call to have a chance to respond.

If you're interested in learning more, I suggest reading about the JavaScript event loop (this article is a great start).

Contributing

This project uses ESLint-style commit messages.