akauppi
akauppi

Reputation: 18046

Expecting a Promise *not* to complete, in Jest

I have the following need to test whether something does not happen.

While testing something like that may be worth a discussion (how long wait is long enough?), I hope there would exist a better way in Jest to integrate with test timeouts. So far, I haven't found one, but let's begin with the test.

test ('User information is not distributed to a project where the user is not a member', async () => {

  // Write in 'userInfo' -> should NOT turn up in project 1.
  //
  await collection("userInfo").doc("xyz").set({ displayName: "blah", photoURL: "https://no-such.png" });

  // (firebase-jest-testing 0.0.3-beta.3)
  await expect( eventually("projects/1/userInfo/xyz", o => !!o, 800 /*ms*/) ).resolves.toBeUndefined();

  // ideally:
  //await expect(prom).not.toComplete;    // ..but with cancelling such a promise

}, 9999 /*ms*/ );

The eventually returns a Promise and I'd like to check that:

Jest provides .resolves and .rejects but nothing that would combine the two.

  1. Can I create the anticipated .not.toComplete using some Jest extension mechanism?
  2. Can I create a "run just before the test would time out" (with ability to make the test pass or fail) trigger?

I think the 2. suggestion might turn handy, and can create a feature request for such, but let's see what comments this gets..


Edit: There's a further complexity in that JS Promises cannot be cancelled from outside (but they can time out, from within).

Upvotes: 5

Views: 2648

Answers (1)

akauppi
akauppi

Reputation: 18046

I eventually solved this with a custom matcher:

/*
* test-fns/matchers/timesOut.js
*
* Usage:
*   <<
*     expect(prom).timesOut(500);
*   <<
*/
import { expect } from '@jest/globals'

expect.extend({
  async timesOut(prom, ms) {   // (Promise of any, number) => { message: () => string, pass: boolean }

    // Wait for either 'prom' to complete, or a timeout.
    //
    const [resolved,error] = await Promise.race([ prom, timeoutMs(ms) ])
      .then(x => [x])
      .catch(err => [undefined,err] );

    const pass = (resolved === TIMED_OUT);

    return pass ? {
      message: () => `expected not to time out in ${ms}ms`,
      pass: true
    } : {
      message: () => `expected to time out in ${ms}ms, but ${ error ? `rejected with ${error}`:`resolved with ${resolved}` }`,
      pass: false
    }
  }
})

const timeoutMs = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); })
  .then( _ => TIMED_OUT);

const TIMED_OUT = Symbol()

source

The good side is, this can be added to any Jest project.

The down side is, one needs to separately mention the delay (and guarantee Jest's time out does not happen before).

Makes the question's code become:

await expect( eventually("projects/1/userInfo/xyz") ).timesOut(300)

Note for Firebase users:

Jest does not exit to OS level if Firestore JS SDK client listeners are still active. You can prevent it by unsubscribing to them in afterAll - but this means keeping track of which listeners are alive and which not. The firebase-jest-testing library does this for you, under the hood. Also, this will eventually ;) get fixed by Firebase.

Upvotes: 6

Related Questions