├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── prettier.config.js ├── renovate.json ├── rollup.config.js ├── src ├── __tests__ │ └── index.test.ts └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # http://editorconfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 80 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | max_line_length = 0 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { "project": "./tsconfig.json" }, 4 | "extends": ["@keplr/eslint-config-backend-node"], 5 | "rules": { 6 | "@typescript-eslint/ban-ts-comment": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [pull_request] 3 | 4 | jobs: 5 | dev-tools: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Test Devs tools, compil and lint 10 | uses: actions/setup-node@v2 11 | - run: npm install 12 | - run: npm build 13 | - run: npm run lint 14 | - run: npm run commit:fake 15 | unit-tests: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | puppeteer-version: [1, 2, 3, 4, 5] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: npm test node ${{matrix.node-version}} puppeteer ${{matrix.puppeteer-version}} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm install @types/puppeteer@${{matrix.puppeteer-version}} 29 | if: ${{ matrix.puppeteer-version < 4 }} 30 | - run: npm install @types/puppeteer@3 31 | if: ${{ matrix.puppeteer-version >= 4 }} 32 | - run: npm install puppeteer@${{matrix.puppeteer-version}} 33 | - run: npm test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .rpt2_cache 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | lib 61 | 62 | .DS_Store 63 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .rpt2_cache 2 | .editorconfig 3 | .eslintrc.js 4 | .gitignore 5 | yarn-error.log 6 | .travis.yml 7 | jest.config.js 8 | prettier.config.js 9 | renovate.json 10 | rollup.config.js 11 | tsconfig.json 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.0.0](https://github.com/jtassin/pending-xhr-puppeteer/compare/v2.3.4...v3.0.0) (2025-04-05) 2 | 3 | ## [2.3.4](https://github.com/jtassin/pending-xhr-puppeteer/compare/v2.3.3...v2.3.4) (2025-04-05) 4 | 5 | ## [2.3.3](https://github.com/jtassin/pending-xhr-puppeteer/compare/v2.3.2...v2.3.3) (2020-12-13) 6 | 7 | ## [2.3.2](https://github.com/jtassin/pending-xhr-puppeteer/compare/v2.3.1...v2.3.2) (2020-06-16) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * expose only interesting files ([a083778](https://github.com/jtassin/pending-xhr-puppeteer/commit/a08377834ea2f4b5fc4107f28ac5489b6603fb27)) 13 | 14 | ## [2.3.1](https://github.com/jtassin/pending-xhr-puppeteer/compare/v2.3.0...v2.3.1) (2020-06-16) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * fix exposition of waitOnceForAllXhrFinished ([b26cbf1](https://github.com/jtassin/pending-xhr-puppeteer/commit/b26cbf19f84ac637c8888c158f852017257689c1)) 20 | 21 | # [2.3.0](https://github.com/jtassin/pending-xhr-puppeteer/compare/v2.2.0...v2.3.0) (2020-06-16) 22 | 23 | 24 | ### Features 25 | 26 | * expose removeListeners function ([8877578](https://github.com/jtassin/pending-xhr-puppeteer/commit/8877578baa30311c9e5ce64a206224bb5b3b5ae5)) 27 | 28 | ### Changelog 29 | 30 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 31 | 32 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 33 | 34 | ### [v2.0.0](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.17...v2.0.0) 35 | 36 | > 30 March 2019 37 | 38 | - Typescript [`#21`](https://github.com/jtassin/pending-xhr-puppeteer/pull/21) 39 | - doc: add example for an event triggered xhr [`#11`](https://github.com/jtassin/pending-xhr-puppeteer/pull/11) 40 | - Update dependency eslint-plugin-jest to v22.4.1 [`#14`](https://github.com/jtassin/pending-xhr-puppeteer/pull/14) 41 | - Update dependency puppeteer to v1.13.0 [`#17`](https://github.com/jtassin/pending-xhr-puppeteer/pull/17) 42 | - Update dependency jest to v24 [`#20`](https://github.com/jtassin/pending-xhr-puppeteer/pull/20) 43 | - Update dependency eslint-config-prettier to v3.6.0 [`#13`](https://github.com/jtassin/pending-xhr-puppeteer/pull/13) 44 | - Update dependency eslint to v5.15.3 [`#9`](https://github.com/jtassin/pending-xhr-puppeteer/pull/9) 45 | - Pin dependencies [`#8`](https://github.com/jtassin/pending-xhr-puppeteer/pull/8) 46 | - Add renovate.json [`#7`](https://github.com/jtassin/pending-xhr-puppeteer/pull/7) 47 | - chore: releases with release-it [`90a3ea3`](https://github.com/jtassin/pending-xhr-puppeteer/commit/90a3ea38425987829e953e704ff48727c4046995) 48 | - chore(typescript): switching codebase to ts [`8e66474`](https://github.com/jtassin/pending-xhr-puppeteer/commit/8e66474cec5cb01c7f975cae0f8e4a45c159b68a) 49 | - chore: add generated changelog [`a6b8638`](https://github.com/jtassin/pending-xhr-puppeteer/commit/a6b8638b4292368349bc55f7aaab2b68d28ed87e) 50 | 51 | #### [v1.0.17](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.16...v1.0.17) 52 | 53 | > 5 January 2019 54 | 55 | - add example with Promise.race [`#4`](https://github.com/jtassin/pending-xhr-puppeteer/pull/4) 56 | - release 1.0.17 [`0585d7b`](https://github.com/jtassin/pending-xhr-puppeteer/commit/0585d7b2ceefb5748b69c75bfce04d70c70e8c50) 57 | 58 | #### [v1.0.16](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.15...v1.0.16) 59 | 60 | > 5 January 2019 61 | 62 | - fixes #1 - handling multiple xhrs [`#3`](https://github.com/jtassin/pending-xhr-puppeteer/pull/3) 63 | - release 1.0.16 [`437a7a2`](https://github.com/jtassin/pending-xhr-puppeteer/commit/437a7a22377e02e8355de902060eca001a844226) 64 | 65 | #### [v1.0.15](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.12...v1.0.15) 66 | 67 | > 2 January 2019 68 | 69 | - Dependencies update [`#2`](https://github.com/jtassin/pending-xhr-puppeteer/pull/2) 70 | - update package [`ea052c1`](https://github.com/jtassin/pending-xhr-puppeteer/commit/ea052c173c5633ae46975197ed7721d999c4e150) 71 | - release 1.0.15 [`7571e91`](https://github.com/jtassin/pending-xhr-puppeteer/commit/7571e9110003571df47fcfad07730f5f6a3f6472) 72 | 73 | #### [v1.0.12](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.11...v1.0.12) 74 | 75 | > 27 May 2018 76 | 77 | #### [v1.0.11](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.10...v1.0.11) 78 | 79 | > 16 May 2018 80 | 81 | #### [v1.0.10](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.9...v1.0.10) 82 | 83 | > 16 May 2018 84 | 85 | - back to pending-puppeteer-xhr [`4a808dc`](https://github.com/jtassin/pending-xhr-puppeteer/commit/4a808dca677b233413cefaa71aa7d68d9a41179f) 86 | 87 | #### [v1.0.9](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.8...v1.0.9) 88 | 89 | > 16 May 2018 90 | 91 | - pending-xhr-puppeteer is working on npm :tada: [`84d5f5f`](https://github.com/jtassin/pending-xhr-puppeteer/commit/84d5f5fef0056cb8cdc9ce0003e8d53b01ff3f1d) 92 | - adding a new package name because of npm (pending-xhr-puppeteer will be still up to date) [`18c371b`](https://github.com/jtassin/pending-xhr-puppeteer/commit/18c371b444d027d33152bf0bfc36589847a93fa1) 93 | 94 | #### [v1.0.8](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.7...v1.0.8) 95 | 96 | > 14 May 2018 97 | 98 | #### [v1.0.7](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.6...v1.0.7) 99 | 100 | > 14 May 2018 101 | 102 | - add publish:patch cmd [`22a51bf`](https://github.com/jtassin/pending-xhr-puppeteer/commit/22a51bfa65f47ec248acc4f048bdf0f2b18f9930) 103 | - merge [`8da4597`](https://github.com/jtassin/pending-xhr-puppeteer/commit/8da45979b03fb984105b6e84377ef2f7ca888576) 104 | - change name because of npmjs [`1209bca`](https://github.com/jtassin/pending-xhr-puppeteer/commit/1209bcae879a2df52e09a5cd60c3420c34cc93ef) 105 | 106 | #### [v1.0.6](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.5...v1.0.6) 107 | 108 | > 11 May 2018 109 | 110 | - still not visible on npmjs :/ [`f9a78c3`](https://github.com/jtassin/pending-xhr-puppeteer/commit/f9a78c3448be6581b921355ed1c4cbed49e1f044) 111 | 112 | #### [v1.0.5](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.4...v1.0.5) 113 | 114 | > 11 May 2018 115 | 116 | - packahe not visible on npmjs [`b950cb3`](https://github.com/jtassin/pending-xhr-puppeteer/commit/b950cb3f8b5bf74d5bd9e6331e6f78eb8485d274) 117 | 118 | #### [v1.0.4](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.3...v1.0.4) 119 | 120 | > 11 May 2018 121 | 122 | #### [v1.0.3](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.2...v1.0.3) 123 | 124 | > 11 May 2018 125 | 126 | - Update README.md [`8874670`](https://github.com/jtassin/pending-xhr-puppeteer/commit/8874670be9d2341ff2010d01593a1eae770677ef) 127 | - Update README.md [`ed9feb4`](https://github.com/jtassin/pending-xhr-puppeteer/commit/ed9feb4aa65b3b7494dceee9ee14cdc05be8ea6a) 128 | - add keywords [`078eceb`](https://github.com/jtassin/pending-xhr-puppeteer/commit/078eceb9dc99aaeb2ed8ea1576b2caec8ce88a2d) 129 | 130 | #### [v1.0.2](https://github.com/jtassin/pending-xhr-puppeteer/compare/v1.0.1...v1.0.2) 131 | 132 | > 10 May 2018 133 | 134 | - add npm version [`d7f8252`](https://github.com/jtassin/pending-xhr-puppeteer/commit/d7f82527c2e635e7522059d16bc45755a0236d8a) 135 | 136 | #### v1.0.1 137 | 138 | > 10 May 2018 139 | 140 | - starting devs [`5a187ab`](https://github.com/jtassin/pending-xhr-puppeteer/commit/5a187ab994b07cec8eda89b1b8ff12a7be63c1bb) 141 | - add prettier [`7816701`](https://github.com/jtassin/pending-xhr-puppeteer/commit/7816701f0f53fa9995ad17f5edf7cabf3473948c) 142 | - first commit :tada: [`786c632`](https://github.com/jtassin/pending-xhr-puppeteer/commit/786c6325ae502d985ce0e20dc114854aa8539242) 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Julien TASSIN 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pending XHR Puppeteer 2 | 3 | [![npm version](https://badge.fury.io/js/pending-xhr-puppeteer.svg)](https://badge.fury.io/js/pending-xhr-puppeteer) 4 | [![Build Status](https://travis-ci.org/jtassin/pending-xhr-puppeteer.svg?branch=master)](https://travis-ci.org/jtassin/pending-xhr-puppeteer) 5 | 6 |

