From dc5a640bb7995487a5bd8beaed2b95e3eb85be3b Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Mon, 23 Feb 2026 12:37:21 +0000 Subject: [PATCH] fix(ui): update fetch --- ui/.depcheckrc.yaml | 2 +- ui/package-lock.json | 296 ++++++++---------- ui/package.json | 12 +- ui/src/App.test.tsx | 6 +- ui/src/Common/Fetch.test.ts | 43 +-- ui/src/Components/AlertAck/index.test.tsx | 168 +++++----- ui/src/Components/AlertHistory/index.test.tsx | 120 +++---- ui/src/Components/Fetcher/index.test.tsx | 8 +- .../Grid/AlertGrid/AlertGroup/index.test.tsx | 16 +- .../Grid/AlertGrid/GridLabelSelect.test.tsx | 6 +- .../MultiGridConfiguration.test.tsx | 6 +- .../MainModal/Configuration/index.test.tsx | 8 +- .../MainModal/MainModalContent.test.tsx | 8 +- ui/src/Components/MainModal/index.test.tsx | 8 +- ui/src/Components/NavBar/index.test.tsx | 8 +- .../SilenceModal/Browser/index.test.tsx | 48 +-- .../SilenceSubmitProgress.test.tsx | 85 ++--- ui/src/Components/SilenceModal/index.test.tsx | 8 +- ui/src/Hooks/useFetchAny.test.tsx | 87 ++--- ui/src/Hooks/useFetchDelete.test.tsx | 51 ++- ui/src/Hooks/useFetchGet.test.tsx | 83 ++--- ui/src/Stores/AlertStore.test.ts | 112 ++++--- ui/src/index.test.tsx | 10 +- ui/src/polyfill-load.test.tsx | 6 +- ui/src/polyfill-noop.test.tsx | 6 +- ui/src/setupTests.ts | 6 + ui/src/testEnvironment.ts | 11 + 27 files changed, 618 insertions(+), 610 deletions(-) create mode 100644 ui/src/testEnvironment.ts diff --git a/ui/.depcheckrc.yaml b/ui/.depcheckrc.yaml index 007b35cbe..2a3b5aba4 100644 --- a/ui/.depcheckrc.yaml +++ b/ui/.depcheckrc.yaml @@ -15,7 +15,7 @@ ignores: - "eslint-plugin-jest" - "node-fetch" - eslint + - fetch-mock - identity-obj-proxy - jest - - jest-fetch-mock - terser diff --git a/ui/package-lock.json b/ui/package-lock.json index 1abf5e8f4..d9251d434 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -52,6 +52,7 @@ "typeface-open-sans": "1.1.13" }, "devDependencies": { + "@fetch-mock/jest": "0.2.20", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "8.0.1", @@ -80,12 +81,11 @@ "eslint-plugin-prettier": "5.5.5", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", - "fetch-mock": "9.11.0", + "fetch-mock": "12.6.0", "identity-obj-proxy": "3.0.0", "jest": "30.2.0", "jest-environment-jsdom": "30.2.0", - "jest-fetch-mock": "3.0.3", - "node-fetch": "2.7.0", + "node-fetch": "3.3.2", "prettier": "3.8.1", "sass": "1.97.3", "terser": "5.46.0", @@ -2769,6 +2769,23 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fetch-mock/jest": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@fetch-mock/jest/-/jest-0.2.20.tgz", + "integrity": "sha512-DGX2bhBInodaWPMV3+UZ530aVM3wDj16sAPjFzkrwb0JwNWIQK07CNbYprQ3Tmd2ixDJeaNx2E0aNb+hRb8FFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-mock": "^12.6.0" + }, + "engines": { + "node": ">=18.11.0" + }, + "peerDependencies": { + "@jest/globals": "*", + "jest": "*" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", @@ -4578,6 +4595,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/glob-to-regexp": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz", + "integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -6338,16 +6362,6 @@ "node": ">= 6" } }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6395,6 +6409,16 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -7531,38 +7555,44 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fetch-mock": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", - "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz", + "integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.0.0", - "@babel/runtime": "^7.0.0", - "core-js": "^3.0.0", - "debug": "^4.1.1", - "glob-to-regexp": "^0.4.0", - "is-subset": "^0.1.1", - "lodash.isequal": "^4.5.0", - "path-to-regexp": "^2.2.1", - "querystring": "^0.2.0", - "whatwg-url": "^6.5.0" + "@types/glob-to-regexp": "^0.4.4", + "dequal": "^2.0.3", + "glob-to-regexp": "^0.4.1", + "regexparam": "^3.0.0" }, "engines": { - "node": ">=4.0.0" - }, - "funding": { - "type": "charity", - "url": "https://www.justgiving.com/refugee-support-europe" - }, - "peerDependencies": { - "node-fetch": "*" - }, - "peerDependenciesMeta": { - "node-fetch": { - "optional": true - } + "node": ">=18.11.0" } }, "node_modules/file-entry-cache": { @@ -7670,6 +7700,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8648,13 +8691,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", - "dev": true, - "license": "MIT" - }, "node_modules/is-symbol": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", @@ -9208,17 +9244,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-fetch-mock": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", - "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-fetch": "^3.0.4", - "promise-polyfill": "^8.1.3" - } - }, "node_modules/jest-haste-map": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", @@ -9958,14 +9983,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isobject": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", @@ -9985,13 +10002,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -10312,6 +10322,27 @@ "license": "MIT", "optional": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -10342,49 +10373,22 @@ } }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/node-int64": { @@ -10758,13 +10762,6 @@ "dev": true, "license": "ISC" }, - "node_modules/path-to-regexp": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", - "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", - "dev": true, - "license": "MIT" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10998,13 +10995,6 @@ "asap": "~2.0.6" } }, - "node_modules/promise-polyfill": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", - "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", - "dev": true, - "license": "MIT" - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -11076,17 +11066,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11434,6 +11413,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/regexpu-core": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", @@ -12594,16 +12583,6 @@ "node": ">=16" } }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/traverse": { "version": "0.6.11", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", @@ -13223,6 +13202,16 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -13263,25 +13252,6 @@ "node": ">=18" } }, - "node_modules/whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 542be295a..06c67dc40 100644 --- a/ui/package.json +++ b/ui/package.json @@ -48,6 +48,7 @@ "typeface-open-sans": "1.1.13" }, "devDependencies": { + "@fetch-mock/jest": "0.2.20", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "8.0.1", @@ -65,23 +66,22 @@ "@types/qs": "6.14.0", "@types/react": "17.0.91", "@types/react-dom": "17.0.26", + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", "@vitejs/plugin-legacy": "7.2.1", "@vitejs/plugin-react": "5.1.4", "csstype": "3.2.3", - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", "eslint": "8.57.1", "eslint-config-prettier": "10.1.8", "eslint-plugin-jest": "29.15.0", "eslint-plugin-prettier": "5.5.5", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", - "fetch-mock": "9.11.0", + "fetch-mock": "12.6.0", "identity-obj-proxy": "3.0.0", "jest": "30.2.0", "jest-environment-jsdom": "30.2.0", - "jest-fetch-mock": "3.0.3", - "node-fetch": "2.7.0", + "node-fetch": "3.3.2", "prettier": "3.8.1", "sass": "1.97.3", "terser": "5.46.0", @@ -110,7 +110,7 @@ "!src/Models/*.ts" ], "preset": "ts-jest/presets/js-with-ts", - "testEnvironment": "jest-environment-jsdom", + "testEnvironment": "/src/testEnvironment.ts", "transformIgnorePatterns": [ "node_modules/(?!(react-hotkeys-hook|react-idle-timer)/)" ], diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx index 64df5c1ca..7fd64cb37 100644 --- a/ui/src/App.test.tsx +++ b/ui/src/App.test.tsx @@ -1,6 +1,6 @@ import { render, within } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { mockMatchMedia } from "__fixtures__/matchMedia"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; @@ -39,8 +39,8 @@ beforeEach(() => { })); global.ResizeObserverEntry = jest.fn(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(EmptyAPIResponse()), }); }); diff --git a/ui/src/Common/Fetch.test.ts b/ui/src/Common/Fetch.test.ts index ad06237d0..3fa06eae2 100644 --- a/ui/src/Common/Fetch.test.ts +++ b/ui/src/Common/Fetch.test.ts @@ -2,11 +2,11 @@ import { CommonOptions, FetchGet, FetchRetryConfig } from "./Fetch"; import merge from "lodash.merge"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; beforeEach(() => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { status: 200, body: "ok", }); @@ -14,7 +14,7 @@ beforeEach(() => { afterEach(() => { jest.restoreAllMocks(); - fetchMock.reset(); + fetchMock.mockReset(); }); describe("Fetch", () => { @@ -23,15 +23,15 @@ describe("Fetch", () => { }; const methodOptions: { [key: string]: RequestInit } = { - FetchGet: { method: "GET", mode: "cors" }, + FetchGet: { method: "get", mode: "cors" }, }; for (const [name, func] of Object.entries(tests)) { it(`${name}: passes '{credentials: include}' to all requests`, async () => { const request = func("http://example.com/", {}, jest.fn()); await expect(request).resolves.toMatchObject({ status: 200 }); - expect(fetchMock.lastUrl()).toBe("http://example.com/"); - expect(fetchMock.lastOptions()).toEqual( + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://example.com/"); + expect(fetchMock.callHistory.lastCall()?.options).toEqual( merge({}, CommonOptions, methodOptions[name]), ); }); @@ -45,8 +45,8 @@ describe("Fetch", () => { () => {}, ); await expect(request).resolves.toMatchObject({ status: 200 }); - expect(fetchMock.lastUrl()).toBe("http://example.com/"); - expect(fetchMock.lastOptions()).toEqual( + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://example.com/"); + expect(fetchMock.callHistory.lastCall()?.options).toEqual( merge( {}, CommonOptions, @@ -67,8 +67,8 @@ describe("Fetch", () => { () => {}, ); await expect(request).resolves.toMatchObject({ status: 200 }); - expect(fetchMock.lastUrl()).toBe("http://example.com/"); - expect(fetchMock.lastOptions()).toEqual( + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://example.com/"); + expect(fetchMock.callHistory.lastCall()?.options).toEqual( merge({}, CommonOptions, methodOptions[name], { credentials: "omit", redirect: "follow", @@ -78,36 +78,41 @@ describe("Fetch", () => { } it("FetchGet switches to no-cors for the last retry", async () => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("Fetch error"), }); const request = FetchGet("http://example.com", {}, jest.fn()); await expect(request).rejects.toThrow("Fetch error"); - expect(fetchMock.calls()).toHaveLength(FetchRetryConfig.retries + 1); - expect(fetchMock.calls().map((r) => r[1])).toMatchObject( + expect(fetchMock.callHistory.calls()).toHaveLength( + FetchRetryConfig.retries + 1, + ); + expect(fetchMock.callHistory.calls().map((r) => r?.options)).toMatchObject( Array.from(Array(FetchRetryConfig.retries + 1).keys(), (i) => ({ mode: i < FetchRetryConfig.retries ? "cors" : "no-cors", credentials: "include", })), ); // ensure that the the second to last call was with cors - expect(fetchMock.calls()[fetchMock.calls().length - 2][1]).toMatchObject({ + expect( + fetchMock.callHistory.calls()[fetchMock.callHistory.calls().length - 2] + ?.options, + ).toMatchObject({ mode: "cors", credentials: "include", }); // ensure that the last call was with no-cors - expect(fetchMock.lastOptions()).toMatchObject({ + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ mode: "no-cors", credentials: "include", }); }); it("FetchGet calls beforeRetry before each retry", async () => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("Fetch error"), }); diff --git a/ui/src/Components/AlertAck/index.test.tsx b/ui/src/Components/AlertAck/index.test.tsx index e5a7305b7..c8bdcd88c 100644 --- a/ui/src/Components/AlertAck/index.test.tsx +++ b/ui/src/Components/AlertAck/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, fireEvent, screen, waitFor } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockAlertGroup, MockAlert } from "__fixtures__/Alerts"; import type { APIAlertT, APIAlertGroupT } from "Models/APITypes"; @@ -70,8 +70,8 @@ beforeEach(() => { foo: ["bar", "baz"], }; - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockReset(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -84,7 +84,7 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.resetHistory(); + fetchMock.mockReset(); jest.clearAllTimers(); jest.clearAllMocks(); jest.restoreAllMocks(); @@ -106,7 +106,7 @@ const renderAndClick = async () => { const badge = screen.getByRole("img", { hidden: true }).parentElement; if (badge) fireEvent.click(badge); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); }; @@ -133,21 +133,16 @@ describe("", () => { }); it("uses faExclamationCircle after failed fetch", async () => { - fetchMock.mock( - "*", - { - status: 500, - body: "error message", - }, - { - overwriteRoutes: true, - }, - ); + fetchMock.mockReset(); + fetchMock.route("*", { + status: 500, + body: "error message", + }); renderAlertAck(); const badge = screen.getByRole("img", { hidden: true }).parentElement; if (badge) fireEvent.click(badge); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); await waitFor(() => { expect(screen.getByRole("img", { hidden: true })).toHaveClass( @@ -157,21 +152,16 @@ describe("", () => { }); it("resets faExclamationCircle after 20s", async () => { - fetchMock.mock( - "*", - { - status: 500, - body: "error message", - }, - { - overwriteRoutes: true, - }, - ); + fetchMock.mockReset(); + fetchMock.route("*", { + status: 500, + body: "error message", + }); renderAlertAck(); const badge = screen.getByRole("img", { hidden: true }).parentElement; if (badge) fireEvent.click(badge); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); await waitFor(() => { expect(screen.getByRole("img", { hidden: true })).toHaveClass( @@ -195,7 +185,7 @@ describe("", () => { const badge = screen.getByRole("img", { hidden: true }).parentElement; if (badge) fireEvent.click(badge); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); await waitFor(() => { expect(screen.getByRole("img", { hidden: true })).toHaveClass( @@ -206,10 +196,10 @@ describe("", () => { it("sends a POST request on click", async () => { await renderAndClick(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastOptions()).toMatchObject({ - method: "POST", - headers: { "Content-Type": "application/json" }, + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ + method: "post", + headers: { "content-type": "application/json" }, }); }); @@ -238,22 +228,22 @@ describe("", () => { }; await renderAndClick(); - expect(fetchMock.calls()).toHaveLength(2); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://m1.example.com/api/v2/silences", ); - expect(fetchMock.calls()[0][1]).toMatchObject({ - method: "POST", + expect(fetchMock.callHistory.calls()[0]?.options).toMatchObject({ + method: "post", credentials: "same-origin", - headers: { "X-Cluster": "c1" }, + headers: { "x-cluster": "c1" }, }); - expect(fetchMock.calls()[1][0]).toBe( + expect(fetchMock.callHistory.calls()[1]?.url).toBe( "http://m3.example.com/api/v2/silences", ); - expect(fetchMock.calls()[1][1]).toMatchObject({ - method: "POST", + expect(fetchMock.callHistory.calls()[1]?.options).toMatchObject({ + method: "post", credentials: "include", - headers: { "X-Cluster": "c2" }, + headers: { "x-cluster": "c2" }, }); }); @@ -282,11 +272,11 @@ describe("", () => { }; await renderAndClick(); - expect(fetchMock.calls()).toHaveLength(2); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://m2.example.com/api/v2/silences", ); - expect(fetchMock.calls()[1][0]).toBe( + expect(fetchMock.callHistory.calls()[1]?.url).toBe( "http://m4.example.com/api/v2/silences", ); }); @@ -297,16 +287,16 @@ describe("", () => { fireEvent.click(button!); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); fireEvent.click(button!); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); }); it("sends correct payload", async () => { - fetchMock.mock( + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -319,7 +309,9 @@ describe("", () => { silenceFormStore.data.setAuthor("karma/ui"); await renderAndClick(); - expect(JSON.parse((fetchMock.lastOptions() as any).body)).toEqual({ + expect( + JSON.parse((fetchMock.callHistory.lastCall()?.options as any).body), + ).toEqual({ comment: "COMMENT", createdBy: "karma/ui", endsAt: "2000-02-01T00:02:03.000Z", @@ -349,7 +341,9 @@ describe("", () => { }, }); await renderAndClick(); - expect(JSON.parse((fetchMock.lastOptions() as any).body)).toEqual({ + expect( + JSON.parse((fetchMock.callHistory.lastCall()?.options as any).body), + ).toEqual({ comment: "comment", createdBy: "me", endsAt: "2000-02-01T00:03:57.000Z", @@ -379,7 +373,9 @@ describe("", () => { }, }); await renderAndClick(); - expect(JSON.parse((fetchMock.lastOptions() as any).body)).toEqual({ + expect( + JSON.parse((fetchMock.callHistory.lastCall()?.options as any).body), + ).toEqual({ comment: "ACK! This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT", createdBy: "me", @@ -410,7 +406,9 @@ describe("", () => { }, }); await renderAndClick(); - const comment = JSON.parse((fetchMock.lastOptions() as any).body).comment; + const comment = JSON.parse( + (fetchMock.callHistory.lastCall()?.options as any).body, + ).comment; expect(comment).not.toEqual( "ACK! This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT", ); @@ -433,7 +431,9 @@ describe("", () => { }, }); await renderAndClick(); - expect(JSON.parse((fetchMock.lastOptions() as any).body)).toEqual({ + expect( + JSON.parse((fetchMock.callHistory.lastCall()?.options as any).body), + ).toEqual({ comment: "FOO: bar", createdBy: "auth@example.com", endsAt: "2000-02-01T00:03:42.000Z", @@ -465,7 +465,9 @@ describe("", () => { }); silenceFormStore.data.setAuthor("bob@example.com"); await renderAndClick(); - expect(JSON.parse((fetchMock.lastOptions() as any).body)).toEqual({ + expect( + JSON.parse((fetchMock.callHistory.lastCall()?.options as any).body), + ).toEqual({ comment: "FOO: bar", createdBy: "bob@example.com", endsAt: "2000-02-01T00:03:42.000Z", @@ -496,7 +498,9 @@ describe("", () => { }); silenceFormStore.data.setAuthor(""); await renderAndClick(); - expect(JSON.parse((fetchMock.lastOptions() as any).body)).toEqual({ + expect( + JSON.parse((fetchMock.callHistory.lastCall()?.options as any).body), + ).toEqual({ comment: "FOO: bar", createdBy: "me", endsAt: "2000-02-01T00:03:42.000Z", @@ -515,25 +519,25 @@ describe("", () => { it("sends POST request to /api/v2/silences", async () => { await renderAndClick(); - const uri = fetchMock.calls()[0][0]; + const uri = fetchMock.callHistory.calls()[0]?.url; expect(uri).toBe("http://localhost/api/v2/silences"); }); it("will retry on another cluster member after 500 response", async () => { - fetchMock.reset(); - fetchMock.mock("http://m1.example.com/api/v2/silences", { + fetchMock.mockReset(); + fetchMock.route("http://m1.example.com/api/v2/silences", { status: 500, body: "error message", }); - fetchMock.mock("http://m2.example.com/api/v2/silences", { + fetchMock.route("http://m2.example.com/api/v2/silences", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ silenceID: "123" }), }); - fetchMock.mock("http://m3.example.com/api/v2/silences", { + fetchMock.route("http://m3.example.com/api/v2/silences", { status: 500, body: "error message", }); - fetchMock.mock("http://m4.example.com/api/v2/silences", { + fetchMock.route("http://m4.example.com/api/v2/silences", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ silenceID: "456" }), }); @@ -564,36 +568,36 @@ describe("", () => { const button = container.querySelector("span.badge"); fireEvent.click(button!); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(4); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(4); + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://m1.example.com/api/v2/silences", ); - expect(fetchMock.calls()[1][0]).toBe( + expect(fetchMock.callHistory.calls()[1]?.url).toBe( "http://m2.example.com/api/v2/silences", ); - expect(fetchMock.calls()[2][0]).toBe( + expect(fetchMock.callHistory.calls()[2]?.url).toBe( "http://m3.example.com/api/v2/silences", ); - expect(fetchMock.calls()[3][0]).toBe( + expect(fetchMock.callHistory.calls()[3]?.url).toBe( "http://m4.example.com/api/v2/silences", ); }); it("will retry on another cluster member after fetch failure", async () => { - fetchMock.reset(); - fetchMock.mock("http://m1.example.com/api/v2/silences", { + fetchMock.mockReset(); + fetchMock.route("http://m1.example.com/api/v2/silences", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://m2.example.com/api/v2/silences", { + fetchMock.route("http://m2.example.com/api/v2/silences", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ silenceID: "123" }), }); - fetchMock.mock("http://m3.example.com/api/v2/silences", { + fetchMock.route("http://m3.example.com/api/v2/silences", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://m4.example.com/api/v2/silences", { + fetchMock.route("http://m4.example.com/api/v2/silences", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ silenceID: "123" }), }); @@ -624,22 +628,22 @@ describe("", () => { const button = container.querySelector("span.badge"); fireEvent.click(button!); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(4); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(4); + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://m1.example.com/api/v2/silences", ); - expect(fetchMock.calls()[1][0]).toBe( + expect(fetchMock.callHistory.calls()[1]?.url).toBe( "http://m2.example.com/api/v2/silences", ); - expect(fetchMock.calls()[2][0]).toBe( + expect(fetchMock.callHistory.calls()[2]?.url).toBe( "http://m3.example.com/api/v2/silences", ); - expect(fetchMock.calls()[3][0]).toBe( + expect(fetchMock.callHistory.calls()[3]?.url).toBe( "http://m4.example.com/api/v2/silences", ); }); @@ -671,10 +675,10 @@ describe("", () => { const button = container.querySelector("span.badge"); fireEvent.click(button!); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://am1.example.com/api/v2/silences", ); expect(consoleSpy).toHaveBeenCalledTimes(1); diff --git a/ui/src/Components/AlertHistory/index.test.tsx b/ui/src/Components/AlertHistory/index.test.tsx index 3f43fa730..1b5db68ad 100644 --- a/ui/src/Components/AlertHistory/index.test.tsx +++ b/ui/src/Components/AlertHistory/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { useInView } from "react-intersection-observer"; @@ -73,15 +73,15 @@ beforeEach(() => { }); afterEach(() => { - fetchMock.resetHistory(); - fetchMock.reset(); + fetchMock.mockClear(); + fetchMock.mockReset(); jest.useRealTimers(); }); describe("", () => { it("send a correct payload with empty grid", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -99,10 +99,10 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.options?.body).toStrictEqual( JSON.stringify({ sources: [ "https://secure.example.com/graph", @@ -115,8 +115,8 @@ describe("", () => { }); it("send a correct payload with non-empty grid", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -132,10 +132,10 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.options?.body).toStrictEqual( JSON.stringify({ sources: [ "https://secure.example.com/graph", @@ -148,8 +148,8 @@ describe("", () => { }); it("send a correct payload with @cluster grid", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -167,10 +167,10 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.options?.body).toStrictEqual( JSON.stringify({ sources: [ "https://secure.example.com/graph", @@ -183,8 +183,8 @@ describe("", () => { }); it("send a correct payload with shared labels", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -204,10 +204,10 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.options?.body).toStrictEqual( JSON.stringify({ sources: [ "https://secure.example.com/graph", @@ -226,8 +226,8 @@ describe("", () => { }); it("matches snapshot with empty response", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -243,16 +243,16 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); expect(asFragment()).toMatchSnapshot(); unmount(); }); it("matches snapshot with rainbow response", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -268,17 +268,17 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); expect(asFragment()).toMatchSnapshot(); unmount(); }); it("doesn't fetch when not in view", async () => { // Verifies that react-intersection-observer prevents fetch when component is outside viewport - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -299,17 +299,17 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); unmount(); - expect(fetchMock.calls()).toHaveLength(0); + expect(fetchMock.callHistory.calls()).toHaveLength(0); }); it("fetches when component transitions from out-of-view to in-view", async () => { // Verifies that react-intersection-observer triggers fetch when component becomes visible - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -330,9 +330,9 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(0); + expect(fetchMock.callHistory.calls()).toHaveLength(0); (useInView as jest.MockedFunction).mockReturnValue([ jest.fn(), @@ -341,16 +341,16 @@ describe("", () => { rerender(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); unmount(); }); it("fetches an update after 300 seconds", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -372,28 +372,28 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); await act(async () => { jest.advanceTimersByTime(1000 * 299); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); await act(async () => { jest.advanceTimersByTime(1000 * 2); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()).toHaveLength(2); unmount(); }); it("handles responses with errors", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -409,16 +409,16 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); expect(asFragment()).toMatchSnapshot(); unmount(); }); it("handles fetch errors", async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { status: 500, @@ -434,9 +434,9 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); expect(asFragment()).toMatchSnapshot(); unmount(); }); @@ -567,8 +567,8 @@ describe("", () => { }; it(`${testCase.title}`, async () => { - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -583,7 +583,7 @@ describe("", () => { , ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); const rects = Array.from(container.querySelectorAll("rect")).map( diff --git a/ui/src/Components/Fetcher/index.test.tsx b/ui/src/Components/Fetcher/index.test.tsx index 7cf9888ee..6935d3e6d 100644 --- a/ui/src/Components/Fetcher/index.test.tsx +++ b/ui/src/Components/Fetcher/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; @@ -48,14 +48,14 @@ afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); jest.useRealTimers(); - fetchMock.reset(); + fetchMock.mockReset(); }); const MockEmptyAPIResponseWithoutFilters = () => { const response = EmptyAPIResponse(); response.filters = []; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { status: 200, body: JSON.stringify(response), }); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx index becfc2db4..681c7516a 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockAlert, MockAlertGroup } from "__fixtures__/Alerts"; import { @@ -465,8 +465,8 @@ describe(" renderConfig", () => { }); it("uses 'z-index: 100' style after setIsMenuOpen() is called on any Alert", async () => { - fetchMock.reset(); - fetchMock.mock("*", { body: "" }); + fetchMock.mockReset(); + fetchMock.route("*", { body: "" }); const promise = Promise.resolve(); MockAlerts(5, 5); @@ -555,8 +555,8 @@ describe(" card theme", () => { }); it("renders AlertHistory when enabled", async () => { - fetchMock.reset(); - fetchMock.mock( + fetchMock.mockReset(); + fetchMock.route( "/history.json", { headers: { "Content-Type": "application/json" }, @@ -574,11 +574,11 @@ describe(" card theme", () => { group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; const { container } = renderAlertGroup(jest.fn()); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); expect(container.innerHTML).toMatch(/alert-history/); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][0]).toBe("/history.json"); + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.url).toContain("/history.json"); }); it("doesn't render AlertHistory when disabled", () => { diff --git a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx index 1a96d4aa2..1284b50b3 100644 --- a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockGrid } from "__fixtures__/Stories"; import { MockThemeContextWithoutAnimations } from "__fixtures__/Theme"; @@ -25,8 +25,8 @@ declare let document: any; declare let window: any; beforeEach(() => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify([]), }); diff --git a/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.tsx b/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.tsx index 62ba698bb..e12a8ed3f 100644 --- a/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.tsx +++ b/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockThemeContext } from "__fixtures__/Theme"; import { useFetchGetMock } from "__fixtures__/useFetchGet"; @@ -10,8 +10,8 @@ import { MultiGridConfiguration } from "./MultiGridConfiguration"; let settingsStore: Settings; beforeEach(() => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify([]), }); diff --git a/ui/src/Components/MainModal/Configuration/index.test.tsx b/ui/src/Components/MainModal/Configuration/index.test.tsx index 1fb3ac806..8b1a2b4e9 100644 --- a/ui/src/Components/MainModal/Configuration/index.test.tsx +++ b/ui/src/Components/MainModal/Configuration/index.test.tsx @@ -1,6 +1,6 @@ import { render } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockThemeContext } from "__fixtures__/Theme"; import { Settings } from "Stores/Settings"; @@ -8,8 +8,8 @@ import { ThemeContext } from "Components/Theme"; import { Configuration } from "."; beforeEach(() => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { status: 200, body: JSON.stringify([]), }); @@ -17,7 +17,7 @@ beforeEach(() => { afterEach(() => { jest.restoreAllMocks(); - fetchMock.reset(); + fetchMock.mockReset(); }); describe("", () => { diff --git a/ui/src/Components/MainModal/MainModalContent.test.tsx b/ui/src/Components/MainModal/MainModalContent.test.tsx index 7796afc11..ef3e657be 100644 --- a/ui/src/Components/MainModal/MainModalContent.test.tsx +++ b/ui/src/Components/MainModal/MainModalContent.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockThemeContext } from "__fixtures__/Theme"; import { AlertStore } from "Stores/AlertStore"; @@ -16,15 +16,15 @@ beforeEach(() => { alertStore = new AlertStore([]); settingsStore = new Settings(null); onHide.mockClear(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify([]), }); }); afterEach(() => { jest.restoreAllMocks(); - fetchMock.reset(); + fetchMock.mockReset(); }); const renderModalContent = () => { diff --git a/ui/src/Components/MainModal/index.test.tsx b/ui/src/Components/MainModal/index.test.tsx index cbaaf3480..aaa5c579d 100644 --- a/ui/src/Components/MainModal/index.test.tsx +++ b/ui/src/Components/MainModal/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, screen, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockThemeContext } from "__fixtures__/Theme"; import { AlertStore } from "Stores/AlertStore"; @@ -18,15 +18,15 @@ beforeEach(() => { settingsStore = new Settings(null); jest.useFakeTimers(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify([]), }); }); afterEach(() => { jest.restoreAllMocks(); - fetchMock.reset(); + fetchMock.mockReset(); document.body.className = ""; }); diff --git a/ui/src/Components/NavBar/index.test.tsx b/ui/src/Components/NavBar/index.test.tsx index 581b63b31..ceb0801ba 100644 --- a/ui/src/Components/NavBar/index.test.tsx +++ b/ui/src/Components/NavBar/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, screen } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { useIdleTimer } from "react-idle-timer"; @@ -73,8 +73,8 @@ beforeEach(() => { clusters: { dev: ["dev"] }, }); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(EmptyAPIResponse()), }); }); @@ -84,7 +84,7 @@ afterEach(() => { jest.runOnlyPendingTimers(); jest.useRealTimers(); }); - fetchMock.reset(); + fetchMock.mockReset(); }); const renderNavbar = (fixedTop?: boolean) => { diff --git a/ui/src/Components/SilenceModal/Browser/index.test.tsx b/ui/src/Components/SilenceModal/Browser/index.test.tsx index f8644a73c..d922c2681 100644 --- a/ui/src/Components/SilenceModal/Browser/index.test.tsx +++ b/ui/src/Components/SilenceModal/Browser/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { useFetchGetMock } from "__fixtures__/useFetchGet"; import { MockSilence } from "__fixtures__/Alerts"; @@ -63,7 +63,7 @@ afterEach(() => { localStorage.setItem("fetchConfig.interval", ""); global.window.innerWidth = 1024; - fetchMock.reset(); + fetchMock.mockReset(); }); const MockSilenceList = (count: number): APIManagedSilenceT[] => { @@ -523,7 +523,7 @@ describe("", () => { cancelGet: jest.fn(), }); - fetchMock.mock("*", { + fetchMock.route("*", { status: 200, body: "ok", }); @@ -554,10 +554,10 @@ describe("", () => { await act(async () => { jest.advanceTimersByTime(2 * 60); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(3); + expect(fetchMock.callHistory.calls()).toHaveLength(3); const closeBtn = container.querySelector(".btn-close"); if (closeBtn) fireEvent.click(closeBtn); @@ -611,7 +611,7 @@ describe("", () => { cancelGet: jest.fn(), }); - fetchMock.mock("*", { + fetchMock.route("*", { status: 200, body: "ok", }); @@ -662,10 +662,10 @@ describe("", () => { await act(async () => { jest.advanceTimersByTime(2 * 60); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()).toHaveLength(2); const closeBtn = container.querySelector(".btn-close"); if (closeBtn) fireEvent.click(closeBtn); @@ -693,7 +693,7 @@ describe("", () => { cancelGet: jest.fn(), }); - fetchMock.mock("*", { + fetchMock.route("*", { status: 200, body: "ok", }); @@ -715,7 +715,7 @@ describe("", () => { await act(async () => { jest.advanceTimersByTime(60); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); PressKey("Escape", 27); @@ -781,30 +781,30 @@ describe("", () => { clusters: { am: ["am1", "am2", "am3"], failed: ["am4"] }, }); - fetchMock.reset(); - fetchMock.mock("http://m1.example.com/api/v2/silence/1", { + fetchMock.mockReset(); + fetchMock.route("http://m1.example.com/api/v2/silence/1", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://m2.example.com/api/v2/silence/1", { + fetchMock.route("http://m2.example.com/api/v2/silence/1", { status: 502, body: "Bad Gateway", }); - fetchMock.mock("http://m3.example.com/api/v2/silence/1", { + fetchMock.route("http://m3.example.com/api/v2/silence/1", { status: 200, body: "OK", }); - fetchMock.mock("http://m1.example.com/api/v2/silence/2", { + fetchMock.route("http://m1.example.com/api/v2/silence/2", { throws: "error text", }); - fetchMock.mock("http://m2.example.com/api/v2/silence/2", { + fetchMock.route("http://m2.example.com/api/v2/silence/2", { status: 200, body: "OK", }); - fetchMock.mock("http://m4.example.com/api/v2/silence/3", { + fetchMock.route("http://m4.example.com/api/v2/silence/3", { status: 400, body: "Bad Request", }); - fetchMock.mock("http://m4.example.com/api/v2/silence/4", { + fetchMock.route("http://m4.example.com/api/v2/silence/4", { throws: new TypeError("failed to fetch"), }); @@ -866,11 +866,11 @@ describe("", () => { await act(async () => { jest.advanceTimersByTime(10 * 60); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(7); - const uris = fetchMock.calls().map((c) => c[0]); + expect(fetchMock.callHistory.calls()).toHaveLength(7); + const uris = fetchMock.callHistory.calls().map((c) => c?.url); expect(uris).toContainEqual("http://m1.example.com/api/v2/silence/1"); expect(uris).toContainEqual("http://m1.example.com/api/v2/silence/2"); expect(uris).toContainEqual("http://m4.example.com/api/v2/silence/3"); @@ -905,8 +905,8 @@ describe("", () => { clusters: { am: ["am1"] }, }); - fetchMock.reset(); - fetchMock.mock("http://m1.example.com/api/v2/silence/1", { + fetchMock.mockReset(); + fetchMock.route("http://m1.example.com/api/v2/silence/1", { status: 500, body: "Internal Server Error", }); @@ -948,7 +948,7 @@ describe("", () => { await act(async () => { jest.advanceTimersByTime(10 * 60); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); const errorDisplay = document.body.querySelector(".bg-dark.text-white"); diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.tsx b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.tsx index ae0aff620..2383199e4 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore, NewClusterRequest } from "Stores/SilenceFormStore"; @@ -43,8 +43,8 @@ beforeEach(() => { ]), }); - fetchMock.resetHistory(); - fetchMock.mock( + fetchMock.mockClear(); + fetchMock.route( "*", { headers: { "Content-Type": "application/json" }, @@ -58,7 +58,7 @@ beforeEach(() => { afterEach(() => { jest.restoreAllMocks(); - fetchMock.resetHistory(); + fetchMock.mockClear(); }); const renderSilenceSubmitProgress = () => { @@ -83,29 +83,28 @@ describe("", () => { it("sends a request on mount", async () => { renderSilenceSubmitProgress(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); }); it("appends /api/v2/silences to the passed URI", async () => { renderSilenceSubmitProgress(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - const uri = fetchMock.calls()[0][0]; + const uri = fetchMock.callHistory.calls()[0]?.url; expect(uri).toBe("http://localhost/api/v2/silences"); }); it("sends correct JSON payload", async () => { renderSilenceSubmitProgress(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - const payload = fetchMock.calls()[0][1]; + const payload = fetchMock.callHistory.calls()[0]?.options; expect(payload).toMatchObject({ - method: "POST", - headers: { "Content-Type": "application/json", foo: "bar" }, + method: "post", body: JSON.stringify({ matchers: [], startsAt: "now", @@ -114,6 +113,10 @@ describe("", () => { comment: "fake payload", }), }); + // Check headers - stored as plain object + const headers = payload?.headers as Record; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["foo"]).toBe("bar"); }); it("uses CORS credentials from alertmanager config", async () => { @@ -122,21 +125,23 @@ describe("", () => { alertStore.data.setUpstreams(upstreams); renderSilenceSubmitProgress(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()[0][0]).toBe("http://localhost/api/v2/silences"); - expect(fetchMock.calls()[0][1]).toMatchObject({ + expect(fetchMock.callHistory.calls()[0]?.url).toBe( + "http://localhost/api/v2/silences", + ); + expect(fetchMock.callHistory.calls()[0]?.options).toMatchObject({ credentials: "same-origin", - method: "POST", + method: "post", }); }); it("will retry on another cluster member after fetch failure", async () => { - fetchMock.reset(); - fetchMock.mock("http://am2.example.com/api/v2/silences", { + fetchMock.mockReset(); + fetchMock.route("http://am2.example.com/api/v2/silences", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://am1.example.com/api/v2/silences", { + fetchMock.route("http://am1.example.com/api/v2/silences", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ silenceID: "123456789" }), }); @@ -190,23 +195,23 @@ describe("", () => { />, ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://am2.example.com/api/v2/silences", ); - expect(fetchMock.calls()).toHaveLength(2); - expect(fetchMock.calls()[1][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()[1]?.url).toBe( "http://am1.example.com/api/v2/silences", ); }); it("will use error message from last failed cluster member", async () => { - fetchMock.reset(); - fetchMock.mock("http://am2.example.com/api/v2/silences", { + fetchMock.mockReset(); + fetchMock.route("http://am2.example.com/api/v2/silences", { throws: new TypeError("failed to fetch from am2"), }); - fetchMock.mock("http://am1.example.com/api/v2/silences", { + fetchMock.route("http://am1.example.com/api/v2/silences", { throws: new TypeError("failed to fetch from am1"), }); alertStore.data.setUpstreams({ @@ -259,9 +264,9 @@ describe("", () => { />, ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()).toHaveLength(2); expect(silenceFormStore.data.requestsByCluster.ha).toMatchObject({ isDone: true, error: "failed to fetch from am1", @@ -310,9 +315,9 @@ describe("", () => { />, ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://am1.example.com/api/v2/silences", ); expect(consoleSpy).toHaveBeenCalledTimes(1); @@ -347,9 +352,9 @@ describe("", () => { />, ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(0); + expect(fetchMock.callHistory.calls()).toHaveLength(0); expect(consoleSpy).toHaveBeenCalledTimes(2); }); @@ -411,10 +416,10 @@ describe("", () => { />, ); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][0]).toBe( + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.url).toBe( "http://am1.example.com/api/v2/silences", ); expect(logs).toEqual(['Alertmanager instance "am2" is read-only']); @@ -478,7 +483,7 @@ describe("", () => { silenceFormStore={silenceFormStore} />, ); - expect(fetchMock.calls()).toHaveLength(0); + expect(fetchMock.callHistory.calls()).toHaveLength(0); expect(logs).toEqual([ 'Alertmanager instance "am2" is read-only', 'Alertmanager instance "am1" is read-only', @@ -489,7 +494,7 @@ describe("", () => { it("renders silence link on successful fetch", async () => { renderSilenceSubmitProgress(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); expect( silenceFormStore.data.requestsByCluster.mockAlertmanager, @@ -502,14 +507,14 @@ describe("", () => { }); it("sets error icon on failed fetch", async () => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { status: 500, body: "error message", }); renderSilenceSubmitProgress(); await act(async () => { - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); expect( silenceFormStore.data.requestsByCluster.mockAlertmanager, diff --git a/ui/src/Components/SilenceModal/index.test.tsx b/ui/src/Components/SilenceModal/index.test.tsx index 5b3b7178f..0db63ddc5 100644 --- a/ui/src/Components/SilenceModal/index.test.tsx +++ b/ui/src/Components/SilenceModal/index.test.tsx @@ -2,7 +2,7 @@ import { act } from "react-dom/test-utils"; import { render, fireEvent } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { MockThemeContext } from "__fixtures__/Theme"; import { ThemeContext } from "Components/Theme"; @@ -21,15 +21,15 @@ beforeEach(() => { silenceFormStore = new SilenceFormStore(); jest.useFakeTimers(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify([]), }); }); afterEach(() => { jest.restoreAllMocks(); - fetchMock.reset(); + fetchMock.mockReset(); document.body.className = ""; }); diff --git a/ui/src/Hooks/useFetchAny.test.tsx b/ui/src/Hooks/useFetchAny.test.tsx index 6d62a1106..3a9163c65 100644 --- a/ui/src/Hooks/useFetchAny.test.tsx +++ b/ui/src/Hooks/useFetchAny.test.tsx @@ -3,43 +3,40 @@ import { act } from "react-dom/test-utils"; import { renderHook } from "@testing-library/react-hooks"; import { render } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { useFetchAny, UpstreamT } from "./useFetchAny"; describe("useFetchAny", () => { - beforeAll(() => { - fetchMock.mock("http://localhost/ok", "body ok"); - fetchMock.mock("http://localhost/ok/json", { + beforeEach(() => { + fetchMock.mockReset(); + fetchMock.route("http://localhost/ok", "body ok"); + fetchMock.route("http://localhost/ok/json", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "ok" }), }); - fetchMock.mock("http://localhost/500", { + fetchMock.route("http://localhost/500", { status: 500, body: "fake error", }); - fetchMock.mock("http://localhost/401", 401); - fetchMock.mock("http://localhost/error", { + fetchMock.route("http://localhost/401", 401); + fetchMock.route("http://localhost/error", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://localhost/unknown", { - throws: "foo", + fetchMock.route("http://localhost/unknown", { + throws: new Error("foo"), }); }); - beforeEach(() => { - fetchMock.resetHistory(); - }); - afterEach(() => { - fetchMock.resetHistory(); + fetchMock.mockClear(); }); it("does nothing on empty upstream list", async () => { const upstreams: UpstreamT[] = []; const { result } = renderHook(() => useFetchAny(upstreams)); - expect(fetchMock.calls()).toHaveLength(0); + expect(fetchMock.callHistory.calls()).toHaveLength(0); expect(result.current.inProgress).toBe(false); }); @@ -49,10 +46,10 @@ describe("useFetchAny", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ - method: "GET", + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ + method: "get", credentials: "include", mode: "cors", redirect: "follow", @@ -63,17 +60,17 @@ describe("useFetchAny", () => { const upstreams: UpstreamT[] = [ { uri: "http://localhost/ok", - options: { method: "POST", credentials: "same-origin" }, + options: { method: "post", credentials: "same-origin" }, }, ]; const { waitForNextUpdate } = renderHook(() => useFetchAny(upstreams)); await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ - method: "POST", + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ + method: "post", credentials: "same-origin", mode: "cors", redirect: "follow", @@ -86,9 +83,9 @@ describe("useFetchAny", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ mode: "cors", credentials: "include", redirect: "follow", @@ -204,13 +201,13 @@ describe("useFetchAny", () => { await waitForNextUpdate(); expect(result.current.response).toBe(null); - expect(result.current.error).toBe("unknown error: foo"); + expect(result.current.error).toBe("foo"); expect(result.current.inProgress).toBe(false); expect(result.current.responseURI).toBe(null); }); it("doesn't update response after cleanup", async () => { - fetchMock.mock( + fetchMock.route( "http://localhost/slow/ok", new Promise((res) => setTimeout(() => res("ok"), 1000)), ); @@ -230,11 +227,11 @@ describe("useFetchAny", () => { const { unmount } = render(); unmount(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't update error on 500 response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/500", { + fetchMock.route("http://localhost/slow/500", { delay: 1000, status: 500, body: "error", @@ -257,11 +254,11 @@ describe("useFetchAny", () => { unmount(); }); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't update error on failed response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/error", { + fetchMock.route("http://localhost/slow/error", { delay: 1000, throws: new TypeError("failed to fetch"), }); @@ -283,7 +280,7 @@ describe("useFetchAny", () => { unmount(); }); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't retry on success", async () => { @@ -298,8 +295,8 @@ describe("useFetchAny", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.calls()[0][0]).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()[0]?.url).toBe("http://localhost/ok"); expect(result.current.response).toBe("body ok"); expect(result.current.error).toBe(null); @@ -319,10 +316,12 @@ describe("useFetchAny", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(3); - expect(fetchMock.calls()[0][0]).toBe("http://localhost/500"); - expect(fetchMock.calls()[1][0]).toBe("http://localhost/error"); - expect(fetchMock.calls()[2][0]).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.calls()).toHaveLength(3); + expect(fetchMock.callHistory.calls()[0]?.url).toBe("http://localhost/500"); + expect(fetchMock.callHistory.calls()[1]?.url).toBe( + "http://localhost/error", + ); + expect(fetchMock.callHistory.calls()[2]?.url).toBe("http://localhost/ok"); expect(result.current.response).toBe("body ok"); expect(result.current.error).toBe(null); @@ -342,9 +341,11 @@ describe("useFetchAny", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(2); - expect(fetchMock.calls()[0][0]).toBe("http://localhost/500"); - expect(fetchMock.calls()[1][0]).toBe("http://localhost/ok/json"); + expect(fetchMock.callHistory.calls()).toHaveLength(2); + expect(fetchMock.callHistory.calls()[0]?.url).toBe("http://localhost/500"); + expect(fetchMock.callHistory.calls()[1]?.url).toBe( + "http://localhost/ok/json", + ); expect(result.current.response).toMatchObject({ status: "ok" }); expect(result.current.error).toBe(null); diff --git a/ui/src/Hooks/useFetchDelete.test.tsx b/ui/src/Hooks/useFetchDelete.test.tsx index 15b807fe7..ee8cad047 100644 --- a/ui/src/Hooks/useFetchDelete.test.tsx +++ b/ui/src/Hooks/useFetchDelete.test.tsx @@ -3,32 +3,29 @@ import { act } from "react-dom/test-utils"; import { renderHook } from "@testing-library/react-hooks"; import { render } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { useFetchDelete } from "./useFetchDelete"; describe("useFetchDelete", () => { - beforeAll(() => { - fetchMock.mock("http://localhost/ok", "body ok"); - fetchMock.mock("http://localhost/401", 401); - fetchMock.mock("http://localhost/500", { + beforeEach(() => { + fetchMock.mockReset(); + fetchMock.route("http://localhost/ok", "body ok"); + fetchMock.route("http://localhost/401", 401); + fetchMock.route("http://localhost/500", { status: 500, body: "fake error", }); - fetchMock.mock("http://localhost/error", { + fetchMock.route("http://localhost/error", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://localhost/unknown", { - throws: "foo", + fetchMock.route("http://localhost/unknown", { + throws: new Error("foo"), }); }); - beforeEach(() => { - fetchMock.resetHistory(); - }); - afterEach(() => { - fetchMock.resetHistory(); + fetchMock.mockClear(); }); const EmptyOptions = {}; @@ -40,10 +37,10 @@ describe("useFetchDelete", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ - method: "DELETE", + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ + method: "delete", }); }); @@ -54,9 +51,9 @@ describe("useFetchDelete", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ mode: "cors", credentials: "include", redirect: "follow", @@ -139,12 +136,12 @@ describe("useFetchDelete", () => { await waitForNextUpdate(); expect(result.current.response).toBe(null); - expect(result.current.error).toBe("unknown error: foo"); + expect(result.current.error).toBe("foo"); expect(result.current.isDeleting).toBe(false); }); it("doesn't update response after cleanup", async () => { - fetchMock.mock( + fetchMock.route( "http://localhost/slow/ok", new Promise((res) => setTimeout(() => res("ok"), 1000)), ); @@ -166,11 +163,11 @@ describe("useFetchDelete", () => { const { unmount } = render(); unmount(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't update error on 500 response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/500", { + fetchMock.route("http://localhost/slow/500", { delay: 1000, status: 500, body: "error", @@ -195,11 +192,11 @@ describe("useFetchDelete", () => { unmount(); }); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't update error on failed response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/error", { + fetchMock.route("http://localhost/slow/error", { delay: 1000, throws: new TypeError("failed to fetch"), }); @@ -223,6 +220,6 @@ describe("useFetchDelete", () => { unmount(); }); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); }); diff --git a/ui/src/Hooks/useFetchGet.test.tsx b/ui/src/Hooks/useFetchGet.test.tsx index 527aa4f3f..38c93a4a7 100644 --- a/ui/src/Hooks/useFetchGet.test.tsx +++ b/ui/src/Hooks/useFetchGet.test.tsx @@ -3,7 +3,7 @@ import { act as actReact } from "react-dom/test-utils"; import { renderHook, act } from "@testing-library/react-hooks"; import { render } from "@testing-library/react"; -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { FetchRetryConfig } from "Common/Fetch"; import { useFetchGet } from "./useFetchGet"; @@ -11,27 +11,24 @@ import { useFetchGet } from "./useFetchGet"; jest.unmock("./useFetchGet"); describe("useFetchGet", () => { - beforeAll(() => { - fetchMock.mock("http://localhost/ok", { + beforeEach(() => { + jest.useFakeTimers(); + fetchMock.mockReset(); + fetchMock.route("http://localhost/ok", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "ok" }), }); - fetchMock.mock("http://localhost/401", 401); - fetchMock.mock("http://localhost/error", { + fetchMock.route("http://localhost/401", 401); + fetchMock.route("http://localhost/error", { throws: new TypeError("failed to fetch"), }); - fetchMock.mock("http://localhost/unknown", { - throws: "foo", + fetchMock.route("http://localhost/unknown", { + throws: new Error("foo"), }); }); - beforeEach(() => { - jest.useFakeTimers(); - fetchMock.resetHistory(); - }); - afterEach(() => { - fetchMock.resetHistory(); + fetchMock.mockClear(); }); it("sends a GET request", async () => { @@ -41,10 +38,10 @@ describe("useFetchGet", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ - method: "GET", + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ + method: "get", }); }); @@ -55,9 +52,9 @@ describe("useFetchGet", () => { await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); - expect(fetchMock.lastOptions()).toMatchObject({ + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({ mode: "cors", credentials: "include", redirect: "follow", @@ -69,15 +66,15 @@ describe("useFetchGet", () => { useFetchGet("http://localhost/ok", { autorun: false }), ); - expect(fetchMock.calls()).toHaveLength(0); + expect(fetchMock.callHistory.calls()).toHaveLength(0); act(() => { result.current.get(); }); await waitForNextUpdate(); - expect(fetchMock.calls()).toHaveLength(1); - expect(fetchMock.lastUrl()).toBe("http://localhost/ok"); + expect(fetchMock.callHistory.calls()).toHaveLength(1); + expect(fetchMock.callHistory.lastCall()?.url).toBe("http://localhost/ok"); }); it("will retry failed requests", async () => { @@ -121,12 +118,16 @@ describe("useFetchGet", () => { expect(result.current.isRetrying).toBe(false); expect(result.current.retryCount).toBe(FetchRetryConfig.retries + 1); - expect(fetchMock.calls()).toHaveLength(FetchRetryConfig.retries + 1); - expect(fetchMock.lastUrl()).toBe("http://localhost/error"); + expect(fetchMock.callHistory.calls()).toHaveLength( + FetchRetryConfig.retries + 1, + ); + expect(fetchMock.callHistory.lastCall()?.url).toBe( + "http://localhost/error", + ); //verify headers for each request for (let i = 0; i <= FetchRetryConfig.retries; i++) { - expect(fetchMock.calls()[i][1]).toMatchObject({ + expect(fetchMock.callHistory.calls()[i]?.options).toMatchObject({ mode: i < FetchRetryConfig.retries ? "cors" : "no-cors", credentials: "include", redirect: "follow", @@ -153,7 +154,7 @@ describe("useFetchGet", () => { }); it("error is updated after 500 response with JSON body", async () => { - fetchMock.mock("http://localhost/500/json", { + fetchMock.route("http://localhost/500/json", { status: 500, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "error" }), @@ -172,7 +173,7 @@ describe("useFetchGet", () => { }); it("error is updated after 500 response with plain body", async () => { - fetchMock.mock("http://localhost/500/text", { + fetchMock.route("http://localhost/500/text", { status: 500, body: "error", }); @@ -245,13 +246,13 @@ describe("useFetchGet", () => { } expect(result.current.response).toBe(null); - expect(result.current.error).toBe("unknown error: foo"); + expect(result.current.error).toBe("foo"); expect(result.current.isLoading).toBe(false); expect(result.current.isRetrying).toBe(false); }); it("error is updated on uparsable JSON", async () => { - fetchMock.mock("http://localhost/json/invalid", { + fetchMock.route("http://localhost/json/invalid", { headers: { "Content-Type": "application/json" }, body: "this is not a valid JSON body", }); @@ -270,14 +271,14 @@ describe("useFetchGet", () => { expect(result.current.response).toBe(null); expect(result.current.error).toBe( - "invalid json response body at http://localhost/json/invalid reason: Unexpected token 'h', \"this is not\"... is not valid JSON", + "unknown error: SyntaxError: Unexpected token 'h', \"this is not\"... is not valid JSON", ); expect(result.current.isLoading).toBe(false); expect(result.current.isRetrying).toBe(false); }); it("doesn't update response on 200 response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/ok", { + fetchMock.route("http://localhost/slow/ok", { delay: 1000, body: JSON.stringify({ status: "ok" }), }); @@ -302,12 +303,12 @@ describe("useFetchGet", () => { for (let i = 0; i <= FetchRetryConfig.retries; i++) { jest.runOnlyPendingTimers(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); } }); it("doesn't update error on 500 response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/500", { + fetchMock.route("http://localhost/slow/500", { delay: 1000, status: 500, body: JSON.stringify({ status: "error" }), @@ -333,12 +334,12 @@ describe("useFetchGet", () => { for (let i = 0; i <= FetchRetryConfig.retries; i++) { jest.runOnlyPendingTimers(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); } }); it("doesn't update error on failed response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/error", { + fetchMock.route("http://localhost/slow/error", { delay: 1000, throws: new TypeError("failed to fetch"), }); @@ -363,12 +364,12 @@ describe("useFetchGet", () => { for (let i = 0; i <= FetchRetryConfig.retries; i++) { jest.runOnlyPendingTimers(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); } }); it("doesn't update error on unparsable JSON after cleanup", async () => { - fetchMock.mock("http://localhost/slow/json/invalid", { + fetchMock.route("http://localhost/slow/json/invalid", { delay: 1000, headers: { "Content-Type": "application/json" }, body: "this is not a valid JSON body", @@ -393,11 +394,11 @@ describe("useFetchGet", () => { }); jest.runOnlyPendingTimers(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't update response after cleanup", async () => { - fetchMock.mock("http://localhost/slow/text", { + fetchMock.route("http://localhost/slow/text", { delay: 1000, body: "ok", }); @@ -421,7 +422,7 @@ describe("useFetchGet", () => { }); jest.runOnlyPendingTimers(); - await fetchMock.flush(true); + await fetchMock.callHistory.flush(true); }); it("doesn't update response after cleanup on slow body read", async () => { diff --git a/ui/src/Stores/AlertStore.test.ts b/ui/src/Stores/AlertStore.test.ts index fea4aac8b..2978c182f 100644 --- a/ui/src/Stores/AlertStore.test.ts +++ b/ui/src/Stores/AlertStore.test.ts @@ -1,4 +1,4 @@ -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; import { MockGroup } from "__fixtures__/Stories"; @@ -14,12 +14,12 @@ import { declare let global: any; beforeEach(() => { - fetchMock.reset(); + fetchMock.mockReset(); }); afterEach(() => { jest.restoreAllMocks(); - fetchMock.reset(); + fetchMock.mockReset(); }); describe("AlertStore.data", () => { @@ -530,8 +530,8 @@ describe("AlertStore.fetch", () => { it("fetch() works with valid response", async () => { const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); @@ -540,14 +540,14 @@ describe("AlertStore.fetch", () => { store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toBeUndefined(); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); expect(store.status.value).toEqual(AlertStoreStatuses.Idle); expect(store.info.version).toBe("fakeVersion"); }); it("fetch() handles response with error correctly", async () => { - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify({ error: "Fetch error" }), }); @@ -556,7 +556,7 @@ describe("AlertStore.fetch", () => { store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toBeUndefined(); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); expect(store.status.value).toEqual(AlertStoreStatuses.Failure); expect(store.info.version).toBe("unknown"); }); @@ -565,8 +565,8 @@ describe("AlertStore.fetch", () => { const consoleSpy = jest .spyOn(console, "trace") .mockImplementation(() => {}); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("fetch error"), }); @@ -575,7 +575,7 @@ describe("AlertStore.fetch", () => { store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toHaveProperty("error"); - expect(fetchMock.calls()).toHaveLength(10); + expect(fetchMock.callHistory.calls()).toHaveLength(10); expect(store.status.value).toEqual(AlertStoreStatuses.Failure); expect(store.info.version).toBe("unknown"); // there should be a trace of the error @@ -586,51 +586,51 @@ describe("AlertStore.fetch", () => { jest.spyOn(console, "trace").mockImplementation(() => {}); const store = new AlertStore([]); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("fetch error"), }); await expect( store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toHaveProperty("error"); - expect(fetchMock.calls()).toHaveLength(10); + expect(fetchMock.callHistory.calls()).toHaveLength(10); }); it("fetch() retry counter is reset after successful fetch", async () => { jest.spyOn(console, "trace").mockImplementation(() => {}); const store = new AlertStore(["label=value"]); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("fetch error"), }); await expect( store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toHaveProperty("error"); - expect(fetchMock.calls()).toHaveLength(10); + expect(fetchMock.callHistory.calls()).toHaveLength(10); const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); await expect( store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toBeUndefined(); - expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.callHistory.calls()).toHaveLength(1); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("fetch error"), }); await expect( store.fetch("", false, "", "", false, {}, 5, {}), ).resolves.toHaveProperty("error"); - expect(fetchMock.calls()).toHaveLength(10); + expect(fetchMock.callHistory.calls()).toHaveLength(10); }); it("fetch() reloads the page after if auth middleware is detected", async () => { @@ -659,8 +659,8 @@ describe("AlertStore.fetch", () => { store.filters.setFilterValues([NewUnappliedFilter("foo")]); jest.spyOn(console, "trace").mockImplementation(() => {}); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { throws: new Error("fetch error"), }); @@ -672,8 +672,8 @@ describe("AlertStore.fetch", () => { it("stored settings are updated if needed after fetch", async () => { const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); @@ -717,8 +717,8 @@ describe("AlertStore.fetch", () => { it("wants to reload page after new version is returned in the API", async () => { const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); const store = new AlertStore(["label=value"]); @@ -728,8 +728,8 @@ describe("AlertStore.fetch", () => { expect(store.info.upgradeReady).toBe(false); response.version = "newFakeVersion"; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); await expect( @@ -793,17 +793,19 @@ describe("AlertStore.fetch", () => { it("uses correct query args with gridSortReverse=false", async () => { const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); const store = new AlertStore(["label=value"]); await expect( store.fetch("", false, "sortOrder", "sortLabel", false, {}, 5, {}), ).resolves.toBeUndefined(); - expect(fetchMock.calls().length).toEqual(1); - expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); - expect(JSON.parse(fetchMock.calls()[0][1]?.body as string)).toStrictEqual({ + expect(fetchMock.callHistory.calls().length).toEqual(1); + expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json"); + expect( + JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string), + ).toStrictEqual({ filters: ["label=value"], gridLabel: "", gridLimits: {}, @@ -818,17 +820,19 @@ describe("AlertStore.fetch", () => { it("uses correct query args with gridSortReverse=true", async () => { const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); const store = new AlertStore(["label=value"]); await expect( store.fetch("cluster", true, "sortOrder", "sortLabel", true, {}, 5, {}), ).resolves.toBeUndefined(); - expect(fetchMock.calls().length).toEqual(1); - expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); - expect(JSON.parse(fetchMock.calls()[0][1]?.body as string)).toStrictEqual({ + expect(fetchMock.callHistory.calls().length).toEqual(1); + expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json"); + expect( + JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string), + ).toStrictEqual({ filters: ["label=value"], gridLabel: "cluster", gridLimits: {}, @@ -843,8 +847,8 @@ describe("AlertStore.fetch", () => { it("uses correct query args with limits", async () => { const response = EmptyAPIResponse(); - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); const store = new AlertStore(["label=value"]); @@ -866,9 +870,11 @@ describe("AlertStore.fetch", () => { { "1234567890": 7, "1234567891": 1 }, ), ).resolves.toBeUndefined(); - expect(fetchMock.calls().length).toEqual(1); - expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); - expect(JSON.parse(fetchMock.calls()[0][1]?.body as string)).toStrictEqual({ + expect(fetchMock.callHistory.calls().length).toEqual(1); + expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json"); + expect( + JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string), + ).toStrictEqual({ filters: ["label=value"], gridLabel: "cluster", gridLimits: { bar: 7 }, @@ -904,8 +910,8 @@ describe("AlertStore.fetch", () => { stateCount: { unprocessed: 0, active: 3, suppressed: 0 }, }, ]; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); @@ -923,9 +929,11 @@ describe("AlertStore.fetch", () => { { g1: 7, g2: 1, g4: 6 }, ), ).resolves.toBeUndefined(); - expect(fetchMock.calls().length).toEqual(1); - expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); - expect(JSON.parse(fetchMock.calls()[0][1]?.body as string)).toStrictEqual({ + expect(fetchMock.callHistory.calls().length).toEqual(1); + expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json"); + expect( + JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string), + ).toStrictEqual({ filters: ["label=value"], gridLabel: "cluster", gridLimits: { bar: 7 }, diff --git a/ui/src/index.test.tsx b/ui/src/index.test.tsx index 3463953d1..3ef352a27 100644 --- a/ui/src/index.test.tsx +++ b/ui/src/index.test.tsx @@ -1,4 +1,4 @@ -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; import { DefaultsBase64 } from "__fixtures__/Defaults"; @@ -34,8 +34,8 @@ it("renders without crashing with missing defaults div", () => { const response = EmptyAPIResponse(); response.filters = []; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); @@ -62,8 +62,8 @@ it("renders without crashing with defaults present", () => { const response = EmptyAPIResponse(); response.filters = []; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); diff --git a/ui/src/polyfill-load.test.tsx b/ui/src/polyfill-load.test.tsx index 60bd8a34f..7ac47d83c 100644 --- a/ui/src/polyfill-load.test.tsx +++ b/ui/src/polyfill-load.test.tsx @@ -1,4 +1,4 @@ -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; import { mockMatchMedia } from "__fixtures__/matchMedia"; @@ -37,8 +37,8 @@ it("loads ResizeObserver polyfill if needed", () => { const response = EmptyAPIResponse(); response.filters = []; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); diff --git a/ui/src/polyfill-noop.test.tsx b/ui/src/polyfill-noop.test.tsx index a8923b45c..2223f2506 100644 --- a/ui/src/polyfill-noop.test.tsx +++ b/ui/src/polyfill-noop.test.tsx @@ -1,4 +1,4 @@ -import fetchMock from "fetch-mock"; +import fetchMock from "@fetch-mock/jest"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; import { mockMatchMedia } from "__fixtures__/matchMedia"; @@ -46,8 +46,8 @@ it("doesn't load ResizeObserver polyfill if not needed", () => { const response = EmptyAPIResponse(); response.filters = []; - fetchMock.reset(); - fetchMock.mock("*", { + fetchMock.mockReset(); + fetchMock.route("*", { body: JSON.stringify(response), }); diff --git a/ui/src/setupTests.ts b/ui/src/setupTests.ts index 6827ad555..e32df6296 100644 --- a/ui/src/setupTests.ts +++ b/ui/src/setupTests.ts @@ -2,6 +2,8 @@ import React from "react"; import "@testing-library/jest-dom"; +import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/jest"; + import { useInView } from "react-intersection-observer"; import { createMocks as createIdleTimerMocks } from "react-idle-timer"; @@ -15,6 +17,10 @@ import { useFetchGet } from "Hooks/useFetchGet"; createIdleTimerMocks(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +manageFetchMockGlobally(jest as any); +fetchMock.mockGlobal(); + configure({ enforceActions: "always", //computedRequiresReaction: true, diff --git a/ui/src/testEnvironment.ts b/ui/src/testEnvironment.ts new file mode 100644 index 000000000..f92c472c2 --- /dev/null +++ b/ui/src/testEnvironment.ts @@ -0,0 +1,11 @@ +import { TestEnvironment } from "jest-environment-jsdom"; + +export default class CustomTestEnvironment extends TestEnvironment { + async setup() { + await super.setup(); + this.global.Request = Request; + this.global.Response = Response; + this.global.ReadableStream = ReadableStream; + this.global.fetch = fetch; + } +}