├── .nvmrc ├── .eslintignore ├── src ├── toBeInRangeMatcher.d.ts ├── waitForExpect.spec.d.ts ├── __snapshots__ │ └── waitForExpect.spec.ts.snap ├── index.d.ts ├── toBeInRangeMatcher.ts ├── helpers.ts ├── index.ts ├── withFakeTimers.spec.ts └── waitForExpect.spec.ts ├── .npmignore ├── .babelrc ├── tsconfig.json ├── .eslintrc ├── .circleci └── config.yml ├── LICENSE ├── .gitignore ├── manual-releases.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.22.12 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*.d.ts -------------------------------------------------------------------------------- /src/toBeInRangeMatcher.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .babelrc 3 | -------------------------------------------------------------------------------- /src/waitForExpect.spec.d.ts: -------------------------------------------------------------------------------- 1 | declare const waitForExpect: any; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/typescript" 5 | ], 6 | "plugins": [ 7 | "@babel/proposal-class-properties", 8 | "@babel/proposal-object-rest-spread", 9 | [ 10 | "add-module-exports", 11 | { 12 | "addDefaultProperty": true 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "lib", 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "pretty": true, 11 | "downlevelIteration": true 12 | }, 13 | "include": [ 14 | "src/**/*" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/__snapshots__/waitForExpect.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`it fails properly with jest error message when it times out without expectation passing 1`] = ` 4 | "expect(received).toEqual(expected) // deep equality 5 | 6 | Expected: 2000 7 | Received: 200" 8 | `; 9 | 10 | exports[`it fails when the change didn't happen fast enough, based on the waitForExpect timeout 1`] = ` 11 | "expect(received).toEqual(expected) // deep equality 12 | 13 | Expected: 3000 14 | Received: 300" 15 | `; 16 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Waits for the expectation to pass and returns a Promise 3 | * 4 | * @param expectation Function Expectation that has to complete without throwing 5 | * @param timeout Number Maximum wait interval, 4500ms by default 6 | * @param interval Number Wait-between-retries interval, 50ms by default 7 | * @return Promise Promise to return a callback result 8 | */ 9 | export default function waitForExpect( 10 | expectation: () => void | Promise, 11 | timeout?: number, 12 | interval?: number 13 | ): any; 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "window": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 7 | "plugins": ["import", "@typescript-eslint", "prettier"], 8 | "rules": { 9 | "prettier/prettier": "error", 10 | "strict": 0, 11 | "import/extensions": [2, { "js": "never", "ts": "never" }], 12 | "no-unused-vars": "off", 13 | "@typescript-eslint/no-unused-vars": "error" 14 | }, 15 | "settings": { 16 | "import/resolver": { 17 | "node": { 18 | "extensions": [".js", ".ts"] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/toBeInRangeMatcher.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // XXX Not sure how to make eslint not to complain here, but looks like this 3 | // declaration works, so leaving it for now 4 | declare namespace jest { 5 | interface Matchers { 6 | toBeInRange(range: { min: number; max: number }): R; 7 | } 8 | } 9 | 10 | function toBeInRange( 11 | received: number, 12 | { min, max }: { min: number; max: number } 13 | ) { 14 | const pass = received >= min && received <= max; 15 | if (pass) { 16 | return { 17 | message: () => `expected ${received} < ${min} or ${max} < ${received}`, 18 | pass: true 19 | }; 20 | } 21 | return { 22 | message: () => `expected ${min} >= ${received} >= ${max}`, 23 | pass: false 24 | }; 25 | } 26 | 27 | expect.extend({ toBeInRange }); 28 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.0.2 5 | 6 | jobs: 7 | build: 8 | executor: node/default 9 | steps: 10 | - checkout 11 | 12 | - run: 13 | name: Configure npm global install dir 14 | command: | 15 | mkdir -p ~/.npm-global 16 | npm config set prefix ~/.npm-global 17 | echo 'export PATH=$HOME/.npm-global/bin:$PATH' >> $BASH_ENV 18 | 19 | - run: 20 | name: Install pnpm 21 | command: npm install -g pnpm 22 | 23 | - restore_cache: 24 | keys: 25 | - pnpm-store-{{ checksum "pnpm-lock.yaml" }} 26 | - run: pnpm install --frozen-lockfile 27 | - save_cache: 28 | key: pnpm-store-{{ checksum "pnpm-lock.yaml" }} 29 | paths: 30 | - ~/.local/share/pnpm-store 31 | 32 | - run: pnpm run build 33 | - run: pnpm test 34 | - run: pnpm run semantic-release || true 35 | 36 | workflows: 37 | pipeline: 38 | jobs: 39 | - build 40 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | /* eslint-env jest */ 3 | // Used to avoid using Jest's fake timers and Date.now mocks 4 | // See https://github.com/TheBrainFamily/wait-for-expect/issues/4 and 5 | // https://github.com/TheBrainFamily/wait-for-expect/issues/12 for more info 6 | const globalObj = typeof window === "undefined" ? global : window; 7 | 8 | // Currently this fn only supports jest timers, but it could support other test runners in the future. 9 | function runWithRealTimers(callback: () => any) { 10 | const usingJestFakeTimers = 11 | typeof jest !== "undefined" && 12 | // @ts-ignore 13 | setTimeout.clock != null && 14 | // @ts-ignore 15 | typeof setTimeout.clock.Date === "function"; 16 | 17 | if (usingJestFakeTimers) { 18 | jest.useRealTimers(); 19 | } 20 | 21 | const callbackReturnValue = callback(); 22 | 23 | if (usingJestFakeTimers) { 24 | jest.useFakeTimers(); 25 | } 26 | 27 | return callbackReturnValue; 28 | } 29 | 30 | export function getSetTimeoutFn() { 31 | return runWithRealTimers(() => globalObj.setTimeout); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 The Brain Software House 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idea 61 | 62 | lib/ -------------------------------------------------------------------------------- /manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when 4 | there are useful changes in the code that justify a release. But sometimes 5 | things get messed up one way or another and we need to trigger the release 6 | ourselves. When this happens, simply bump the number below and commit that with 7 | the following commit message based on your needs: 8 | 9 | **Major** 10 | 11 | ``` 12 | fix(release): manually release a major version 13 | 14 | There was an issue with a major release, so this manual-releases.md 15 | change is to release a new major version. 16 | 17 | Reference: # 18 | 19 | BREAKING CHANGE: 20 | ``` 21 | 22 | **Minor** 23 | 24 | ``` 25 | feat(release): manually release a minor version 26 | 27 | There was an issue with a minor release, so this manual-releases.md 28 | change is to release a new minor version. 29 | 30 | Reference: # 31 | ``` 32 | 33 | **Patch** 34 | 35 | ``` 36 | fix(release): manually release a patch version 37 | 38 | There was an issue with a patch release, so this manual-releases.md 39 | change is to release a new patch version. 40 | 41 | Reference: # 42 | ``` 43 | 44 | The number of times we've had to do a manual release is: 1 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getSetTimeoutFn } from "./helpers"; 2 | 3 | const defaults = { 4 | timeout: 4500, 5 | interval: 50 6 | }; 7 | 8 | /** 9 | * Waits for the expectation to pass and returns a Promise 10 | * 11 | * @param expectation Function Expectation that has to complete without throwing 12 | * @param timeout Number Maximum wait interval, 4500ms by default 13 | * @param interval Number Wait-between-retries interval, 50ms by default 14 | * @return Promise Promise to return a callback result 15 | */ 16 | const waitForExpect = function waitForExpect( 17 | expectation: () => void | Promise, 18 | timeout = defaults.timeout, 19 | interval = defaults.interval 20 | ) { 21 | const setTimeout = getSetTimeoutFn(); 22 | 23 | // eslint-disable-next-line no-param-reassign 24 | if (interval < 1) interval = 1; 25 | const maxTries = Math.ceil(timeout / interval); 26 | let tries = 0; 27 | return new Promise((resolve, reject) => { 28 | const rejectOrRerun = (error: Error) => { 29 | if (tries > maxTries) { 30 | reject(error); 31 | return; 32 | } 33 | // eslint-disable-next-line no-use-before-define 34 | setTimeout(runExpectation, interval); 35 | }; 36 | function runExpectation() { 37 | tries += 1; 38 | try { 39 | Promise.resolve(expectation()) 40 | .then(() => resolve()) 41 | .catch(rejectOrRerun); 42 | } catch (error) { 43 | rejectOrRerun(error as Error); 44 | } 45 | } 46 | setTimeout(runExpectation, 0); 47 | }); 48 | }; 49 | 50 | waitForExpect.defaults = defaults; 51 | 52 | export default waitForExpect; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wait-for-expect", 3 | "version": "0.0.0-development", 4 | "description": "Wait for expectation to be true, useful for integration and end to end testing", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "eslint --report-unused-disable-directives src/**/*.ts && jest", 9 | "build": "tsc --emitDeclarationOnly && babel src --out-dir lib --extensions \".ts,.tsx\"", 10 | "build:watch": "npm run build -- --watch", 11 | "type-check": "tsc --noEmit", 12 | "semantic-release": "semantic-release", 13 | "prettier": "prettier --write src/**/*.ts" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/TheBrainFamily/wait-for-expect.git" 18 | }, 19 | "keywords": [ 20 | "jest", 21 | "expect", 22 | "wait", 23 | "async", 24 | "await", 25 | "promise", 26 | "integration", 27 | "testing", 28 | "unit" 29 | ], 30 | "author": "Lukasz Gandecki", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/TheBrainFamily/wait-for-expect/issues" 34 | }, 35 | "homepage": "https://github.com/TheBrainFamily/wait-for-expect#readme", 36 | "devDependencies": { 37 | "@babel/cli": "^7.21.5", 38 | "@babel/core": "^7.0.0", 39 | "@babel/plugin-proposal-class-properties": "^7.0.0", 40 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 41 | "@babel/preset-env": "^7.0.0", 42 | "@babel/preset-typescript": "^7.0.0", 43 | "@types/eslint": "^4.16.0", 44 | "@types/eslint-plugin-prettier": "^2.2.0", 45 | "@types/jest": "30.0.0", 46 | "@types/node": "22.13.10", 47 | "@types/prettier": "^1.10.0", 48 | "@typescript-eslint/eslint-plugin": "^2.5.0", 49 | "@typescript-eslint/parser": "^2.5.0", 50 | "babel-core": "^7.0.0-0", 51 | "babel-jest": "30.0.5", 52 | "babel-plugin-add-module-exports": "^1.0.4", 53 | "eslint": "^6.5.1", 54 | "eslint-config-airbnb-base": "^14.0.0", 55 | "eslint-config-prettier": "^2.9.0", 56 | "eslint-plugin-import": "^2.18.2", 57 | "eslint-plugin-prettier": "^2.6.0", 58 | "jest": "30.0.5", 59 | "jest-serializer-ansi": "^1.0.3", 60 | "prettier": "^1.11.1", 61 | "semantic-release": "^15.12.0", 62 | "typescript": "5.7.3" 63 | }, 64 | "jest": { 65 | "snapshotSerializers": [ 66 | "jest-serializer-ansi" 67 | ], 68 | "moduleDirectories": [ 69 | "./node_modules", 70 | "./src" 71 | ], 72 | "moduleFileExtensions": [ 73 | "ts", 74 | "tsx", 75 | "js", 76 | "jsx" 77 | ], 78 | "testRegex": "/src/.*\\.spec\\.(js|ts|tsx)$", 79 | "testEnvironment": "node" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/withFakeTimers.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import "./toBeInRangeMatcher"; 3 | import waitForExpect from "./index"; 4 | 5 | // this is a copy of "it waits for expectation to pass" modified to use jestFakeTimers and two ways of Date.now mocking 6 | // This breakes when we remove the const { setTimeout, Date: { now } } = typeof window !== "undefined" ? window : global; 7 | // line from the index.ts 8 | 9 | beforeEach(() => { 10 | jest.resetModules(); 11 | jest.restoreAllMocks(); 12 | jest.useRealTimers(); 13 | }); 14 | 15 | test("it always uses real timers even if they were set to fake before importing the module", async () => { 16 | jest.useFakeTimers(); 17 | /* eslint-disable global-require */ 18 | const importedWaitForExpect = require("./index"); 19 | jest.useRealTimers(); 20 | 21 | let numberToChange = 10; 22 | // we are using random timeout here to simulate a real-time example 23 | // of an async operation calling a callback at a non-deterministic time 24 | const randomTimeout = Math.floor(Math.random() * 300); 25 | 26 | setTimeout(() => { 27 | numberToChange = 100; 28 | }, randomTimeout); 29 | 30 | jest.useFakeTimers(); 31 | 32 | await importedWaitForExpect(() => { 33 | expect(numberToChange).toEqual(100); 34 | }); 35 | }); 36 | 37 | // Date.now might be mocked with two main ways: 38 | // via mocking whole Date, or by mocking just Date.now 39 | // hence two test cases covered both ways 40 | test("it works even if the Date was mocked", async () => { 41 | /* eslint-disable no-global-assign */ 42 | // @ts-ignore: Cannot reassign to const Date 43 | Date = jest.fn(() => ({ 44 | now() { 45 | return 1482363367071; 46 | } 47 | })); 48 | /* eslint-enable */ 49 | let numberToChange = 10; 50 | 51 | setTimeout(() => { 52 | numberToChange = 100; 53 | }, 100); 54 | let expectFailingMessage; 55 | try { 56 | await waitForExpect(() => { 57 | expect(numberToChange).toEqual(101); 58 | }, 1000); 59 | } catch (e) { 60 | expectFailingMessage = (e as Error).message; 61 | } 62 | expect(expectFailingMessage).toMatch(/toEqual/); 63 | expect(expectFailingMessage).toMatch("101"); 64 | expect(expectFailingMessage).toMatch("Received:"); 65 | expect(expectFailingMessage).toMatch("100"); 66 | }); 67 | 68 | test("it works even if the Date.now was mocked", async () => { 69 | Date.now = jest.fn(() => 1482363367071); 70 | let numberToChange = 10; 71 | 72 | setTimeout(() => { 73 | numberToChange = 100; 74 | }, 100); 75 | let expectFailingMessage; 76 | try { 77 | await waitForExpect(() => { 78 | expect(numberToChange).toEqual(101); 79 | }, 1000); 80 | } catch (e) { 81 | expectFailingMessage = (e as Error).message; 82 | } 83 | expect(expectFailingMessage).toMatch(/toEqual/); 84 | expect(expectFailingMessage).toMatch("101"); 85 | expect(expectFailingMessage).toMatch("Received:"); 86 | expect(expectFailingMessage).toMatch("100"); 87 | }); 88 | -------------------------------------------------------------------------------- /src/waitForExpect.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import "./toBeInRangeMatcher"; 3 | import waitForExpect from "./index"; 4 | 5 | const originalDefaults = { ...waitForExpect.defaults }; 6 | beforeEach(() => { 7 | Object.assign(waitForExpect.defaults, originalDefaults); 8 | }); 9 | 10 | test("it waits for expectation to pass", async () => { 11 | let numberToChange = 10; 12 | // we are using random timeout here to simulate a real-time example 13 | // of an async operation calling a callback at a non-deterministic time 14 | const randomTimeout = Math.floor(Math.random() * 300); 15 | 16 | setTimeout(() => { 17 | numberToChange = 100; 18 | }, randomTimeout); 19 | 20 | await waitForExpect(() => { 21 | expect(numberToChange).toEqual(100); 22 | }); 23 | }); 24 | 25 | test("it fails properly with jest error message when it times out without expectation passing", async () => { 26 | const numberNotToChange = 200; 27 | try { 28 | await waitForExpect(() => { 29 | expect(numberNotToChange).toEqual(2000); 30 | }, 300); 31 | } catch (e) { 32 | expect((e as Error).message).toMatchSnapshot(); 33 | } 34 | }, 1000); 35 | 36 | test("it fails when the change didn't happen fast enough, based on the waitForExpect timeout", async () => { 37 | let numberToChangeTooLate = 300; 38 | const timeToPassForTheChangeToHappen = 1000; 39 | 40 | setTimeout(() => { 41 | numberToChangeTooLate = 3000; 42 | }, timeToPassForTheChangeToHappen); 43 | 44 | try { 45 | await waitForExpect(() => { 46 | expect(numberToChangeTooLate).toEqual(3000); 47 | }, timeToPassForTheChangeToHappen - 200); 48 | } catch (e) { 49 | expect((e as Error).message).toMatchSnapshot(); 50 | } 51 | }, 1500); 52 | 53 | test("it reruns the expectation every x ms, as provided with the second argument", async () => { 54 | // using this would be preferable but somehow jest shares the expect.assertions between tests! 55 | // expect.assertions(1 + Math.floor(timeout / interval)); 56 | let timesRun = 0; 57 | const timeout = 600; 58 | const interval = 150; 59 | try { 60 | await waitForExpect( 61 | () => { 62 | timesRun += 1; 63 | expect(true).toEqual(false); 64 | }, 65 | timeout, 66 | interval 67 | ); 68 | } catch (e) { 69 | // initial run + reruns 70 | const expectedTimesToRun = 1 + Math.floor(timeout / interval); 71 | expect(timesRun).toEqual(expectedTimesToRun); 72 | expect(timesRun).toBeInRange({ 73 | min: expectedTimesToRun - 1, 74 | max: expectedTimesToRun + 1 75 | }); 76 | } 77 | }); 78 | 79 | test("it reruns the expectation every x ms, as provided by the default timeout and interval", async () => { 80 | const timeout = 600; 81 | const interval = 150; 82 | waitForExpect.defaults.timeout = timeout; 83 | waitForExpect.defaults.interval = interval; 84 | const mockExpectation = jest.fn(); 85 | mockExpectation.mockImplementation(() => expect(true).toEqual(false)); 86 | try { 87 | await waitForExpect(mockExpectation); 88 | throw Error("waitForExpect should have thrown"); 89 | } catch (e) { 90 | // initial run + reruns 91 | const expectedTimesToRun = 1 + Math.floor(timeout / interval); 92 | expect(mockExpectation).toHaveBeenCalledTimes(expectedTimesToRun); 93 | } 94 | }); 95 | 96 | test("it works with promises", async () => { 97 | let numberToChange = 10; 98 | const randomTimeout = Math.floor(Math.random() * 300); 99 | 100 | setTimeout(() => { 101 | numberToChange = 100; 102 | }, randomTimeout); 103 | 104 | const sleep = (ms: number) => 105 | new Promise(resolve => setTimeout(() => resolve(), ms)); 106 | 107 | await waitForExpect(async () => { 108 | await sleep(10); 109 | expect(numberToChange).toEqual(100); 110 | }); 111 | }); 112 | 113 | test("it works with a zero interval", async () => { 114 | let numberToChange = 1; 115 | setTimeout(() => { 116 | numberToChange = 2; 117 | }, 10); 118 | 119 | await waitForExpect( 120 | () => { 121 | expect(numberToChange).toEqual(2); 122 | }, 123 | 100, 124 | 0 125 | ); 126 | }); 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 2 | [![CircleCI](https://circleci.com/gh/TheBrainFamily/wait-for-expect.svg?style=shield)](https://circleci.com/gh/TheBrainFamily/wait-for-expect) 3 | 4 | # wait-for-expect 5 | Wait for expectation to be true, useful for integration and end to end testing 6 | 7 | Think things like calling external APIs, database operations, or even GraphQL subscriptions. 8 | We will add examples for all of them soon, for now please enjoy the simple docs. :-) 9 | 10 | # Usage: 11 | 12 | ```javascript 13 | const waitForExpect = require("wait-for-expect") 14 | 15 | test("it waits for the number to change", async () => { 16 | let numberToChange = 10; 17 | // we are using random timeout here to simulate a real-time example 18 | // of an async operation calling a callback at a non-deterministic time 19 | const randomTimeout = Math.floor(Math.random() * 300); 20 | 21 | setTimeout(() => { 22 | numberToChange = 100; 23 | }, randomTimeout); 24 | 25 | await waitForExpect(() => { 26 | expect(numberToChange).toEqual(100); 27 | }); 28 | }); 29 | ``` 30 | 31 | instead of: 32 | 33 | ```javascript 34 | 35 | test("it waits for the number to change", () => { 36 | let numberToChange = 10; 37 | const randomTimeout = Math.floor(Math.random() * 300); 38 | 39 | setTimeout(() => { 40 | numberToChange = 100; 41 | }, randomTimeout); 42 | 43 | setTimeout(() => { 44 | expect(numberToChange).toEqual(100); 45 | }, 700); 46 | }); 47 | ``` 48 | 49 | It will check whether the expectation passes right away in the next available "tick" (very useful with, for example, integration testing of react when mocking fetches, like here: https://github.com/kentcdodds/react-testing-library#usage). 50 | 51 | If it doesn't, it will keep repeating for the duration of, at most, the specified timeout, every 50 ms. The default timeout is 4.5 seconds to fit below the default 5 seconds that Jest waits for before throwing an error. 52 | 53 | Nice thing about this simple tool is that if the expectation keeps failing till the timeout, it will check it one last time, but this time the same way your test runner would run it - so you basically get your expectation library error, the sam way like if you used setTimeout to wait but didn't wait long enough. 54 | 55 | To show an example - if I change the expectation to wait for 105 in above code, you will get nice and familiar: 56 | 57 | ``` 58 | 59 | FAIL src/waitForExpect.spec.js (5.042s) 60 | ✕ it waits for the number to change (4511ms) 61 | 62 | ● it waits for the number to change 63 | 64 | expect(received).toEqual(expected) 65 | 66 | Expected value to equal: 67 | 105 68 | Received: 69 | 100 70 | 71 | 9 | }, 600); 72 | 10 | await waitForExpect(() => { 73 | > 11 | expect(numberToChange).toEqual(105); 74 | 12 | }); 75 | 13 | }); 76 | 14 | 77 | 78 | at waitForExpect (src/waitForExpect.spec.js:11:28) 79 | at waitUntil.catch (src/index.js:61:5) 80 | 81 | Test Suites: 1 failed, 1 total 82 | Tests: 1 failed, 1 total 83 | Snapshots: 0 total 84 | Time: 5.807s 85 | ``` 86 | 87 | You can add multiple expectations to wait for, all of them have to pass, and if one of them don't, it will be marked. 88 | For example, let's add another expectation for a different number, notice how jest tells you that that's the expectation that failed. 89 | 90 | ``` 91 | expect(received).toEqual(expected) 92 | 93 | Expected value to equal: 94 | 110 95 | Received: 96 | 105 97 | 98 | 11 | await waitForExpect(() => { 99 | 12 | expect(numberToChange).toEqual(100); 100 | > 13 | expect(numberThatWontChange).toEqual(110); 101 | 14 | }); 102 | 15 | }); 103 | 16 | 104 | 105 | at waitForExpect (src/waitForExpect.spec.js:13:34) 106 | at waitUntil.catch (src/index.js:61:5) 107 | ``` 108 | 109 | Since 0.6.0 we can now work with promises, for example, this is now possible: 110 | 111 | ```javascript 112 | test("rename todo by typing", async () => { 113 | // (..) 114 | const todoToChange = getTodoByText("original todo"); 115 | todoToChange.value = "different text now"; 116 | Simulate.change(todoToChange); 117 | 118 | await waitForExpect(() => 119 | expect( 120 | todoItemsCollection.findOne({ 121 | text: "different text now" 122 | })).resolves.not.toBeNull() 123 | ); 124 | }); 125 | ``` 126 | 127 | Async Await also works, as in this example - straight from our test case 128 | 129 | ```javascript 130 | test("it works with promises", async () => { 131 | let numberToChange = 10; 132 | const randomTimeout = Math.floor(Math.random() * 300); 133 | 134 | setTimeout(() => { 135 | numberToChange = 100; 136 | }, randomTimeout); 137 | 138 | const sleep = (ms) => 139 | new Promise(resolve => setTimeout(() => resolve(), ms)); 140 | 141 | await waitForExpect(async () => { 142 | await sleep(10); 143 | expect(numberToChange).toEqual(100); 144 | }); 145 | }); 146 | ``` 147 | 148 | (Note: Obviously, in this case it doesn't make sense to put the await sleep there, this is just for demonstration purpose) 149 | 150 | # API 151 | waitForExpect takes 3 arguments, 2 optional. 152 | 153 | ```javascript 154 | /** 155 | * Waits for predicate to not throw and returns a Promise 156 | * 157 | * @param expectation Function Predicate that has to complete without throwing 158 | * @param timeout Number Maximum wait interval, 4500ms by default 159 | * @param interval Number Wait interval, 50ms by default 160 | * @return Promise Promise to return a callback result 161 | */ 162 | ``` 163 | 164 | The defaults for `timeout` and `interval` can also be edited globally, e.g. in a jest setup file: 165 | ```javascript 166 | import waitForExpect from 'wait-for-expect'; 167 | 168 | waitForExpect.defaults.timeout = 2000; 169 | waitForExpect.defaults.interval = 10; 170 | ``` 171 | 172 | ## Changelog 173 | 1.0.0 - 15 June 2018 174 | 175 | ( For most people this change doesn't matter. ) 176 | Export the function directly in module.exports instead of exporting as an object that has default key. If that's not clear (...it isn't ;-) ) - check #8 #9 . 177 | Thanks to @mbaranovski for the PR and @BenBrostoff for creating the issue! I'm making this 1.0.0 as this is breaking for people that currently did: 178 | ```javascript 179 | const { default: waitFor } = require('wait-for-expect'); 180 | ``` 181 | 182 | 0.6.0 - 3 May 2018 183 | 184 | Work with promises. 185 | 186 | 0.5.0 - 10 April 2018 187 | 188 | Play nicely with jest fake timers (and also in any test tool that overwrites setTimeout) - thanks to @slightlytyler and @kentcoddods for helping to get this resolved. 189 | 190 | ## Credit 191 | Originally based on ideas from https://github.com/devlato/waitUntil. 192 | Simplified highly and rewritten for 0.1.0 version. 193 | Simplified even more and rewritten even more for 0.2.0 with guidance from Kent C. Dodds: https://github.com/kentcdodds/react-testing-library/pull/25 --------------------------------------------------------------------------------