7 | | Introduction 8 | | Installation 9 | | Usage 10 | | Contribute | 11 |

12 | 13 | ## Introduction 14 | 15 | Pending XHR Puppeteer is a tool that detect when there is xhr requests not yet finished. You can use it to have a xhr requests count or to wait for all xhr requests to be finished. 16 | 17 | ## Installation 18 | 19 | To install with yarn : 20 | 21 | ```bash 22 | yarn add pending-xhr-puppeteer -D 23 | ``` 24 | 25 | To install with npm : 26 | 27 | ```bash 28 | npm install pending-xhr-puppeteer --save-dev 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### wait for all xhr requests to be finished 34 | 35 | ```javascript 36 | const puppeteer = require('puppeteer'); 37 | const { PendingXHR } = require('pending-xhr-puppeteer'); 38 | 39 | const browser = await puppeteer.launch({ 40 | headless: true, 41 | args, 42 | }); 43 | 44 | const page = await browser.newPage(); 45 | const pendingXHR = new PendingXHR(page); 46 | await page.goto(`http://page-with-xhr`); 47 | // Here all xhr requests are not finished 48 | await pendingXHR.waitForAllXhrFinished(); 49 | // Here all xhr requests are finished 50 | ``` 51 | 52 | ### Get the number of pending xhr 53 | 54 | ```javascript 55 | const puppeteer = require('puppeteer'); 56 | const { PendingXHR } = require('pending-xhr-puppeteer'); 57 | 58 | const browser = await puppeteer.launch({ 59 | headless: true, 60 | args, 61 | }); 62 | 63 | const page = await browser.newPage(); 64 | const pendingXHR = new PendingXHR(page); 65 | await page.goto(`http://page-with-xhr`); 66 | console.log(pendingXHR.pendingXhrCount()); 67 | // Display the number of xhr pending 68 | ``` 69 | 70 | ### Usage with Promise.race 71 | 72 | If you need to wait xhrs but not longer than a specific time, You can race **pending-xhr-puppeteer** and `setTimeout` in a `Promise.race`. 73 | 74 | ```javascript 75 | const puppeteer = require('puppeteer'); 76 | const { PendingXHR } = require('pending-xhr-puppeteer'); 77 | 78 | const browser = await puppeteer.launch({ 79 | headless: true, 80 | args, 81 | }); 82 | 83 | const page = await browser.newPage(); 84 | const pendingXHR = new PendingXHR(page); 85 | await page.goto(`http://page-with-xhr`); 86 | // We will wait max 1 seconde for xhrs 87 | await Promise.race([ 88 | pendingXHR.waitForAllXhrFinished(), 89 | new Promise(resolve => { 90 | setTimeout(resolve, 1000); 91 | }), 92 | ]); 93 | console.log(pendingXHR.pendingXhrCount()); 94 | // May or may not have pending xhrs 95 | ``` 96 | 97 | ## Wait for all xhr triggered by all the events of the page 98 | 99 | You can use this lib to wait for xhr triggered by any event from the UI (click, typing, ...). 100 | 101 | Exemple : 102 | 103 | ```javascript 104 | const pendingXHR = new PendingXHR(page); 105 | await page.goto(`http://page-with-xhr`); 106 | await page.click('.my-selector'); // This action will trigger some xhr 107 | // Here all xhr requests triggered by the click are not finished 108 | await pendingXHR.waitForAllXhrFinished(); 109 | // Here all xhr requests triggered by the click are finished 110 | // You can then perform an other xhr producer event 111 | await page.click('.my-selector2'); // This action will trigger some xhr 112 | // You can rewait them 113 | await pendingXHR.waitForAllXhrFinished(); 114 | ``` 115 | 116 | This mode is usefull to test SPA, you d'ont have to recreate a new instance at each time. 117 | The request listeners will be deleted when you leave the page. 118 | 119 | ## Wait for all xhr triggered by an event of the page 120 | 121 | with `waitOnceForAllXhrFinished` you can wait until all the xhr are finished and them remove the listeners. 122 | This is usefull when `waitForAllXhrFinished` has a leaking behaviour for you. 123 | 124 | Exemple : 125 | 126 | ```javascript 127 | const pendingXHR = new PendingXHR(page); 128 | await page.goto(`http://page-with-xhr`); 129 | await page.click('.my-selector'); // This action will trigger some xhr 130 | // Here all xhr requests triggered by the click are not finished 131 | await pendingXHR.waitOnceForAllXhrFinished(); 132 | // Here all xhr requests triggered by the click are finished 133 | // All pendingXHR listeners are remove here too 134 | ``` 135 | 136 | ## Contribute 137 | 138 | ```bash 139 | git clone https://github.com/jtassin/pending-xhr-puppeteer.git 140 | cd pending-xhr-puppeteer 141 | yarn 142 | yarn test 143 | ``` 144 | 145 | Merge requests and issues are welcome. 146 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | setupFilesAfterEnv: ['./jest.setup.ts'], 5 | testPathIgnorePatterns: ['node_modules/', '.history'], 6 | collectCoverageFrom: ['src/*.{ts}', '!**/node_modules/**', '!**/vendor/**'], 7 | coverageReporters: ['json', 'html'], 8 | coverageThreshold: { 9 | global: { 10 | statements: 100, 11 | branches: 76, 12 | lines: 100, 13 | functions: 100, 14 | }, 15 | }, 16 | moduleNameMapper: { 17 | "puppeteer-core/internal/puppeteer-core.js": "/node_modules/puppeteer-core/lib/cjs/puppeteer/puppeteer-core.js", 18 | "puppeteer-core/internal/node/PuppeteerNode.js": "/node_modules/puppeteer-core/lib/cjs/puppeteer/node/PuppeteerNode.js" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | jest.setTimeout(20e3); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pending-xhr-puppeteer", 3 | "node": ">= 8.0.0", 4 | "version": "3.0.0", 5 | "description": "Small tool for wait that all xhr are finished in pupeteer", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "module": "lib/es.js", 9 | "homepage": "https://github.com/jtassin/pending-xhr-puppeteer", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/jtassin/pending-xhr-puppeteer.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/jtassin/pending-xhr-puppeteer/issues" 16 | }, 17 | "files": [ 18 | "lib" 19 | ], 20 | "author": "Julien TASSIN", 21 | "license": "MIT", 22 | "private": false, 23 | "keywords": [ 24 | "puppeteer", 25 | "xhr", 26 | "pending", 27 | "wait", 28 | "nodejs", 29 | "e2e" 30 | ], 31 | "devDependencies": { 32 | "@keplr/eslint-config-backend-node": "2.5.1", 33 | "@release-it/conventional-changelog": "3.3.0", 34 | "@types/express": "4.17.13", 35 | "@types/jest": "27.0.3", 36 | "@types/node": "16.11.11", 37 | "@types/puppeteer": "5.4.4", 38 | "auto-changelog": "2.3.0", 39 | "delay": "5.0.0", 40 | "eslint": "7.32.0", 41 | "eslint-config-prettier": "8.3.0", 42 | "eslint-plugin-jest": "25.3.0", 43 | "husky": "7.0.4", 44 | "jest": "27.4.3", 45 | "lint-staged": "12.1.2", 46 | "prettier": "2.5.0", 47 | "puppeteer": "22.8.2", 48 | "release-it": "14.11.8", 49 | "rollup": "2.60.2", 50 | "rollup-plugin-typescript": "1.0.1", 51 | "rollup-plugin-typescript2": "0.31.1", 52 | "ts-jest": "27.0.7", 53 | "tsc": "2.0.3", 54 | "tslib": "2.3.1", 55 | "typescript": "4.3.5" 56 | }, 57 | "scripts": { 58 | "prettier:write": "prettier --single-quote --trailing-comma es5 --write src/**/*", 59 | "lint": "eslint src/*.ts src/**/*.ts", 60 | "test": "jest", 61 | "test:coverage": "jest --coverage", 62 | "precommit": "lint-staged", 63 | "commit:fake": "git commit --dry-run -m foo --allow-empty --amend", 64 | "release:fake": "GITHUB_TOKEN=XXX release-it patch --dry-run --no-git.requireCleanWorkingDir --ci", 65 | "build": "rollup -c" 66 | }, 67 | "release-it": { 68 | "hooks": { 69 | "after:bump": "npm run build" 70 | }, 71 | "plugins": { 72 | "@release-it/conventional-changelog": { 73 | "preset": "angular", 74 | "infile": "CHANGELOG.md" 75 | } 76 | }, 77 | "git": { 78 | "tagName": "v${version}" 79 | }, 80 | "github": { 81 | "release": true 82 | } 83 | }, 84 | "lint-staged": { 85 | "*.{js,json,css,md}": [ 86 | "prettier --write", 87 | "git add" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | bracketSpacing: true, 7 | arrowParens: 'avoid', 8 | }; 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "packageRules": [ 4 | { 5 | "depTypeList": ["devDependencies"], 6 | "automerge": true 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import pkg from './package.json'; 3 | 4 | module.exports = { 5 | input: 'src/index.ts', 6 | plugins: [typescript({ typescript: require('typescript') })], 7 | output: [ 8 | { 9 | file: pkg.main, 10 | format: 'cjs', 11 | }, 12 | { 13 | file: pkg.module, 14 | format: 'es', 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | import delay from 'delay'; 4 | import { launch, Browser } from 'puppeteer'; 5 | 6 | import { PendingXHR } from '../index'; 7 | 8 | let port: number; 9 | 10 | const OK_NO_XHR = ` 11 | 12 | OK_NO_XHR 13 | 14 | `; 15 | 16 | function getOkWithOneXhr() { 17 | return ` 18 | 19 | OK_WITH_1_SLOW_XHR 20 | 28 | 29 | `; 30 | } 31 | 32 | function getOkWithOneFailingXhr() { 33 | return ` 34 | 35 | OK_WITH_1_SLOW_XHR 36 | 44 | 45 | `; 46 | } 47 | 48 | function getOkWithTwoXhr() { 49 | return ` 50 | 51 | OK_WITH_1_SLOW_XHR 52 | 66 | 67 | `; 68 | } 69 | 70 | let request1Resolver: ((param: [number, string]) => void) | undefined; 71 | const request1Promise = () => 72 | new Promise<[number, unknown]>(resolve => { 73 | request1Resolver = resolve; 74 | }); 75 | 76 | let request2Resolver: ((param: [number, string]) => void) | undefined; 77 | const request2Promise = () => 78 | new Promise<[number, unknown]>(resolve => { 79 | request2Resolver = resolve; 80 | }); 81 | 82 | let server: http.Server; 83 | let browser: Browser; 84 | 85 | describe('PendingXHR', () => { 86 | beforeAll(async () => { 87 | const args = []; 88 | //eslint-disable-next-line no-process-env 89 | if (process.env.CI) { 90 | args.push('--no-sandbox'); 91 | } 92 | browser = await launch({ 93 | headless: true, 94 | args, 95 | }); 96 | }); 97 | 98 | afterAll(async () => { 99 | await browser.close(); 100 | }); 101 | 102 | afterEach(() => { 103 | if (request1Resolver) { 104 | request1Resolver([0, 'afterEach']); 105 | } 106 | }); 107 | 108 | afterEach(() => { 109 | server.close(); 110 | }); 111 | 112 | function startServerReturning(html: string) { 113 | //eslint-disable-next-line @typescript-eslint/no-misused-promises 114 | server = http.createServer(async (request, response) => { 115 | if (request.url === '/go') { 116 | response.statusCode = 200; 117 | response.end(html); 118 | } else if (request.url === '/request1') { 119 | const [statusCode, body] = await request1Promise(); 120 | response.statusCode = statusCode; 121 | response.end(body); 122 | } else if (request.url === '/request2') { 123 | const [statusCode, body] = await request2Promise(); 124 | response.statusCode = statusCode; 125 | response.end(body); 126 | } 127 | }); 128 | server = server.listen(); 129 | // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access 130 | port = (server.address()! as any).port; 131 | } 132 | 133 | describe('pendingXhrCount', () => { 134 | it('returns 0 if no xhr pending count', async () => { 135 | startServerReturning(OK_NO_XHR); 136 | const page = await browser.newPage(); 137 | const pendingXHR = new PendingXHR(page); 138 | await page.goto(`http://localhost:${port}/go`); 139 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 140 | }); 141 | 142 | it('returns the xhr pending count', async () => { 143 | startServerReturning(getOkWithTwoXhr()); 144 | const page = await browser.newPage(); 145 | const pendingXHR = new PendingXHR(page); 146 | await page.goto(`http://localhost:${port}/go`); 147 | expect(pendingXHR.pendingXhrCount()).toEqual(2); 148 | setTimeout(() => { 149 | request1Resolver!([200, 'first xhr finished']); 150 | }, 0); 151 | await delay(1000); 152 | expect(pendingXHR.pendingXhrCount()).toEqual(1); 153 | setTimeout(() => { 154 | request2Resolver!([200, 'second xhr finished']); 155 | }, 0); 156 | await delay(1000); 157 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 158 | }); 159 | }); 160 | 161 | describe('removePageListeners', () => { 162 | it('removes all listenners', async () => { 163 | startServerReturning(OK_NO_XHR); 164 | const page = await browser.newPage(); 165 | const count = page.listenerCount('request'); 166 | const pendingXHR = new PendingXHR(page); 167 | await page.goto(`http://localhost:${port}/go`); 168 | await pendingXHR.waitForAllXhrFinished(); 169 | expect(page.listenerCount('request')).toBe(count + 1); 170 | pendingXHR.removePageListeners(); 171 | expect(page.listenerCount('request')).toBe(count); 172 | }); 173 | }); 174 | 175 | describe('waitOnceForAllXhrFinished', () => { 176 | it('returns and removes all listeners immediatly if no xhr pending', async () => { 177 | startServerReturning(OK_NO_XHR); 178 | const page = await browser.newPage(); 179 | const count = page.listenerCount('request'); 180 | const pendingXHR = new PendingXHR(page); 181 | await page.goto(`http://localhost:${port}/go`); 182 | await pendingXHR.waitOnceForAllXhrFinished(); 183 | expect(page.listenerCount('request')).toBe(count); 184 | }); 185 | 186 | describe('one XHR', () => { 187 | it('resolves removes all listeners once finished', async () => { 188 | startServerReturning(getOkWithOneXhr()); 189 | const page = await browser.newPage(); 190 | const count = page.listenerCount('request'); 191 | const pendingXHR = new PendingXHR(page); 192 | await page.goto(`http://localhost:${port}/go`); 193 | expect(pendingXHR.pendingXhrCount()).toEqual(1); 194 | setTimeout(() => { 195 | request1Resolver!([200, '']); 196 | }, 0); 197 | await pendingXHR.waitOnceForAllXhrFinished(); 198 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 199 | expect(page.listenerCount('request')).toBe(count); 200 | }); 201 | }); 202 | 203 | describe('several XHR', () => { 204 | it('resolves removes all listeners once finished', async () => { 205 | startServerReturning(getOkWithTwoXhr()); 206 | const page = await browser.newPage(); 207 | const count = page.listenerCount('request'); 208 | const pendingXHR = new PendingXHR(page); 209 | await page.goto(`http://localhost:${port}/go`); 210 | expect(pendingXHR.pendingXhrCount()).toEqual(2); 211 | setTimeout(() => { 212 | request1Resolver!([200, '']); 213 | }, 0); 214 | setTimeout(() => { 215 | request2Resolver!([200, '']); 216 | }, 0); 217 | await pendingXHR.waitOnceForAllXhrFinished(); 218 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 219 | expect(page.listenerCount('request')).toBe(count); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('waitForAllXhrFinished', () => { 225 | it('let a listenner connected', async () => { 226 | startServerReturning(OK_NO_XHR); 227 | const page = await browser.newPage(); 228 | const count = page.listenerCount('request'); 229 | const pendingXHR = new PendingXHR(page); 230 | await page.goto(`http://localhost:${port}/go`); 231 | await pendingXHR.waitForAllXhrFinished(); 232 | expect(page.listenerCount('request')).toBe(count + 1); 233 | }); 234 | it('returns immediatly if no xhr pending count', async () => { 235 | startServerReturning(OK_NO_XHR); 236 | const page = await browser.newPage(); 237 | const pendingXHR = new PendingXHR(page); 238 | await page.goto(`http://localhost:${port}/go`); 239 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 240 | await pendingXHR.waitForAllXhrFinished(); 241 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 242 | }); 243 | 244 | it('waits for 1 xhr to end', async () => { 245 | startServerReturning(getOkWithOneXhr()); 246 | const page = await browser.newPage(); 247 | const pendingXHR = new PendingXHR(page); 248 | await page.goto(`http://localhost:${port}/go`); 249 | expect(pendingXHR.pendingXhrCount()).toEqual(1); 250 | setTimeout(() => { 251 | request1Resolver!([200, '']); 252 | }, 0); 253 | await pendingXHR.waitForAllXhrFinished(); 254 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 255 | }); 256 | 257 | it('can be trigerred multiple times', async () => { 258 | startServerReturning(getOkWithOneXhr()); 259 | const page = await browser.newPage(); 260 | const pendingXHR = new PendingXHR(page); 261 | await page.goto(`http://localhost:${port}/go`); 262 | expect(pendingXHR.pendingXhrCount()).toEqual(1); 263 | setTimeout(() => { 264 | request1Resolver!([200, '']); 265 | }, 0); 266 | await pendingXHR.waitForAllXhrFinished(); 267 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 268 | await pendingXHR.waitForAllXhrFinished(); 269 | }); 270 | 271 | it('works with Promise.race', async () => { 272 | startServerReturning(getOkWithOneXhr()); 273 | const page = await browser.newPage(); 274 | const pendingXHR = new PendingXHR(page); 275 | await page.goto(`http://localhost:${port}/go`); 276 | setTimeout(() => { 277 | request1Resolver!([200, '']); 278 | }, 200); 279 | await Promise.race([ 280 | pendingXHR.waitForAllXhrFinished(), 281 | new Promise(resolve => { 282 | setTimeout(resolve, 100); 283 | }), 284 | ]); 285 | expect(pendingXHR.pendingXhrCount()).toEqual(1); 286 | 287 | await Promise.race([ 288 | pendingXHR.waitForAllXhrFinished(), 289 | new Promise(resolve => { 290 | setTimeout(resolve, 300); 291 | }), 292 | ]); 293 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 294 | }); 295 | 296 | it('waits for 2 xhr to end', async () => { 297 | startServerReturning(getOkWithTwoXhr()); 298 | const page = await browser.newPage(); 299 | const pendingXHR = new PendingXHR(page); 300 | await page.goto(`http://localhost:${port}/go`); 301 | expect(pendingXHR.pendingXhrCount()).toEqual(2); 302 | setTimeout(() => { 303 | request1Resolver!([200, 'first xhr finished']); 304 | }, 0); 305 | setTimeout(() => { 306 | request2Resolver!([200, 'second xhr finished']); 307 | }, 200); 308 | await pendingXHR.waitForAllXhrFinished(); 309 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 310 | }); 311 | 312 | it('handle correctly failed xhr', async () => { 313 | startServerReturning(getOkWithOneFailingXhr()); 314 | const page = await browser.newPage(); 315 | const pendingXHR = new PendingXHR(page); 316 | await page.goto(`http://localhost:${port}/go`); 317 | expect(pendingXHR.pendingXhrCount()).toEqual(1); 318 | await pendingXHR.waitForAllXhrFinished(); 319 | expect(pendingXHR.pendingXhrCount()).toEqual(0); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTPRequest, Page } from 'puppeteer'; 2 | 3 | interface ResolvableRequest extends HTTPRequest { 4 | pendingXhrResolver?: () => void; 5 | } 6 | 7 | export class PendingXHR { 8 | page: Page; 9 | 10 | resourceType: string; 11 | 12 | pendingXhrs: Set; 13 | 14 | finishedWithSuccessXhrs: Set; 15 | 16 | finishedWithErrorsXhrs: Set; 17 | 18 | promisees: Promise[]; 19 | 20 | requestListener: (request: ResolvableRequest) => void; 21 | 22 | requestFailedListener: (request: ResolvableRequest) => void; 23 | 24 | requestFinishedListener: (request: ResolvableRequest) => void; 25 | 26 | constructor(page: Page) { 27 | this.promisees = []; 28 | this.page = page; 29 | this.resourceType = 'xhr'; 30 | this.pendingXhrs = new Set(); 31 | this.finishedWithSuccessXhrs = new Set(); 32 | this.finishedWithErrorsXhrs = new Set(); 33 | 34 | this.requestListener = (request: ResolvableRequest) => { 35 | if (request.resourceType() === this.resourceType) { 36 | this.pendingXhrs.add(request); 37 | this.promisees.push( 38 | new Promise(resolve => { 39 | request.pendingXhrResolver = resolve; 40 | }), 41 | ); 42 | } 43 | }; 44 | 45 | this.requestFailedListener = (request: ResolvableRequest) => { 46 | if (request.resourceType() === this.resourceType) { 47 | this.pendingXhrs.delete(request); 48 | this.finishedWithErrorsXhrs.add(request); 49 | if (request.pendingXhrResolver) { 50 | request.pendingXhrResolver(); 51 | } 52 | delete request.pendingXhrResolver; 53 | } 54 | }; 55 | 56 | this.requestFinishedListener = (request: ResolvableRequest) => { 57 | if (request.resourceType() === this.resourceType) { 58 | this.pendingXhrs.delete(request); 59 | this.finishedWithSuccessXhrs.add(request); 60 | if (request.pendingXhrResolver) { 61 | request.pendingXhrResolver(); 62 | } 63 | delete request.pendingXhrResolver; 64 | } 65 | }; 66 | 67 | page.on('request', this.requestListener); 68 | page.on('requestfailed', this.requestFailedListener); 69 | page.on('requestfinished', this.requestFinishedListener); 70 | } 71 | 72 | removePageListeners() { 73 | this.page.off('request', this.requestListener); 74 | this.page.off('requestfailed', this.requestFailedListener); 75 | this.page.off('requestfinished', this.requestFinishedListener); 76 | } 77 | 78 | async waitForAllXhrFinished() { 79 | if (this.pendingXhrCount() === 0) { 80 | return; 81 | } 82 | await Promise.all(this.promisees); 83 | } 84 | 85 | async waitOnceForAllXhrFinished() { 86 | await this.waitForAllXhrFinished(); 87 | 88 | this.removePageListeners(); 89 | } 90 | 91 | pendingXhrCount() { 92 | return this.pendingXhrs.size; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "./dist", 5 | "outDir": "build", 6 | "module": "ES2015", 7 | "target": "es5", 8 | "lib": ["ES2015"], 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "rootDir": "src", 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true 17 | }, 18 | "exclude": ["node_modules", "build", "jest.setup.ts"], 19 | "types": ["typePatches"] 20 | } 21 | --------------------------------------------------------------------------------