Browse Source

[web] Update `fetch-mock` to version 12 (#24837)

* Update fetch-mock to version 12

* Replace `fetchMock.done` by `fetchMock.callHistory.done`

* Replace `…Mock.called` by `…Mock.callHistory.called`

* Replace `fetchMock.reset` by `fetchMock.hardReset`

* Replace `fetchMock.restore` by `fetchMock.hardReset`

* Replace `fetchMock.resetHistory` by `fetchMock.clearHistory`

* Replace `fetchMock.calls` by `fetchMock.callHistory.calls`

* Replace `fetchMock.flush` by `fetchMock.callHistory.flush`

* Update tests for fetch-mock version 12

See https://www.wheresrhys.co.uk/fetch-mock/docs/Usage/upgrade-guide

* Update stories for fetch-mock version 12

* Remove `overwriteRoutes` option

* Add `fetchMock.spyGlobal()` to storybook

* Remove deprecated `sendAsJson` param

* Replace `fetchMock.hardReset()` by `fetchMock.removeRoutes().clearHistory()`

* Fixup fetch-mock in storybook:

Call `mockGlobal` inside the hook, call `removeRoutes` and `unmockGlobal` on cleanup

Behaviour can be tested by navigating between

https://storybook.dev-overleaf.com/main/?path=/story/editor-ai-error-assistant-compile-log-entries--first-log-entry
https://storybook.dev-overleaf.com/main/?path=/story/editor-ai-error-assistant-compile-log-entries--rate-limited

https://storybook.dev-overleaf.com/main/?path=/story/project-list-notifications--project-invite
https://storybook.dev-overleaf.com/main/?path=/story/project-list-notifications--project-invite-network-error

And clicking the buttons

GitOrigin-RevId: 35611b4430259e4c21c3d819ad18b2e6dab66242
Antoine Clausse 1 year ago
parent
commit
b901bb6c75
99 changed files with 727 additions and 728 deletions
  1. 34 78
      package-lock.json
  2. 6 10
      services/web/frontend/stories/file-view/file-view.stories.jsx
  3. 46 55
      services/web/frontend/stories/fixtures/compile.js
  4. 5 4
      services/web/frontend/stories/hooks/use-fetch-mock.tsx
  5. 2 6
      services/web/frontend/stories/modals/create-file/create-file-modal.stories.jsx
  6. 1 5
      services/web/frontend/stories/notification.stories.tsx
  7. 13 17
      services/web/frontend/stories/project-list/helpers/emails.ts
  8. 3 3
      services/web/frontend/stories/project-list/system-messages.stories.tsx
  9. 0 2
      services/web/frontend/stories/settings/helpers/emails.js
  10. 3 3
      services/web/modules/user-activate/test/frontend/js/components/register-form.test.jsx
  11. 7 7
      services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.jsx
  12. 1 1
      services/web/package.json
  13. 6 0
      services/web/test/frontend/bootstrap.js
  14. 3 3
      services/web/test/frontend/features/chat/components/chat-pane.test.jsx
  15. 50 56
      services/web/test/frontend/features/chat/context/chat-context.test.jsx
  16. 15 11
      services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.jsx
  17. 1 1
      services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx
  18. 1 1
      services/web/test/frontend/features/editor-left-menu/components/actions-menu.test.jsx
  19. 7 3
      services/web/test/frontend/features/editor-left-menu/components/actions-word-count.test.jsx
  20. 1 1
      services/web/test/frontend/features/editor-left-menu/components/download-menu.test.jsx
  21. 1 1
      services/web/test/frontend/features/editor-left-menu/components/help-contact-us.test.jsx
  22. 1 1
      services/web/test/frontend/features/editor-left-menu/components/help-menu.test.jsx
  23. 1 1
      services/web/test/frontend/features/editor-left-menu/components/help-show-hotkeys.test.jsx
  24. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-auto-close-brackets.test.tsx
  25. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-auto-complete.test.tsx
  26. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-compiler.test.tsx
  27. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-document.test.tsx
  28. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-editor-theme.test.tsx
  29. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-font-family.test.tsx
  30. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-font-size.test.tsx
  31. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-image-name.test.tsx
  32. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-keybindings.test.tsx
  33. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-line-height.test.tsx
  34. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx
  35. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-overall-theme.test.tsx
  36. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-pdf-viewer.test.tsx
  37. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-spell-check-language.test.tsx
  38. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-syntax-validation.test.tsx
  39. 1 1
      services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.jsx
  40. 1 1
      services/web/test/frontend/features/file-view/components/file-view-header.test.jsx
  41. 1 1
      services/web/test/frontend/features/file-view/components/file-view-refresh-button.test.jsx
  42. 1 1
      services/web/test/frontend/features/file-view/components/file-view-text.test.jsx
  43. 1 1
      services/web/test/frontend/features/file-view/components/file-view.test.jsx
  44. 1 1
      services/web/test/frontend/features/group-management/components/members-table/unlink-user-modal.test.tsx
  45. 1 1
      services/web/test/frontend/features/project-list/components/load-more.test.tsx
  46. 1 1
      services/web/test/frontend/features/project-list/components/new-project-button.test.tsx
  47. 5 5
      services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
  48. 55 54
      services/web/test/frontend/features/project-list/components/notifications.test.tsx
  49. 85 60
      services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
  50. 2 2
      services/web/test/frontend/features/project-list/components/project-search.test.tsx
  51. 10 10
      services/web/test/frontend/features/project-list/components/sidebar/add-affiliation.test.tsx
  52. 17 9
      services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx
  53. 5 5
      services/web/test/frontend/features/project-list/components/system-messages.test.tsx
  54. 5 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/archive-project-button.test.tsx
  55. 1 1
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx
  56. 5 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/copy-project-button.test.tsx
  57. 2 1
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/delete-project-button.test.tsx
  58. 3 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/leave-project-button.test.tsx
  59. 3 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/rename-project-button.test.tsx
  60. 3 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/trash-project-button.test.tsx
  61. 5 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/unarchive-project-button.test.tsx
  62. 3 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/untrash-project-button.test.tsx
  63. 6 3
      services/web/test/frontend/features/project-list/components/table/cells/inline-tags.test.tsx
  64. 7 7
      services/web/test/frontend/features/project-list/components/table/project-list-table.test.tsx
  65. 1 1
      services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx
  66. 4 2
      services/web/test/frontend/features/project-list/components/table/project-tools/rename-project-modal.test.tsx
  67. 1 1
      services/web/test/frontend/features/project-list/helpers/render-with-context.tsx
  68. 27 27
      services/web/test/frontend/features/settings/components/account-info-section.test.tsx
  69. 20 13
      services/web/test/frontend/features/settings/components/emails/add-email-input.test.tsx
  70. 5 5
      services/web/test/frontend/features/settings/components/emails/emails-row.test.tsx
  71. 4 4
      services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx
  72. 31 27
      services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx
  73. 11 13
      services/web/test/frontend/features/settings/components/emails/emails-section-institution-and-role.test.tsx
  74. 2 2
      services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx
  75. 4 4
      services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx
  76. 1 1
      services/web/test/frontend/features/settings/components/leave/modal-content.test.tsx
  77. 6 6
      services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx
  78. 1 1
      services/web/test/frontend/features/settings/components/leave/modal.test.tsx
  79. 1 1
      services/web/test/frontend/features/settings/components/leavers-survey-alert.test.tsx
  80. 1 1
      services/web/test/frontend/features/settings/components/linking-section.test.tsx
  81. 10 10
      services/web/test/frontend/features/settings/components/password-section.test.tsx
  82. 1 1
      services/web/test/frontend/features/settings/components/security-section.test.tsx
  83. 3 3
      services/web/test/frontend/features/settings/context/sso-context.test.tsx
  84. 24 20
      services/web/test/frontend/features/settings/context/user-email-context.test.tsx
  85. 52 49
      services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
  86. 2 2
      services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx
  87. 10 7
      services/web/test/frontend/features/subscription/components/dashboard/managed-institutions.test.tsx
  88. 1 1
      services/web/test/frontend/features/subscription/components/dashboard/managed-publishers.test.tsx
  89. 1 1
      services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx
  90. 4 4
      services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
  91. 1 1
      services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx
  92. 1 1
      services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
  93. 1 1
      services/web/test/frontend/features/subscription/components/group-invite/has-individual-recurly-subscription.test.tsx
  94. 1 1
      services/web/test/frontend/features/subscription/components/group-invite/join-group.test.tsx
  95. 1 1
      services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx
  96. 1 1
      services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.jsx
  97. 3 14
      services/web/test/frontend/infrastructure/fetch-json.test.js
  98. 33 33
      services/web/test/frontend/infrastructure/project-snapshot.test.ts
  99. 4 4
      services/web/test/frontend/shared/hooks/use-abort-controller.test.tsx

+ 34 - 78
package-lock.json

@@ -12218,6 +12218,13 @@
         "@types/node": "*"
         "@types/node": "*"
       }
       }
     },
     },
+    "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/hapi__catbox": {
     "node_modules/@types/hapi__catbox": {
       "version": "10.2.4",
       "version": "10.2.4",
       "resolved": "https://registry.npmjs.org/@types/hapi__catbox/-/hapi__catbox-10.2.4.tgz",
       "resolved": "https://registry.npmjs.org/@types/hapi__catbox/-/hapi__catbox-10.2.4.tgz",
@@ -22764,45 +22771,6 @@
         "node": ">=4.0.0"
         "node": ">=4.0.0"
       }
       }
     },
     },
-    "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==",
-      "dev": true,
-      "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"
-      },
-      "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_modules/fetch-mock/node_modules/path-to-regexp": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
-      "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
-      "dev": true
-    },
     "node_modules/figures": {
     "node_modules/figures": {
       "version": "3.2.0",
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
       "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -26431,12 +26399,6 @@
         "url": "https://github.com/sponsors/ljharb"
         "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": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
-      "dev": true
-    },
     "node_modules/is-symbol": {
     "node_modules/is-symbol": {
       "version": "1.0.4",
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
@@ -28433,12 +28395,6 @@
       "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
       "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
       "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
       "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
     },
     },
-    "node_modules/lodash.sortby": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
-      "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
-      "dev": true
-    },
     "node_modules/lodash.support": {
     "node_modules/lodash.support": {
       "version": "2.4.1",
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.4.1.tgz",
       "resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.4.1.tgz",
@@ -35391,6 +35347,16 @@
         "url": "https://github.com/sponsors/ljharb"
         "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/regexpp": {
     "node_modules/regexpp": {
       "version": "3.2.0",
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
       "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -39423,15 +39389,6 @@
         "node": ">= 4.0.0"
         "node": ">= 4.0.0"
       }
       }
     },
     },
-    "node_modules/tr46": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
-      "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
-      "dev": true,
-      "dependencies": {
-        "punycode": "^2.1.0"
-      }
-    },
     "node_modules/traverse": {
     "node_modules/traverse": {
       "version": "0.6.9",
       "version": "0.6.9",
       "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.9.tgz",
       "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.9.tgz",
@@ -41219,23 +41176,6 @@
         "node": ">=12"
         "node": ">=12"
       }
       }
     },
     },
-    "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,
-      "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
-    },
     "node_modules/which": {
     "node_modules/which": {
       "version": "2.0.2",
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -44521,7 +44461,7 @@
         "esmock": "^2.6.7",
         "esmock": "^2.6.7",
         "events": "^3.3.0",
         "events": "^3.3.0",
         "fake-indexeddb": "^6.0.0",
         "fake-indexeddb": "^6.0.0",
-        "fetch-mock": "^9.10.2",
+        "fetch-mock": "^12.5.2",
         "formik": "^2.2.9",
         "formik": "^2.2.9",
         "fuse.js": "^3.0.0",
         "fuse.js": "^3.0.0",
         "glob": "^7.1.6",
         "glob": "^7.1.6",
@@ -45294,6 +45234,22 @@
       "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
       "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
       "dev": true
       "dev": true
     },
     },
+    "services/web/node_modules/fetch-mock": {
+      "version": "12.5.2",
+      "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.5.2.tgz",
+      "integrity": "sha512-b5KGDFmdmado2MPQjZl6ix3dAG3iwCitb0XQwN72y2s9VnWZ3ObaGNy+bkpm1390foiLDybdJ7yjRGKD36kATw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/glob-to-regexp": "^0.4.4",
+        "dequal": "^2.0.3",
+        "glob-to-regexp": "^0.4.1",
+        "regexparam": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=18.11.0"
+      }
+    },
     "services/web/node_modules/google-auth-library": {
     "services/web/node_modules/google-auth-library": {
       "version": "8.7.0",
       "version": "8.7.0",
       "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz",
       "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz",

+ 6 - 10
services/web/frontend/stories/file-view/file-view.stories.jsx

@@ -158,11 +158,9 @@ ImageFile.args = {
 
 
 export const TextFile = args => {
 export const TextFile = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/blob/:hash',
-      { body: bodies.text },
-      { overwriteRoutes: true }
-    )
+    setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
+      body: bodies.text,
+    })
   )
   )
   return <FileView {...args} />
   return <FileView {...args} />
 }
 }
@@ -180,11 +178,9 @@ TextFile.args = {
 
 
 export const UploadedFile = args => {
 export const UploadedFile = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).head(
-      'express:/project/:project_id/blob/:hash',
-      { status: 500 },
-      { overwriteRoutes: true }
-    )
+    setupFetchMock(fetchMock).head('express:/project/:project_id/blob/:hash', {
+      status: 500,
+    })
   )
   )
   return <FileView {...args} />
   return <FileView {...args} />
 }
 }

+ 46 - 55
services/web/frontend/stories/fixtures/compile.js

@@ -58,7 +58,7 @@ export const mockCompile = (fetchMock, delay = 1000) =>
         outputFiles: cloneDeep(outputFiles),
         outputFiles: cloneDeep(outputFiles),
       },
       },
     },
     },
-    { delay, overwriteRoutes: true }
+    { delay }
   )
   )
 
 
 export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
 export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
@@ -91,27 +91,24 @@ export const mockCompileValidationIssues = (
         },
         },
       }
       }
     },
     },
-    { delay, overwriteRoutes: true }
+    { delay }
   )
   )
 
 
 export const mockClearCache = fetchMock =>
 export const mockClearCache = fetchMock =>
   fetchMock.delete('express:/project/:projectId/output', 204, {
   fetchMock.delete('express:/project/:projectId/output', 204, {
     delay: 1000,
     delay: 1000,
-    overwriteRoutes: true,
   })
   })
 
 
 export const mockBuildFile = fetchMock =>
 export const mockBuildFile = fetchMock =>
-  fetchMock.get(
-    'express:/build/:file',
-    (url, options, request) => {
-      const { pathname } = new URL(url, 'https://example.com')
+  fetchMock.get('express:/build/:file', (url, options, request) => {
+    const { pathname } = new URL(url, 'https://example.com')
 
 
-      switch (pathname) {
-        case '/build/output.blg':
-          return 'This is BibTeX, Version 4.0' // FIXME
+    switch (pathname) {
+      case '/build/output.blg':
+        return 'This is BibTeX, Version 4.0' // FIXME
 
 
-        case '/build/output.log':
-          return `
+      case '/build/output.log':
+        return `
 The LaTeX compiler output
 The LaTeX compiler output
   * With a lot of details
   * With a lot of details
 
 
@@ -134,31 +131,29 @@ LaTeX Font Info:    External font \`cmex10' loaded for size
 
 
 `
 `
 
 
-        case '/build/output.pdf':
-          return new Promise(resolve => {
-            const xhr = new XMLHttpRequest()
-            xhr.addEventListener('load', () => {
-              resolve({
-                status: 200,
-                headers: {
-                  'Content-Length': xhr.getResponseHeader('Content-Length'),
-                  'Content-Type': xhr.getResponseHeader('Content-Type'),
-                },
-                body: xhr.response,
-              })
+      case '/build/output.pdf':
+        return new Promise(resolve => {
+          const xhr = new XMLHttpRequest()
+          xhr.addEventListener('load', () => {
+            resolve({
+              status: 200,
+              headers: {
+                'Content-Length': xhr.getResponseHeader('Content-Length'),
+                'Content-Type': xhr.getResponseHeader('Content-Type'),
+              },
+              body: xhr.response,
             })
             })
-            xhr.open('GET', examplePdf)
-            xhr.responseType = 'arraybuffer'
-            xhr.send()
           })
           })
+          xhr.open('GET', examplePdf)
+          xhr.responseType = 'arraybuffer'
+          xhr.send()
+        })
 
 
-        default:
-          console.log(pathname)
-          return 404
-      }
-    },
-    { sendAsJson: false, overwriteRoutes: true }
-  )
+      default:
+        console.log(pathname)
+        return 404
+    }
+  })
 
 
 const mockHighlights = [
 const mockHighlights = [
   {
   {
@@ -195,29 +190,25 @@ export const mockEventTracking = fetchMock =>
   fetchMock.get('express:/event/:event', 204)
   fetchMock.get('express:/event/:event', 204)
 
 
 export const mockValidPdf = fetchMock =>
 export const mockValidPdf = fetchMock =>
-  fetchMock.get(
-    'express:/build/output.pdf',
-    (url, options, request) => {
-      return new Promise(resolve => {
-        const xhr = new XMLHttpRequest()
-        xhr.addEventListener('load', () => {
-          resolve({
-            status: 200,
-            headers: {
-              'Content-Length': xhr.getResponseHeader('Content-Length'),
-              'Content-Type': xhr.getResponseHeader('Content-Type'),
-              'Accept-Ranges': 'bytes',
-            },
-            body: xhr.response,
-          })
+  fetchMock.get('express:/build/output.pdf', (url, options, request) => {
+    return new Promise(resolve => {
+      const xhr = new XMLHttpRequest()
+      xhr.addEventListener('load', () => {
+        resolve({
+          status: 200,
+          headers: {
+            'Content-Length': xhr.getResponseHeader('Content-Length'),
+            'Content-Type': xhr.getResponseHeader('Content-Type'),
+            'Accept-Ranges': 'bytes',
+          },
+          body: xhr.response,
         })
         })
-        xhr.open('GET', examplePdf)
-        xhr.responseType = 'arraybuffer'
-        xhr.send()
       })
       })
-    },
-    { sendAsJson: false, overwriteRoutes: true }
-  )
+      xhr.open('GET', examplePdf)
+      xhr.responseType = 'arraybuffer'
+      xhr.send()
+    })
+  })
 
 
 export const mockSynctex = fetchMock =>
 export const mockSynctex = fetchMock =>
   fetchMock
   fetchMock

+ 5 - 4
services/web/frontend/stories/hooks/use-fetch-mock.tsx

@@ -1,18 +1,19 @@
 import { useLayoutEffect } from 'react'
 import { useLayoutEffect } from 'react'
 import fetchMock from 'fetch-mock'
 import fetchMock from 'fetch-mock'
-fetchMock.config.fallbackToNetwork = true
 
 
 /**
 /**
- * Run callback to mock fetch routes, call restore() when unmounted
+ * Run callback to mock fetch routes, call removeRoutes() and unmockGlobal() when unmounted
  */
  */
 export default function useFetchMock(
 export default function useFetchMock(
   callback: (value: typeof fetchMock) => void
   callback: (value: typeof fetchMock) => void
 ) {
 ) {
+  fetchMock.mockGlobal()
+
   useLayoutEffect(() => {
   useLayoutEffect(() => {
     callback(fetchMock)
     callback(fetchMock)
-
     return () => {
     return () => {
-      fetchMock.restore()
+      fetchMock.removeRoutes()
+      fetchMock.unmockGlobal()
     }
     }
   }, [callback])
   }, [callback])
 }
 }

+ 2 - 6
services/web/frontend/stories/modals/create-file/create-file-modal.stories.jsx

@@ -37,9 +37,7 @@ export const ErrorImportingFileFromExternalURL = args => {
   useFetchMock(fetchMock => {
   useFetchMock(fetchMock => {
     mockCreateFileModalFetch(fetchMock)
     mockCreateFileModalFetch(fetchMock)
 
 
-    fetchMock.post('express:/project/:projectId/linked_file', 500, {
-      overwriteRoutes: true,
-    })
+    fetchMock.post('express:/project/:projectId/linked_file', 500)
   })
   })
 
 
   getMeta('ol-ExposedSettings').hasLinkUrlFeature = true
   getMeta('ol-ExposedSettings').hasLinkUrlFeature = true
@@ -52,9 +50,7 @@ export const ErrorImportingFileFromReferenceProvider = args => {
   useFetchMock(fetchMock => {
   useFetchMock(fetchMock => {
     mockCreateFileModalFetch(fetchMock)
     mockCreateFileModalFetch(fetchMock)
 
 
-    fetchMock.post('express:/project/:projectId/linked_file', 500, {
-      overwriteRoutes: true,
-    })
+    fetchMock.post('express:/project/:projectId/linked_file', 500)
   })
   })
 
 
   return <FileTreeModalCreateFile {...args} />
   return <FileTreeModalCreateFile {...args} />

+ 1 - 5
services/web/frontend/stories/notification.stories.tsx

@@ -315,11 +315,7 @@ export const OverlayedWithCustomClass = (args: Args) => {
 
 
 export const SuccessFlow = (args: Args) => {
 export const SuccessFlow = (args: Args) => {
   console.log('.....render')
   console.log('.....render')
-  fetchMock.post(
-    'express:/test-success',
-    { status: 200 },
-    { delay: 250, overwriteRoutes: true }
-  )
+  fetchMock.post('express:/test-success', { status: 200 }, { delay: 250 })
 
 
   const { isLoading, isSuccess, runAsync } = useAsync()
   const { isLoading, isSuccess, runAsync } = useAsync()
   function handleClick() {
   function handleClick() {

+ 13 - 17
services/web/frontend/stories/project-list/helpers/emails.ts

@@ -1,5 +1,5 @@
 import { merge, cloneDeep } from 'lodash'
 import { merge, cloneDeep } from 'lodash'
-import { FetchMockStatic } from 'fetch-mock'
+import { type FetchMock } from 'fetch-mock'
 import { UserEmailData } from '../../../../types/user-email'
 import { UserEmailData } from '../../../../types/user-email'
 import {
 import {
   Institution,
   Institution,
@@ -32,7 +32,7 @@ export const fakeReconfirmationUsersData = {
   default: false,
   default: false,
 } as DeepReadonly<UserEmailData>
 } as DeepReadonly<UserEmailData>
 
 
-export function defaultSetupMocks(fetchMock: FetchMockStatic) {
+export function defaultSetupMocks(fetchMock: FetchMock) {
   // at least one project is required to show some notifications
   // at least one project is required to show some notifications
   const projects = [{}] as Project[]
   const projects = [{}] as Project[]
   fetchMock.post(/\/api\/project/, {
   fetchMock.post(/\/api\/project/, {
@@ -54,7 +54,7 @@ export function setDefaultMeta() {
   window.metaAttributesCache.set('ol-userEmails', [])
   window.metaAttributesCache.set('ol-userEmails', [])
 }
 }
 
 
-export function errorsMocks(fetchMock: FetchMockStatic) {
+export function errorsMocks(fetchMock: FetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
   fetchMock.post(/\/user\/emails\/*/, 500, { delay: MOCK_DELAY })
   fetchMock.post(/\/user\/emails\/*/, 500, { delay: MOCK_DELAY })
   fetchMock.post(
   fetchMock.post(
@@ -74,7 +74,7 @@ export function setInstitutionMeta(institutionData: Partial<Institution>) {
   ])
   ])
 }
 }
 
 
-export function institutionSetupMocks(fetchMock: FetchMockStatic) {
+export function institutionSetupMocks(fetchMock: FetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
   fetchMock.delete(/\/notifications\/*/, 200, { delay: MOCK_DELAY })
   fetchMock.delete(/\/notifications\/*/, 200, { delay: MOCK_DELAY })
 }
 }
@@ -84,7 +84,7 @@ export function setCommonMeta(notificationData: DeepPartial<Notification>) {
   window.metaAttributesCache.set('ol-notifications', [notificationData])
   window.metaAttributesCache.set('ol-notifications', [notificationData])
 }
 }
 
 
-export function commonSetupMocks(fetchMock: FetchMockStatic) {
+export function commonSetupMocks(fetchMock: FetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
   fetchMock.post(
   fetchMock.post(
     /\/project\/[A-Za-z0-9]+\/invite\/token\/[A-Za-z0-9]+\/accept/,
     /\/project\/[A-Za-z0-9]+\/invite\/token\/[A-Za-z0-9]+\/accept/,
@@ -98,7 +98,7 @@ export function setReconfirmationMeta() {
   window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData])
   window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData])
 }
 }
 
 
-export function reconfirmationSetupMocks(fetchMock: FetchMockStatic) {
+export function reconfirmationSetupMocks(fetchMock: FetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
   fetchMock.post(/\/user\/emails\/resend_confirmation/, 200, {
   fetchMock.post(/\/user\/emails\/resend_confirmation/, 200, {
     delay: MOCK_DELAY,
     delay: MOCK_DELAY,
@@ -113,19 +113,15 @@ export function setReconfirmAffiliationMeta() {
   )
   )
 }
 }
 
 
-export function reconfirmAffiliationSetupMocks(fetchMock: FetchMockStatic) {
+export function reconfirmAffiliationSetupMocks(fetchMock: FetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
-  fetchMock.post(
-    /\/api\/project/,
-    {
-      status: 200,
-      body: {
-        projects: [{}],
-        totalSize: 0,
-      },
+  fetchMock.post(/\/api\/project/, {
+    status: 200,
+    body: {
+      projects: [{}],
+      totalSize: 0,
     },
     },
-    { overwriteRoutes: true }
-  )
+  })
   fetchMock.post(/\/user\/emails\/send-reconfirmation/, 200, {
   fetchMock.post(/\/user\/emails\/send-reconfirmation/, 200, {
     delay: MOCK_DELAY,
     delay: MOCK_DELAY,
   })
   })

+ 3 - 3
services/web/frontend/stories/project-list/system-messages.stories.tsx

@@ -1,9 +1,9 @@
 import SystemMessages from '@/shared/components/system-messages'
 import SystemMessages from '@/shared/components/system-messages'
 import useFetchMock from '../hooks/use-fetch-mock'
 import useFetchMock from '../hooks/use-fetch-mock'
-import { FetchMockStatic } from 'fetch-mock'
+import { type FetchMock } from 'fetch-mock'
 
 
 export const SystemMessage = (args: any) => {
 export const SystemMessage = (args: any) => {
-  useFetchMock((fetchMock: FetchMockStatic) => {
+  useFetchMock((fetchMock: FetchMock) => {
     fetchMock.get(/\/system\/messages/, [
     fetchMock.get(/\/system\/messages/, [
       {
       {
         _id: 1,
         _id: 1,
@@ -23,7 +23,7 @@ export const SystemMessage = (args: any) => {
 }
 }
 
 
 export const TranslationMessage = (args: any) => {
 export const TranslationMessage = (args: any) => {
-  useFetchMock((fetchMock: FetchMockStatic) => {
+  useFetchMock((fetchMock: FetchMock) => {
     fetchMock.get(/\/system\/messages/, [])
     fetchMock.get(/\/system\/messages/, [])
   })
   })
 
 

+ 0 - 2
services/web/frontend/stories/settings/helpers/emails.js

@@ -174,7 +174,6 @@ export function reconfirmationSetupMocks(fetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
   fetchMock.get(/\/user\/emails/, fakeReconfirmationUsersData, {
   fetchMock.get(/\/user\/emails/, fakeReconfirmationUsersData, {
     delay: MOCK_DELAY,
     delay: MOCK_DELAY,
-    overwriteRoutes: true,
   })
   })
 }
 }
 
 
@@ -186,7 +185,6 @@ export function emailLimitSetupMocks(fetchMock) {
   defaultSetupMocks(fetchMock)
   defaultSetupMocks(fetchMock)
   fetchMock.get(/\/user\/emails/, userData, {
   fetchMock.get(/\/user\/emails/, userData, {
     delay: MOCK_DELAY,
     delay: MOCK_DELAY,
-    overwriteRoutes: true,
   })
   })
 }
 }
 
 

+ 3 - 3
services/web/modules/user-activate/test/frontend/js/components/register-form.test.jsx

@@ -6,10 +6,10 @@ import RegisterForm from '../../../../frontend/js/components/register-form'
 
 
 describe('RegisterForm', function () {
 describe('RegisterForm', function () {
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
   it('should render the register form', async function () {
   it('should render the register form', async function () {
     const setRegistrationSuccessStub = sinon.stub()
     const setRegistrationSuccessStub = sinon.stub()
@@ -57,6 +57,6 @@ describe('RegisterForm', function () {
     const registerButton = screen.getByRole('button', { name: /register/i })
     const registerButton = screen.getByRole('button', { name: /register/i })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.click(registerButton)
     fireEvent.click(registerButton)
-    expect(registerMock.called()).to.be.true
+    expect(registerMock.callHistory.called()).to.be.true
   })
   })
 })
 })

+ 7 - 7
services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.jsx

@@ -5,10 +5,10 @@ import UserActivateRegister from '../../../../frontend/js/components/user-activa
 
 
 describe('UserActivateRegister', function () {
 describe('UserActivateRegister', function () {
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
   it('should display the error message', async function () {
   it('should display the error message', async function () {
     const email = 'abc@gmail.com'
     const email = 'abc@gmail.com'
@@ -23,7 +23,7 @@ describe('UserActivateRegister', function () {
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.click(registerButton)
     fireEvent.click(registerButton)
 
 
-    expect(registerMock.called()).to.be.true
+    expect(registerMock.callHistory.called()).to.be.true
     await screen.findByText('Sorry, an error occured', { exact: false })
     await screen.findByText('Sorry, an error occured', { exact: false })
   })
   })
 
 
@@ -44,7 +44,7 @@ describe('UserActivateRegister', function () {
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.click(registerButton)
     fireEvent.click(registerButton)
 
 
-    expect(registerMock.called()).to.be.true
+    expect(registerMock.callHistory.called()).to.be.true
     await screen.findByText(
     await screen.findByText(
       "We've sent out welcome emails to the registered users."
       "We've sent out welcome emails to the registered users."
     )
     )
@@ -78,7 +78,7 @@ describe('UserActivateRegister', function () {
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.click(registerButton)
     fireEvent.click(registerButton)
 
 
-    expect(registerMock.called()).to.be.true
+    expect(registerMock.callHistory.called()).to.be.true
     await screen.findByText('abc@gmail.com')
     await screen.findByText('abc@gmail.com')
     await screen.findByText('def@gmail.com')
     await screen.findByText('def@gmail.com')
   })
   })
@@ -103,7 +103,7 @@ describe('UserActivateRegister', function () {
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.click(registerButton)
     fireEvent.click(registerButton)
 
 
-    expect(registerMock.called()).to.be.true
+    expect(registerMock.callHistory.called()).to.be.true
     await screen.findByText('abc@')
     await screen.findByText('abc@')
     await screen.findByText('def@')
     await screen.findByText('def@')
   })
   })
@@ -133,7 +133,7 @@ describe('UserActivateRegister', function () {
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.change(registerInput, { target: { value: email } })
     fireEvent.click(registerButton)
     fireEvent.click(registerButton)
 
 
-    expect(registerMock.called()).to.be.true
+    expect(registerMock.callHistory.called()).to.be.true
     await screen.findByText('abc@gmail.com')
     await screen.findByText('abc@gmail.com')
     await screen.findByText('def@')
     await screen.findByText('def@')
   })
   })

+ 1 - 1
services/web/package.json

@@ -295,7 +295,7 @@
     "esmock": "^2.6.7",
     "esmock": "^2.6.7",
     "events": "^3.3.0",
     "events": "^3.3.0",
     "fake-indexeddb": "^6.0.0",
     "fake-indexeddb": "^6.0.0",
-    "fetch-mock": "^9.10.2",
+    "fetch-mock": "^12.5.2",
     "formik": "^2.2.9",
     "formik": "^2.2.9",
     "fuse.js": "^3.0.0",
     "fuse.js": "^3.0.0",
     "glob": "^7.1.6",
     "glob": "^7.1.6",

+ 6 - 0
services/web/test/frontend/bootstrap.js

@@ -98,3 +98,9 @@ globalThis.DOMParser = window.DOMParser
 
 
 // Polyfill for IndexedDB
 // Polyfill for IndexedDB
 require('fake-indexeddb/auto')
 require('fake-indexeddb/auto')
+
+const fetchMock = require('fetch-mock').default
+
+fetchMock.spyGlobal()
+fetchMock.config.fetch = global.fetch
+fetchMock.config.Response = fetch.Response

+ 3 - 3
services/web/test/frontend/features/chat/components/chat-pane.test.jsx

@@ -26,7 +26,7 @@ describe('<ChatPane />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   const testMessages = [
   const testMessages = [
@@ -45,7 +45,7 @@ describe('<ChatPane />', function () {
   ]
   ]
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     cleanUpContext()
     cleanUpContext()
 
 
     stubMathJax()
     stubMathJax()
@@ -73,7 +73,7 @@ describe('<ChatPane />', function () {
     await screen.findByText('Try again')
     await screen.findByText('Try again')
 
 
     // bring chat back up
     // bring chat back up
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     fetchMock.get(/messages/, [])
     fetchMock.get(/messages/, [])
 
 
     const reconnectButton = screen.getByRole('button', {
     const reconnectButton = screen.getByRole('button', {

+ 50 - 56
services/web/test/frontend/features/chat/context/chat-context.test.jsx

@@ -23,7 +23,7 @@ describe('ChatContext', function () {
   const uuidValue = '00000000-0000-0000-0000-000000000000'
   const uuidValue = '00000000-0000-0000-0000-000000000000'
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     cleanUpContext()
     cleanUpContext()
 
 
     stubMathJax()
     stubMathJax()
@@ -43,14 +43,15 @@ describe('ChatContext', function () {
   describe('socket connection', function () {
   describe('socket connection', function () {
     beforeEach(function () {
     beforeEach(function () {
       // Mock GET messages to return no messages
       // Mock GET messages to return no messages
-      fetchMock.get('express:/project/:projectId/messages', [])
+      // FIXME?
+      // fetchMock.get('express:/project/:projectId/messages', [])
 
 
       // Mock POST new message to return 200
       // Mock POST new message to return 200
       fetchMock.post('express:/project/:projectId/messages', 200)
       fetchMock.post('express:/project/:projectId/messages', 200)
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('subscribes when mounted', function () {
     it('subscribes when mounted', function () {
@@ -106,22 +107,18 @@ describe('ChatContext', function () {
         socket,
         socket,
       })
       })
 
 
-      fetchMock.get(
-        'express:/project/:projectId/messages',
-        [
-          {
-            id: 'msg_1',
-            content: 'new message',
-            timestamp: Date.now(),
-            user: {
-              id: 'another_fake_user',
-              first_name: 'another_fake_user_first_name',
-              email: 'another_fake@example.com',
-            },
+      fetchMock.get('express:/project/:projectId/messages', [
+        {
+          id: 'msg_1',
+          content: 'new message',
+          timestamp: Date.now(),
+          user: {
+            id: 'another_fake_user',
+            first_name: 'another_fake_user_first_name',
+            email: 'another_fake@example.com',
           },
           },
-        ],
-        { overwriteRoutes: true }
-      )
+        },
+      ])
 
 
       // Mock message being received from another user
       // Mock message being received from another user
       socket.emitToClient('new-chat-message', {
       socket.emitToClient('new-chat-message', {
@@ -157,22 +154,18 @@ describe('ChatContext', function () {
         socket,
         socket,
       })
       })
 
 
-      fetchMock.get(
-        'express:/project/:projectId/messages',
-        [
-          {
-            id: 'msg_1',
-            content: 'new message',
-            timestamp: Date.now(),
-            user: {
-              id: 'another_fake_user',
-              first_name: 'another_fake_user_first_name',
-              email: 'another_fake@example.com',
-            },
+      fetchMock.get('express:/project/:projectId/messages', [
+        {
+          id: 'msg_1',
+          content: 'new message',
+          timestamp: Date.now(),
+          user: {
+            id: 'another_fake_user',
+            first_name: 'another_fake_user_first_name',
+            email: 'another_fake@example.com',
           },
           },
-        ],
-        { overwriteRoutes: true }
-      )
+        },
+      ])
 
 
       // Wait until initial messages have loaded
       // Wait until initial messages have loaded
       result.current.loadInitialMessages()
       result.current.loadInitialMessages()
@@ -320,11 +313,13 @@ describe('ChatContext', function () {
 
 
       // Calling a second time won't do anything
       // Calling a second time won't do anything
       result.current.loadInitialMessages()
       result.current.loadInitialMessages()
-      expect(fetchMock.calls()).to.have.lengthOf(1)
+      expect(
+        fetchMock.callHistory.calls('express:/project/:projectId/messages')
+      ).to.have.lengthOf(1)
     })
     })
 
 
     it('provides an error on failure', async function () {
     it('provides an error on failure', async function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
       fetchMock.get('express:/project/:projectId/messages', 500)
       fetchMock.get('express:/project/:projectId/messages', 500)
       const { result, waitForNextUpdate } = renderChatContextHook({})
       const { result, waitForNextUpdate } = renderChatContextHook({})
 
 
@@ -369,18 +364,14 @@ describe('ChatContext', function () {
           // Resolve a full "page" of messages (50)
           // Resolve a full "page" of messages (50)
           createMessages(50, user, new Date('2021-03-04T10:00:00').getTime())
           createMessages(50, user, new Date('2021-03-04T10:00:00').getTime())
         )
         )
-        .getOnce(
-          'express:/project/:projectId/messages',
-          [
-            {
-              id: 'msg_51',
-              content: 'message from second page',
-              user,
-              timestamp: new Date('2021-03-04T11:00:00').getTime(),
-            },
-          ],
-          { overwriteRoutes: false }
-        )
+        .getOnce('express:/project/:projectId/messages', [
+          {
+            id: 'msg_51',
+            content: 'message from second page',
+            user,
+            timestamp: new Date('2021-03-04T11:00:00').getTime(),
+          },
+        ])
 
 
       const { result, waitForNextUpdate } = renderChatContextHook({})
       const { result, waitForNextUpdate } = renderChatContextHook({})
 
 
@@ -422,7 +413,9 @@ describe('ChatContext', function () {
       result.current.loadMoreMessages()
       result.current.loadMoreMessages()
 
 
       expect(result.current.atEnd).to.be.true
       expect(result.current.atEnd).to.be.true
-      expect(fetchMock.calls()).to.have.lengthOf(1)
+      expect(
+        fetchMock.callHistory.calls('express:/project/:projectId/messages')
+      ).to.have.lengthOf(1)
     })
     })
 
 
     it('handles socket messages while loading', async function () {
     it('handles socket messages while loading', async function () {
@@ -479,7 +472,7 @@ describe('ChatContext', function () {
     })
     })
 
 
     it('provides an error on failures', async function () {
     it('provides an error on failures', async function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
       fetchMock.get('express:/project/:projectId/messages', 500)
       fetchMock.get('express:/project/:projectId/messages', 500)
       const { result, waitForNextUpdate } = renderChatContextHook({})
       const { result, waitForNextUpdate } = renderChatContextHook({})
 
 
@@ -515,10 +508,11 @@ describe('ChatContext', function () {
 
 
       result.current.sendMessage('sent message')
       result.current.sendMessage('sent message')
 
 
-      const [, { body }] = fetchMock.lastCall(
-        'express:/project/:projectId/messages',
-        'POST'
-      )
+      const {
+        options: { body },
+      } = fetchMock.callHistory
+        .calls('express:/project/:projectId/messages', { method: 'POST' })
+        .at(-1)
       expect(JSON.parse(body)).to.deep.include({ content: 'sent message' })
       expect(JSON.parse(body)).to.deep.include({ content: 'sent message' })
     })
     })
 
 
@@ -529,14 +523,14 @@ describe('ChatContext', function () {
 
 
       expect(result.current.messages).to.be.empty
       expect(result.current.messages).to.be.empty
       expect(
       expect(
-        fetchMock.called('express:/project/:projectId/messages', {
+        fetchMock.callHistory.called('express:/project/:projectId/messages', {
           method: 'post',
           method: 'post',
         })
         })
       ).to.be.false
       ).to.be.false
     })
     })
 
 
     it('provides an error on failure', async function () {
     it('provides an error on failure', async function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
       fetchMock
       fetchMock
         .get('express:/project/:projectId/messages', [])
         .get('express:/project/:projectId/messages', [])
         .postOnce('express:/project/:projectId/messages', 500)
         .postOnce('express:/project/:projectId/messages', 500)
@@ -614,7 +608,7 @@ function createMessages(number, user, timestamp = Date.now()) {
  * Get query param by key from the last fetchMock response
  * Get query param by key from the last fetchMock response
  */
  */
 function getLastFetchMockQueryParam(key) {
 function getLastFetchMockQueryParam(key) {
-  const url = fetchMock.lastUrl()
+  const { url } = fetchMock.callHistory.calls().at(-1)
   const { searchParams } = new URL(url, 'https://www.overleaf.com')
   const { searchParams } = new URL(url, 'https://www.overleaf.com')
   return searchParams.get(key)
   return searchParams.get(key)
 }
 }

+ 15 - 11
services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.jsx

@@ -7,11 +7,11 @@ import { renderWithEditorContext } from '../../../helpers/render-with-context'
 
 
 describe('<EditorCloneProjectModalWrapper />', function () {
 describe('<EditorCloneProjectModalWrapper />', function () {
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   after(function () {
   after(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   const project = {
   const project = {
@@ -78,19 +78,23 @@ describe('<EditorCloneProjectModalWrapper />', function () {
     fireEvent.click(submitButton)
     fireEvent.click(submitButton)
     expect(submitButton.disabled).to.be.true
     expect(submitButton.disabled).to.be.true
 
 
-    await fetchMock.flush(true)
-    expect(fetchMock.done()).to.be.true
-    const [url, options] = fetchMock.lastCall(
-      'express:/project/:projectId/clone'
+    await fetchMock.callHistory.flush(true)
+    expect(fetchMock.callHistory.done()).to.be.true
+    const { url, options } = fetchMock.callHistory
+      .calls('express:/project/:projectId/clone')
+      .at(-1)
+    expect(url).to.equal(
+      'https://www.test-overleaf.com/project/project-1/clone'
     )
     )
-    expect(url).to.equal('/project/project-1/clone')
 
 
     expect(JSON.parse(options.body)).to.deep.equal({
     expect(JSON.parse(options.body)).to.deep.equal({
       projectName: 'A Cloned Project',
       projectName: 'A Cloned Project',
       tags: [],
       tags: [],
     })
     })
 
 
-    expect(openProject).to.be.calledOnce
+    await waitFor(() => {
+      expect(openProject).to.be.calledOnce
+    })
 
 
     const errorMessage = screen.queryByText('Sorry, something went wrong')
     const errorMessage = screen.queryByText('Sorry, something went wrong')
     expect(errorMessage).to.be.null
     expect(errorMessage).to.be.null
@@ -129,7 +133,7 @@ describe('<EditorCloneProjectModalWrapper />', function () {
 
 
     fireEvent.click(button)
     fireEvent.click(button)
 
 
-    expect(fetchMock.done(matcher)).to.be.true
+    expect(fetchMock.callHistory.done(matcher)).to.be.true
     expect(openProject).not.to.be.called
     expect(openProject).not.to.be.called
 
 
     await screen.findByText('Sorry, something went wrong')
     await screen.findByText('Sorry, something went wrong')
@@ -165,9 +169,9 @@ describe('<EditorCloneProjectModalWrapper />', function () {
     expect(cancelButton.disabled).to.be.false
     expect(cancelButton.disabled).to.be.false
 
 
     fireEvent.click(button)
     fireEvent.click(button)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
-    expect(fetchMock.done(matcher)).to.be.true
+    expect(fetchMock.callHistory.done(matcher)).to.be.true
     expect(openProject).not.to.be.called
     expect(openProject).not.to.be.called
 
 
     await screen.findByText('There was an error!')
     await screen.findByText('There was an error!')

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx

@@ -20,7 +20,7 @@ describe('<ActionsCopyProject />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     this.locationStub.restore()
     this.locationStub.restore()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct modal when clicked', async function () {
   it('shows correct modal when clicked', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/actions-menu.test.jsx

@@ -21,7 +21,7 @@ describe('<ActionsMenu />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu for non-anonymous users', async function () {
   it('shows correct menu for non-anonymous users', async function () {

+ 7 - 3
services/web/test/frontend/features/editor-left-menu/components/actions-word-count.test.jsx

@@ -6,7 +6,7 @@ import { renderWithEditorContext } from '../../../helpers/render-with-context'
 
 
 describe('<ActionsWordCount />', function () {
 describe('<ActionsWordCount />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct modal when clicked after document is compiled', async function () {
   it('shows correct modal when clicked after document is compiled', async function () {
@@ -51,11 +51,15 @@ describe('<ActionsWordCount />', function () {
     // when loading, we don't render the "Word Count" as button yet
     // when loading, we don't render the "Word Count" as button yet
     expect(screen.queryByRole('button', { name: 'Word Count' })).to.equal(null)
     expect(screen.queryByRole('button', { name: 'Word Count' })).to.equal(null)
 
 
-    await waitFor(() => expect(fetchMock.called(compileEndpoint)).to.be.true)
+    await waitFor(
+      () => expect(fetchMock.callHistory.called(compileEndpoint)).to.be.true
+    )
 
 
     const button = await screen.findByRole('button', { name: 'Word Count' })
     const button = await screen.findByRole('button', { name: 'Word Count' })
     button.click()
     button.click()
 
 
-    await waitFor(() => expect(fetchMock.called(wordcountEndpoint)).to.be.true)
+    await waitFor(
+      () => expect(fetchMock.callHistory.called(wordcountEndpoint)).to.be.true
+    )
   })
   })
 })
 })

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/download-menu.test.jsx

@@ -6,7 +6,7 @@ import { renderWithEditorContext } from '../../../helpers/render-with-context'
 
 
 describe('<DownloadMenu />', function () {
 describe('<DownloadMenu />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows download links with correct url', async function () {
   it('shows download links with correct url', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/help-contact-us.test.jsx

@@ -14,7 +14,7 @@ describe('<HelpContactUs />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('open contact us modal when clicked', function () {
   it('open contact us modal when clicked', function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/help-menu.test.jsx

@@ -14,7 +14,7 @@ describe('<HelpMenu />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu if `showSupport` is `true`', function () {
   it('shows correct menu if `showSupport` is `true`', function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/help-show-hotkeys.test.jsx

@@ -6,7 +6,7 @@ import fetchMock from 'fetch-mock'
 
 
 describe('<HelpShowHotkeys />', function () {
 describe('<HelpShowHotkeys />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('open hotkeys modal when clicked', function () {
   it('open hotkeys modal when clicked', function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-auto-close-brackets.test.tsx

@@ -7,7 +7,7 @@ import { EditorProviders } from '../../../../helpers/editor-providers'
 
 
 describe('<SettingsAutoCloseBrackets />', function () {
 describe('<SettingsAutoCloseBrackets />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-auto-complete.test.tsx

@@ -7,7 +7,7 @@ import { EditorProviders } from '../../../../helpers/editor-providers'
 
 
 describe('<SettingsAutoComplete />', function () {
 describe('<SettingsAutoComplete />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-compiler.test.tsx

@@ -7,7 +7,7 @@ import { EditorProviders } from '../../../../helpers/editor-providers'
 
 
 describe('<SettingsCompiler />', function () {
 describe('<SettingsCompiler />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-document.test.tsx

@@ -30,7 +30,7 @@ describe('<SettingsDocument />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     window.metaAttributesCache.set('ol-ExposedSettings', originalSettings)
     window.metaAttributesCache.set('ol-ExposedSettings', originalSettings)
   })
   })
 
 

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-editor-theme.test.tsx

@@ -16,7 +16,7 @@ describe('<SettingsEditorTheme />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-font-family.test.tsx

@@ -7,7 +7,7 @@ import { EditorProviders } from '../../../../helpers/editor-providers'
 
 
 describe('<SettingsFontFamily />', function () {
 describe('<SettingsFontFamily />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-font-size.test.tsx

@@ -9,7 +9,7 @@ describe('<SettingsFontSize />', function () {
   const sizes = ['10', '11', '12', '13', '14', '16', '18', '20', '22', '24']
   const sizes = ['10', '11', '12', '13', '14', '16', '18', '20', '22', '24']
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-image-name.test.tsx

@@ -23,7 +23,7 @@ describe('<SettingsImageName />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-keybindings.test.tsx

@@ -7,7 +7,7 @@ import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/e
 
 
 describe('<SettingsKeybindings />', function () {
 describe('<SettingsKeybindings />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-line-height.test.tsx

@@ -7,7 +7,7 @@ import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/e
 
 
 describe('<SettingsLineHeight />', function () {
 describe('<SettingsLineHeight />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx

@@ -7,7 +7,7 @@ import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/e
 
 
 describe('<SettingsMathPreview />', function () {
 describe('<SettingsMathPreview />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-overall-theme.test.tsx

@@ -32,7 +32,7 @@ describe('<SettingsOverallTheme />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-pdf-viewer.test.tsx

@@ -7,7 +7,7 @@ import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/e
 
 
 describe('<SettingsPdfViewer />', function () {
 describe('<SettingsPdfViewer />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-spell-check-language.test.tsx

@@ -25,7 +25,7 @@ describe('<SettingsSpellCheckLanguage />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-syntax-validation.test.tsx

@@ -7,7 +7,7 @@ import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/e
 
 
 describe('<SettingsSyntaxValidation />', function () {
 describe('<SettingsSyntaxValidation />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows correct menu', async function () {
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.jsx

@@ -22,7 +22,7 @@ describe('<LayoutDropdownButton />', function () {
   afterEach(function () {
   afterEach(function () {
     openStub.restore()
     openStub.restore()
     sendMBSpy.restore()
     sendMBSpy.restore()
-    fetchMock.restore()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('should mark current layout option as selected', async function () {
   it('should mark current layout option as selected', async function () {

+ 1 - 1
services/web/test/frontend/features/file-view/components/file-view-header.test.jsx

@@ -39,7 +39,7 @@ describe('<FileViewHeader/>', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('header text', function () {
   describe('header text', function () {

+ 1 - 1
services/web/test/frontend/features/file-view/components/file-view-refresh-button.test.jsx

@@ -23,7 +23,7 @@ describe('<FileViewRefreshButton />', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   // eslint-disable-next-line mocha/no-skipped-tests
   // eslint-disable-next-line mocha/no-skipped-tests

+ 1 - 1
services/web/test/frontend/features/file-view/components/file-view-text.test.jsx

@@ -19,7 +19,7 @@ describe('<FileViewText/>', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders a text view', async function () {
   it('renders a text view', async function () {

+ 1 - 1
services/web/test/frontend/features/file-view/components/file-view.test.jsx

@@ -32,7 +32,7 @@ describe('<FileView/>', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('for a text file', function () {
   describe('for a text file', function () {

+ 1 - 1
services/web/test/frontend/features/group-management/components/members-table/unlink-user-modal.test.tsx

@@ -31,7 +31,7 @@ describe('<UnlinkUserModal />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('displays the modal', async function () {
   it('displays the modal', async function () {

+ 1 - 1
services/web/test/frontend/features/project-list/components/load-more.test.tsx

@@ -11,7 +11,7 @@ import { renderWithProjectListContext } from '../helpers/render-with-context'
 
 
 describe('<LoadMore />', function () {
 describe('<LoadMore />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders on a project list longer than 40', async function () {
   it('renders on a project list longer than 40', async function () {

+ 1 - 1
services/web/test/frontend/features/project-list/components/new-project-button.test.tsx

@@ -7,7 +7,7 @@ import getMeta from '@/utils/meta'
 
 
 describe('<NewProjectButton />', function () {
 describe('<NewProjectButton />', function () {
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('for every user (affiliated and non-affiliated)', function () {
   describe('for every user (affiliated and non-affiliated)', function () {

+ 5 - 5
services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx

@@ -20,7 +20,7 @@ describe('<ModalContentNewProjectForm />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     this.locationStub.restore()
     this.locationStub.restore()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('submits form', async function () {
   it('submits form', async function () {
@@ -49,7 +49,7 @@ describe('<ModalContentNewProjectForm />', function () {
 
 
     fireEvent.click(createButton)
     fireEvent.click(createButton)
 
 
-    expect(newProjectMock.called()).to.be.true
+    expect(newProjectMock.callHistory.called()).to.be.true
 
 
     await waitFor(() => {
     await waitFor(() => {
       sinon.assert.calledOnce(assignStub)
       sinon.assert.calledOnce(assignStub)
@@ -76,7 +76,7 @@ describe('<ModalContentNewProjectForm />', function () {
     })
     })
     fireEvent.click(createButton)
     fireEvent.click(createButton)
 
 
-    expect(newProjectMock.called()).to.be.true
+    expect(newProjectMock.callHistory.called()).to.be.true
 
 
     await waitFor(() => {
     await waitFor(() => {
       screen.getByText(errorMessage)
       screen.getByText(errorMessage)
@@ -102,7 +102,7 @@ describe('<ModalContentNewProjectForm />', function () {
     })
     })
     fireEvent.click(createButton)
     fireEvent.click(createButton)
 
 
-    expect(newProjectMock.called()).to.be.true
+    expect(newProjectMock.callHistory.called()).to.be.true
 
 
     await waitFor(() => {
     await waitFor(() => {
       screen.getByText(errorMessage)
       screen.getByText(errorMessage)
@@ -141,7 +141,7 @@ describe('<ModalContentNewProjectForm />', function () {
     })
     })
     fireEvent.click(createButton)
     fireEvent.click(createButton)
 
 
-    expect(newProjectMock.called()).to.be.true
+    expect(newProjectMock.callHistory.called()).to.be.true
 
 
     await waitFor(() => {
     await waitFor(() => {
       screen.getByText(errorMessage)
       screen.getByText(errorMessage)

+ 55 - 54
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -69,7 +69,7 @@ describe('<UserNotifications />', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
 
 
     // at least one project is required to show some notifications
     // at least one project is required to show some notifications
     const projects = [{}] as Project[]
     const projects = [{}] as Project[]
@@ -83,7 +83,7 @@ describe('<UserNotifications />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('<Common>', function () {
   describe('<Common>', function () {
@@ -93,7 +93,7 @@ describe('<UserNotifications />', function () {
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('accepts project invite', async function () {
     it('accepts project invite', async function () {
@@ -106,14 +106,14 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
 
 
       const deleteMock = fetchMock.delete(
       const deleteMock = fetchMock.delete(
         `/notifications/${reconfiguredNotification._id}`,
         `/notifications/${reconfiguredNotification._id}`,
         200
         200
       )
       )
       const acceptMock = fetchMock.post(
       const acceptMock = fetchMock.post(
-        `project/${notificationProjectInvite.messageOpts.projectId}/invite/token/${notificationProjectInvite.messageOpts.token}/accept`,
+        `/project/${notificationProjectInvite.messageOpts.projectId}/invite/token/${notificationProjectInvite.messageOpts.token}/accept`,
         200
         200
       )
       )
 
 
@@ -134,8 +134,9 @@ describe('<UserNotifications />', function () {
         screen.getByRole('button', { name: /joining/i })
         screen.getByRole('button', { name: /joining/i })
       )
       )
 
 
-      expect(acceptMock.called()).to.be.true
-      screen.getByText(/joined/i)
+      expect(acceptMock.callHistory.called()).to.be.true
+      await screen.findByText(/joined/i)
+
       expect(screen.queryByRole('button', { name: /join project/i })).to.be.null
       expect(screen.queryByRole('button', { name: /join project/i })).to.be.null
 
 
       const openProject = screen.getByRole('button', { name: /open project/i })
       const openProject = screen.getByRole('button', { name: /open project/i })
@@ -146,7 +147,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(deleteMock.called()).to.be.true
+      expect(deleteMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -160,9 +161,9 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.post(
       fetchMock.post(
-        `project/${notificationProjectInvite.messageOpts.projectId}/invite/token/${notificationProjectInvite.messageOpts.token}/accept`,
+        `/project/${notificationProjectInvite.messageOpts.projectId}/invite/token/${notificationProjectInvite.messageOpts.token}/accept`,
         500
         500
       )
       )
 
 
@@ -179,7 +180,7 @@ describe('<UserNotifications />', function () {
         screen.getByRole('button', { name: /joining/i })
         screen.getByRole('button', { name: /joining/i })
       )
       )
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       screen.getByRole('button', { name: /join project/i })
       screen.getByRole('button', { name: /join project/i })
       expect(screen.queryByRole('button', { name: /open project/i })).to.be.null
       expect(screen.queryByRole('button', { name: /open project/i })).to.be.null
     })
     })
@@ -194,7 +195,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
@@ -207,7 +208,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -225,7 +226,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
@@ -246,7 +247,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -264,7 +265,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
@@ -281,7 +282,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -294,7 +295,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
       screen.getByText(/file limit/i)
       screen.getByText(/file limit/i)
@@ -323,7 +324,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
@@ -337,7 +338,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -355,7 +356,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
@@ -371,7 +372,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -385,7 +386,7 @@ describe('<UserNotifications />', function () {
       ])
       ])
 
 
       renderWithinProjectListProvider(Common)
       renderWithinProjectListProvider(Common)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
       fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
 
 
       screen.getByRole('alert')
       screen.getByRole('alert')
@@ -394,7 +395,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -414,7 +415,7 @@ describe('<UserNotifications />', function () {
           ])
           ])
 
 
           renderWithinProjectListProvider(Common)
           renderWithinProjectListProvider(Common)
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
           fetchMock.delete(`/notifications/${notificationGroupInvite._id}`, 200)
           fetchMock.delete(`/notifications/${notificationGroupInvite._id}`, 200)
           screen.getByRole('alert')
           screen.getByRole('alert')
           screen.getByText('inviter@overleaf.com')
           screen.getByText('inviter@overleaf.com')
@@ -444,7 +445,7 @@ describe('<UserNotifications />', function () {
             )
             )
 
 
             renderWithinProjectListProvider(Common)
             renderWithinProjectListProvider(Common)
-            await fetchMock.flush(true)
+            await fetchMock.callHistory.flush(true)
             fetchMock.delete(
             fetchMock.delete(
               `/notifications/${notificationGroupInvite._id}`,
               `/notifications/${notificationGroupInvite._id}`,
               200
               200
@@ -465,11 +466,11 @@ describe('<UserNotifications />', function () {
   describe('<Institution>', function () {
   describe('<Institution>', function () {
     beforeEach(function () {
     beforeEach(function () {
       Object.assign(getMeta('ol-ExposedSettings'), exposedSettings)
       Object.assign(getMeta('ol-ExposedSettings'), exposedSettings)
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('shows sso available', function () {
     it('shows sso available', function () {
@@ -513,7 +514,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -537,7 +538,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -563,7 +564,7 @@ describe('<UserNotifications />', function () {
       const closeBtn = screen.getByRole('button', { name: /close/i })
       const closeBtn = screen.getByRole('button', { name: /close/i })
       fireEvent.click(closeBtn)
       fireEvent.click(closeBtn)
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -658,7 +659,7 @@ describe('<UserNotifications />', function () {
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
       this.clock.restore()
       this.clock.restore()
     })
     })
 
 
@@ -670,7 +671,7 @@ describe('<UserNotifications />', function () {
         window.metaAttributesCache.set('ol-userEmails', userEmails)
         window.metaAttributesCache.set('ol-userEmails', userEmails)
 
 
         renderWithinProjectListProvider(ConfirmEmail)
         renderWithinProjectListProvider(ConfirmEmail)
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
         fetchMock.post('/user/emails/resend_confirmation', 200)
         fetchMock.post('/user/emails/resend_confirmation', 200)
 
 
         const email = userEmails[0].email
         const email = userEmails[0].email
@@ -693,7 +694,7 @@ describe('<UserNotifications />', function () {
           screen.getByRole('button', { name: /resend/i })
           screen.getByRole('button', { name: /resend/i })
         )
         )
 
 
-        expect(fetchMock.called()).to.be.true
+        expect(fetchMock.callHistory.called()).to.be.true
         expect(screen.queryByRole('alert')).to.be.null
         expect(screen.queryByRole('alert')).to.be.null
       })
       })
     }
     }
@@ -712,7 +713,7 @@ describe('<UserNotifications />', function () {
       window.metaAttributesCache.set('ol-userEmails', [untrustedUserData])
       window.metaAttributesCache.set('ol-userEmails', [untrustedUserData])
 
 
       renderWithinProjectListProvider(ConfirmEmail)
       renderWithinProjectListProvider(ConfirmEmail)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.post('/user/emails/resend_confirmation', 200)
       fetchMock.post('/user/emails/resend_confirmation', 200)
 
 
       const email = untrustedUserData.email
       const email = untrustedUserData.email
@@ -730,7 +731,7 @@ describe('<UserNotifications />', function () {
         screen.getByRole('button', { name: /resend/i })
         screen.getByRole('button', { name: /resend/i })
       )
       )
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       expect(screen.queryByRole('alert')).to.be.null
       expect(screen.queryByRole('alert')).to.be.null
     })
     })
 
 
@@ -738,7 +739,7 @@ describe('<UserNotifications />', function () {
       window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
       window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
 
 
       renderWithinProjectListProvider(ConfirmEmail)
       renderWithinProjectListProvider(ConfirmEmail)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       fetchMock.post('/user/emails/resend_confirmation', 500)
       fetchMock.post('/user/emails/resend_confirmation', 500)
 
 
       const resendButtons = screen.getAllByRole('button', { name: /resend/i })
       const resendButtons = screen.getAllByRole('button', { name: /resend/i })
@@ -752,7 +753,7 @@ describe('<UserNotifications />', function () {
         )
         )
       )
       )
 
 
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       screen.getByText(/something went wrong/i)
       screen.getByText(/something went wrong/i)
     })
     })
 
 
@@ -764,7 +765,7 @@ describe('<UserNotifications />', function () {
         window.metaAttributesCache.set('ol-usersBestSubscription', subscription)
         window.metaAttributesCache.set('ol-usersBestSubscription', subscription)
 
 
         renderWithinProjectListProvider(ConfirmEmail)
         renderWithinProjectListProvider(ConfirmEmail)
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         const alert = screen.getByRole('alert')
         const alert = screen.getByRole('alert')
         const email = unconfirmedCommonsUserData.email
         const email = unconfirmedCommonsUserData.email
@@ -785,7 +786,7 @@ describe('<UserNotifications />', function () {
         window.metaAttributesCache.set('ol-usersBestSubscription', subscription)
         window.metaAttributesCache.set('ol-usersBestSubscription', subscription)
 
 
         renderWithinProjectListProvider(ConfirmEmail)
         renderWithinProjectListProvider(ConfirmEmail)
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         const alert = screen.getByRole('alert')
         const alert = screen.getByRole('alert')
         const email = unconfirmedCommonsUserData.email
         const email = unconfirmedCommonsUserData.email
@@ -818,12 +819,12 @@ describe('<UserNotifications />', function () {
         reload: sinon.stub(),
         reload: sinon.stub(),
         setHash: sinon.stub(),
         setHash: sinon.stub(),
       })
       })
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
       this.locationStub.restore()
       this.locationStub.restore()
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('shows reconfirm message with SSO disabled', async function () {
     it('shows reconfirm message with SSO disabled', async function () {
@@ -860,12 +861,12 @@ describe('<UserNotifications />', function () {
         .be.null
         .be.null
       expect(screen.queryByRole('link', { name: /remove it/i })).to.be.null
       expect(screen.queryByRole('link', { name: /remove it/i })).to.be.null
       expect(screen.queryByRole('link', { name: /learn more/i })).to.be.null
       expect(screen.queryByRole('link', { name: /learn more/i })).to.be.null
-      expect(sendReconfirmationMock.called()).to.be.true
+      expect(sendReconfirmationMock.callHistory.called()).to.be.true
       fireEvent.click(
       fireEvent.click(
         screen.getByRole('button', { name: /resend confirmation email/i })
         screen.getByRole('button', { name: /resend confirmation email/i })
       )
       )
       await waitForElementToBeRemoved(() => screen.getByText('Sending…'))
       await waitForElementToBeRemoved(() => screen.getByText('Sending…'))
-      expect(sendReconfirmationMock.calls()).to.have.lengthOf(2)
+      expect(sendReconfirmationMock.callHistory.calls()).to.have.lengthOf(2)
     })
     })
 
 
     it('shows reconfirm message with SSO enabled', async function () {
     it('shows reconfirm message with SSO enabled', async function () {
@@ -905,7 +906,7 @@ describe('<UserNotifications />', function () {
   describe('<GroupsAndEnterpriseBanner />', function () {
   describe('<GroupsAndEnterpriseBanner />', function () {
     beforeEach(function () {
     beforeEach(function () {
       localStorage.clear()
       localStorage.clear()
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
 
 
       // at least one project is required to show some notifications
       // at least one project is required to show some notifications
       const projects = [{}] as Project[]
       const projects = [{}] as Project[]
@@ -924,14 +925,14 @@ describe('<UserNotifications />', function () {
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('does not show the banner for users that are in group or are affiliated', async function () {
     it('does not show the banner for users that are in group or are affiliated', async function () {
       window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', false)
       window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', false)
 
 
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
 
 
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.be.null
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.be.null
     })
     })
@@ -941,7 +942,7 @@ describe('<UserNotifications />', function () {
       localStorage.setItem('has_dismissed_groups_and_enterprise_banner', true)
       localStorage.setItem('has_dismissed_groups_and_enterprise_banner', true)
 
 
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
 
 
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
         .null
         .null
@@ -957,7 +958,7 @@ describe('<UserNotifications />', function () {
       )
       )
 
 
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
 
 
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
         .null
         .null
@@ -973,7 +974,7 @@ describe('<UserNotifications />', function () {
       )
       )
 
 
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
 
 
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.be.null
       expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.be.null
     })
     })
@@ -981,7 +982,7 @@ describe('<UserNotifications />', function () {
     describe('users that are not in group and are not affiliated', function () {
     describe('users that are not in group and are not affiliated', function () {
       beforeEach(function () {
       beforeEach(function () {
         localStorage.clear()
         localStorage.clear()
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
 
 
         // at least one project is required to show some notifications
         // at least one project is required to show some notifications
         const projects = [{}] as Project[]
         const projects = [{}] as Project[]
@@ -997,7 +998,7 @@ describe('<UserNotifications />', function () {
       })
       })
 
 
       afterEach(function () {
       afterEach(function () {
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
       })
       })
 
 
       after(function () {
       after(function () {
@@ -1011,7 +1012,7 @@ describe('<UserNotifications />', function () {
         )
         )
 
 
         renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
         renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         screen.getByText(
         screen.getByText(
           'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
           'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
@@ -1028,7 +1029,7 @@ describe('<UserNotifications />', function () {
         )
         )
 
 
         renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
         renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         screen.getByText(
         screen.getByText(
           'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
           'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'

+ 85 - 60
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -79,7 +79,7 @@ describe('<ProjectListRoot />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     sendMBSpy.restore()
     sendMBSpy.restore()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     this.locationStub.restore()
     this.locationStub.restore()
   })
   })
 
 
@@ -88,11 +88,11 @@ describe('<ProjectListRoot />', function () {
       renderWithProjectListContext(<ProjectListRoot />, {
       renderWithProjectListContext(<ProjectListRoot />, {
         projects: [],
         projects: [],
       })
       })
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
     })
     })
 
 
     it('the welcome page is displayed', async function () {
     it('the welcome page is displayed', async function () {
-      screen.getByRole('heading', { name: 'Welcome to Overleaf' })
+      await screen.findByRole('heading', { name: 'Welcome to Overleaf' })
     })
     })
 
 
     it('the email confirmation alert is not displayed', async function () {
     it('the email confirmation alert is not displayed', async function () {
@@ -110,7 +110,7 @@ describe('<ProjectListRoot />', function () {
         projects: fullList,
         projects: fullList,
       })
       })
       this.unmount = unmount
       this.unmount = unmount
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       await screen.findByRole('table')
       await screen.findByRole('table')
     })
     })
 
 
@@ -172,13 +172,17 @@ describe('<ProjectListRoot />', function () {
           await waitFor(
           await waitFor(
             () =>
             () =>
               expect(
               expect(
-                archiveProjectMock.called(`/project/${project1Id}/archive`)
+                archiveProjectMock.callHistory.called(
+                  `/project/${project1Id}/archive`
+                )
               ).to.be.true
               ).to.be.true
           )
           )
           await waitFor(
           await waitFor(
             () =>
             () =>
               expect(
               expect(
-                archiveProjectMock.called(`/project/${project2Id}/archive`)
+                archiveProjectMock.callHistory.called(
+                  `/project/${project2Id}/archive`
+                )
               ).to.be.true
               ).to.be.true
           )
           )
         })
         })
@@ -203,13 +207,19 @@ describe('<ProjectListRoot />', function () {
 
 
           await waitFor(
           await waitFor(
             () =>
             () =>
-              expect(trashProjectMock.called(`/project/${project1Id}/trash`)).to
-                .be.true
+              expect(
+                trashProjectMock.callHistory.called(
+                  `/project/${project1Id}/trash`
+                )
+              ).to.be.true
           )
           )
           await waitFor(
           await waitFor(
             () =>
             () =>
-              expect(trashProjectMock.called(`/project/${project2Id}/trash`)).to
-                .be.true
+              expect(
+                trashProjectMock.callHistory.called(
+                  `/project/${project2Id}/trash`
+                )
+              ).to.be.true
           )
           )
         })
         })
 
 
@@ -281,8 +291,8 @@ describe('<ProjectListRoot />', function () {
           })
           })
           fireEvent.click(unarchiveButton)
           fireEvent.click(unarchiveButton)
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
           await screen.findByText('No projects')
           await screen.findByText('No projects')
         })
         })
@@ -296,8 +306,8 @@ describe('<ProjectListRoot />', function () {
             archivedProjects.length - 1
             archivedProjects.length - 1
           )
           )
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
           expect(screen.queryByText('No projects')).to.be.null
           expect(screen.queryByText('No projects')).to.be.null
         })
         })
@@ -348,8 +358,8 @@ describe('<ProjectListRoot />', function () {
             within(actionsToolbar).getByText<HTMLButtonElement>('Restore')
             within(actionsToolbar).getByText<HTMLButtonElement>('Restore')
           fireEvent.click(untrashButton)
           fireEvent.click(untrashButton)
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
           await screen.findByText('No projects')
           await screen.findByText('No projects')
         })
         })
@@ -361,8 +371,8 @@ describe('<ProjectListRoot />', function () {
           const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
           const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
           expect(allCheckboxesChecked.length).to.equal(trashedList.length - 1)
           expect(allCheckboxesChecked.length).to.equal(trashedList.length - 1)
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
           expect(screen.queryByText('No projects')).to.be.null
           expect(screen.queryByText('No projects')).to.be.null
         })
         })
@@ -386,26 +396,28 @@ describe('<ProjectListRoot />', function () {
           fireEvent.click(confirmButton)
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
           expect(confirmButton.disabled).to.be.true
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
-          const calls = fetchMock.calls().map(([url]) => url)
+          const calls = fetchMock.callHistory.calls().map(({ url }) => url)
 
 
           trashedList.forEach(project => {
           trashedList.forEach(project => {
-            expect(calls).to.contain(`/project/${project.id}/archive`)
+            expect(calls).to.contain(
+              `https://www.test-overleaf.com/project/${project.id}/archive`
+            )
           })
           })
         })
         })
 
 
         it('removes only selected projects from view when leaving', async function () {
         it('removes only selected projects from view when leaving', async function () {
           // rerender content with different projects
           // rerender content with different projects
           this.unmount()
           this.unmount()
-          fetchMock.restore()
+          fetchMock.removeRoutes().clearHistory()
 
 
           renderWithProjectListContext(<ProjectListRoot />, {
           renderWithProjectListContext(<ProjectListRoot />, {
             projects: leavableList,
             projects: leavableList,
           })
           })
 
 
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
           await screen.findByRole('table')
           await screen.findByRole('table')
 
 
           expect(leavableList.length).to.be.greaterThan(0)
           expect(leavableList.length).to.be.greaterThan(0)
@@ -446,25 +458,27 @@ describe('<ProjectListRoot />', function () {
           fireEvent.click(confirmButton)
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
           expect(confirmButton.disabled).to.be.true
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
-          const calls = fetchMock.calls().map(([url]) => url)
+          const calls = fetchMock.callHistory.calls().map(({ url }) => url)
           leavableList.forEach(project => {
           leavableList.forEach(project => {
-            expect(calls).to.contain(`/project/${project.id}/leave`)
+            expect(calls).to.contain(
+              `https://www.test-overleaf.com/project/${project.id}/leave`
+            )
           })
           })
         })
         })
 
 
         it('removes only selected projects from view when deleting', async function () {
         it('removes only selected projects from view when deleting', async function () {
           // rerender content with different projects
           // rerender content with different projects
           this.unmount()
           this.unmount()
-          fetchMock.restore()
+          fetchMock.removeRoutes().clearHistory()
 
 
           renderWithProjectListContext(<ProjectListRoot />, {
           renderWithProjectListContext(<ProjectListRoot />, {
             projects: deletableList,
             projects: deletableList,
           })
           })
 
 
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
           await screen.findByRole('table')
           await screen.findByRole('table')
 
 
           expect(deletableList.length).to.be.greaterThan(0)
           expect(deletableList.length).to.be.greaterThan(0)
@@ -505,19 +519,21 @@ describe('<ProjectListRoot />', function () {
           fireEvent.click(confirmButton)
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
           expect(confirmButton.disabled).to.be.true
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
-          const calls = fetchMock.calls().map(([url]) => url)
+          const calls = fetchMock.callHistory.calls().map(({ url }) => url)
           deletableList.forEach(project => {
           deletableList.forEach(project => {
-            expect(calls).to.contain(`/project/${project.id}`)
+            expect(calls).to.contain(
+              `https://www.test-overleaf.com/project/${project.id}`
+            )
           })
           })
         })
         })
 
 
         it('removes only selected projects from view when deleting and leaving', async function () {
         it('removes only selected projects from view when deleting and leaving', async function () {
           // rerender content with different projects
           // rerender content with different projects
           this.unmount()
           this.unmount()
-          fetchMock.restore()
+          fetchMock.removeRoutes().clearHistory()
 
 
           const deletableAndLeavableList = [...deletableList, ...leavableList]
           const deletableAndLeavableList = [...deletableList, ...leavableList]
 
 
@@ -525,7 +541,7 @@ describe('<ProjectListRoot />', function () {
             projects: deletableAndLeavableList,
             projects: deletableAndLeavableList,
           })
           })
 
 
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
           await screen.findByRole('table')
           await screen.findByRole('table')
 
 
           expect(deletableList.length).to.be.greaterThan(0)
           expect(deletableList.length).to.be.greaterThan(0)
@@ -573,14 +589,14 @@ describe('<ProjectListRoot />', function () {
           fireEvent.click(confirmButton)
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
           expect(confirmButton.disabled).to.be.true
 
 
-          await fetchMock.flush(true)
-          expect(fetchMock.done()).to.be.true
+          await fetchMock.callHistory.flush(true)
+          expect(fetchMock.callHistory.done()).to.be.true
 
 
-          const calls = fetchMock.calls().map(([url]) => url)
+          const calls = fetchMock.callHistory.calls().map(({ url }) => url)
           deletableAndLeavableList.forEach(project => {
           deletableAndLeavableList.forEach(project => {
             expect(calls).to.contain.oneOf([
             expect(calls).to.contain.oneOf([
-              `/project/${project.id}`,
-              `/project/${project.id}/leave`,
+              `https://www.test-overleaf.com/project/${project.id}`,
+              `https://www.test-overleaf.com/project/${project.id}/leave`,
             ])
             ])
           })
           })
         })
         })
@@ -589,7 +605,7 @@ describe('<ProjectListRoot />', function () {
       describe('tags', function () {
       describe('tags', function () {
         it('does not show archived or trashed project', async function () {
         it('does not show archived or trashed project', async function () {
           this.unmount()
           this.unmount()
-          fetchMock.restore()
+          fetchMock.removeRoutes().clearHistory()
           window.metaAttributesCache.set('ol-tags', [
           window.metaAttributesCache.set('ol-tags', [
             {
             {
               _id: this.tagId,
               _id: this.tagId,
@@ -642,7 +658,7 @@ describe('<ProjectListRoot />', function () {
             )
             )
             await waitFor(() => {
             await waitFor(() => {
               expect(
               expect(
-                trashProjectMock.called(
+                trashProjectMock.callHistory.called(
                   `/project/${projectsData[index].id}/trash`
                   `/project/${projectsData[index].id}/trash`
                 )
                 )
               ).to.be.true
               ).to.be.true
@@ -702,11 +718,13 @@ describe('<ProjectListRoot />', function () {
           })
           })
           fireEvent.click(createButton)
           fireEvent.click(createButton)
 
 
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
 
 
-          expect(fetchMock.called('/tag', { name: this.newTagName })).to.be.true
           expect(
           expect(
-            fetchMock.called(`/tag/${this.newTagId}/projects`, {
+            fetchMock.callHistory.called('/tag', { name: this.newTagName })
+          ).to.be.true
+          expect(
+            fetchMock.callHistory.called(`/tag/${this.newTagId}/projects`, {
               body: {
               body: {
                 projectIds: [projectsData[0].id, projectsData[1].id],
                 projectIds: [projectsData[0].id, projectsData[1].id],
               },
               },
@@ -734,10 +752,10 @@ describe('<ProjectListRoot />', function () {
           )
           )
           fireEvent.click(tagButton)
           fireEvent.click(tagButton)
 
 
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
 
 
           expect(
           expect(
-            deleteProjectsFromTagMock.called(
+            deleteProjectsFromTagMock.callHistory.called(
               `/tag/${this.tagId}/projects/remove`,
               `/tag/${this.tagId}/projects/remove`,
               {
               {
                 body: {
                 body: {
@@ -770,14 +788,17 @@ describe('<ProjectListRoot />', function () {
           )
           )
           fireEvent.click(tagButton)
           fireEvent.click(tagButton)
 
 
-          await fetchMock.flush(true)
+          await fetchMock.callHistory.flush(true)
 
 
           expect(
           expect(
-            addProjectsToTagMock.called(`/tag/${this.tagId}/projects`, {
-              body: {
-                projectIds: [projectsData[2].id],
-              },
-            })
+            addProjectsToTagMock.callHistory.called(
+              `/tag/${this.tagId}/projects`,
+              {
+                body: {
+                  projectIds: [projectsData[2].id],
+                },
+              }
+            )
           ).to.be.true
           ).to.be.true
           screen.getByRole('button', { name: `${this.tagName} (3)` })
           screen.getByRole('button', { name: `${this.tagName} (3)` })
         })
         })
@@ -919,10 +940,12 @@ describe('<ProjectListRoot />', function () {
             expect(confirmButton.disabled).to.be.false
             expect(confirmButton.disabled).to.be.false
             fireEvent.click(confirmButton)
             fireEvent.click(confirmButton)
 
 
-            await fetchMock.flush(true)
+            await fetchMock.callHistory.flush(true)
 
 
             expect(
             expect(
-              renameProjectMock.called(`/project/${projectsData[0].id}/rename`)
+              renameProjectMock.callHistory.called(
+                `/project/${projectsData[0].id}/rename`
+              )
             ).to.be.true
             ).to.be.true
 
 
             const table = await screen.findByRole('table')
             const table = await screen.findByRole('table')
@@ -982,10 +1005,12 @@ describe('<ProjectListRoot />', function () {
             ) as HTMLElement
             ) as HTMLElement
             fireEvent.click(copyConfirmButton)
             fireEvent.click(copyConfirmButton)
 
 
-            await fetchMock.flush(true)
+            await fetchMock.callHistory.flush(true)
 
 
             expect(
             expect(
-              cloneProjectMock.called(`/project/${projectsData[1].id}/clone`)
+              cloneProjectMock.callHistory.called(
+                `/project/${projectsData[1].id}/clone`
+              )
             ).to.be.true
             ).to.be.true
 
 
             expect(sendMBSpy).to.have.been.calledTwice
             expect(sendMBSpy).to.have.been.calledTwice
@@ -1149,8 +1174,8 @@ describe('<ProjectListRoot />', function () {
         ) as HTMLElement
         ) as HTMLElement
         fireEvent.click(copyConfirmButton)
         fireEvent.click(copyConfirmButton)
 
 
-        await fetchMock.flush(true)
-        expect(fetchMock.done()).to.be.true
+        await fetchMock.callHistory.flush(true)
+        expect(fetchMock.callHistory.done()).to.be.true
 
 
         expect(sendMBSpy).to.have.been.calledTwice
         expect(sendMBSpy).to.have.been.calledTwice
         expect(sendMBSpy).to.have.been.calledWith('loads_v2_dash')
         expect(sendMBSpy).to.have.been.calledWith('loads_v2_dash')

+ 2 - 2
services/web/test/frontend/features/project-list/components/project-search.test.tsx

@@ -12,11 +12,11 @@ describe('Project list search form', function () {
 
 
   beforeEach(function () {
   beforeEach(function () {
     sendMBSpy = sinon.spy(eventTracking, 'sendMB')
     sendMBSpy = sinon.spy(eventTracking, 'sendMB')
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     sendMBSpy.restore()
     sendMBSpy.restore()
   })
   })
 
 

+ 10 - 10
services/web/test/frontend/features/project-list/components/sidebar/add-affiliation.test.tsx

@@ -14,11 +14,11 @@ describe('Add affiliation widget', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders the component', async function () {
   it('renders the component', async function () {
@@ -27,8 +27,8 @@ describe('Add affiliation widget', function () {
 
 
     renderWithProjectListContext(<AddAffiliation />)
     renderWithProjectListContext(<AddAffiliation />)
 
 
-    await fetchMock.flush(true)
-    await waitFor(() => expect(fetchMock.called('/api/project')))
+    await fetchMock.callHistory.flush(true)
+    await waitFor(() => expect(fetchMock.callHistory.called('/api/project')))
 
 
     screen.getByText(/are you affiliated with an institution/i)
     screen.getByText(/are you affiliated with an institution/i)
     const addAffiliationLink = screen.getByRole('button', {
     const addAffiliationLink = screen.getByRole('button', {
@@ -43,8 +43,8 @@ describe('Add affiliation widget', function () {
 
 
     renderWithProjectListContext(<AddAffiliation />)
     renderWithProjectListContext(<AddAffiliation />)
 
 
-    await fetchMock.flush(true)
-    await waitFor(() => expect(fetchMock.called('/api/project')))
+    await fetchMock.callHistory.flush(true)
+    await waitFor(() => expect(fetchMock.callHistory.called('/api/project')))
 
 
     validateNonExistence()
     validateNonExistence()
   })
   })
@@ -57,8 +57,8 @@ describe('Add affiliation widget', function () {
       projects: [],
       projects: [],
     })
     })
 
 
-    await fetchMock.flush(true)
-    await waitFor(() => expect(fetchMock.called('/api/project')))
+    await fetchMock.callHistory.flush(true)
+    await waitFor(() => expect(fetchMock.callHistory.called('/api/project')))
 
 
     validateNonExistence()
     validateNonExistence()
   })
   })
@@ -69,8 +69,8 @@ describe('Add affiliation widget', function () {
 
 
     renderWithProjectListContext(<AddAffiliation />)
     renderWithProjectListContext(<AddAffiliation />)
 
 
-    await fetchMock.flush(true)
-    await waitFor(() => expect(fetchMock.called('/api/project')))
+    await fetchMock.callHistory.flush(true)
+    await waitFor(() => expect(fetchMock.callHistory.called('/api/project')))
 
 
     validateNonExistence()
     validateNonExistence()
   })
   })

+ 17 - 9
services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx

@@ -29,16 +29,16 @@ describe('<TagsList />', function () {
     })
     })
     fetchMock.post('express:/tag/:tagId/projects', 200)
     fetchMock.post('express:/tag/:tagId/projects', 200)
     fetchMock.post('express:/tag/:tagId/edit', 200)
     fetchMock.post('express:/tag/:tagId/edit', 200)
-    fetchMock.delete('express:/tag/:tagId', 200)
+    fetchMock.delete('express:/tag/:tagId', 200, { name: 'delete tag' })
 
 
     renderWithProjectListContext(<TagsList />)
     renderWithProjectListContext(<TagsList />)
 
 
-    await fetchMock.flush(true)
-    await waitFor(() => expect(fetchMock.called('/api/project')))
+    await fetchMock.callHistory.flush(true)
+    await waitFor(() => expect(fetchMock.callHistory.called('/api/project')))
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('displays the tags list', function () {
   it('displays the tags list', function () {
@@ -146,7 +146,9 @@ describe('<TagsList />', function () {
 
 
       await fireEvent.click(createButton)
       await fireEvent.click(createButton)
 
 
-      await waitFor(() => expect(fetchMock.called(`/tag`)).to.be.true)
+      await waitFor(
+        () => expect(fetchMock.callHistory.called(`/tag`)).to.be.true
+      )
 
 
       expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
       expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
 
 
@@ -237,7 +239,9 @@ describe('<TagsList />', function () {
 
 
       await fireEvent.click(saveButton)
       await fireEvent.click(saveButton)
 
 
-      await waitFor(() => expect(fetchMock.called(`/tag/abc123def456/rename`)))
+      await waitFor(() =>
+        expect(fetchMock.callHistory.called(`/tag/abc123def456/rename`))
+      )
 
 
       expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
       expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
 
 
@@ -278,7 +282,9 @@ describe('<TagsList />', function () {
       const deleteButton = within(modal).getByRole('button', { name: 'Delete' })
       const deleteButton = within(modal).getByRole('button', { name: 'Delete' })
       await fireEvent.click(deleteButton)
       await fireEvent.click(deleteButton)
 
 
-      await waitFor(() => expect(fetchMock.called(`/tag/bcd234efg567`)))
+      await waitFor(() =>
+        expect(fetchMock.callHistory.called(`/tag/bcd234efg567`))
+      )
 
 
       expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
       expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
       expect(
       expect(
@@ -289,13 +295,15 @@ describe('<TagsList />', function () {
     })
     })
 
 
     it('a failed request displays an error message', async function () {
     it('a failed request displays an error message', async function () {
-      fetchMock.delete('express:/tag/:tagId', 500, { overwriteRoutes: true })
+      fetchMock.modifyRoute('delete tag', { response: { status: 500 } })
 
 
       const modal = screen.getAllByRole('dialog', { hidden: false })[0]
       const modal = screen.getAllByRole('dialog', { hidden: false })[0]
       const deleteButton = within(modal).getByRole('button', { name: 'Delete' })
       const deleteButton = within(modal).getByRole('button', { name: 'Delete' })
       await fireEvent.click(deleteButton)
       await fireEvent.click(deleteButton)
 
 
-      await waitFor(() => expect(fetchMock.called(`/tag/bcd234efg567`)))
+      await waitFor(() =>
+        expect(fetchMock.callHistory.called(`/tag/bcd234efg567`))
+      )
 
 
       within(modal).getByText('Sorry, something went wrong')
       within(modal).getByText('Sorry, something went wrong')
     })
     })

+ 5 - 5
services/web/test/frontend/features/project-list/components/system-messages.test.tsx

@@ -5,12 +5,12 @@ import SystemMessages from '@/shared/components/system-messages'
 
 
 describe('<SystemMessages />', function () {
 describe('<SystemMessages />', function () {
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     localStorage.clear()
     localStorage.clear()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     localStorage.clear()
     localStorage.clear()
   })
   })
 
 
@@ -22,7 +22,7 @@ describe('<SystemMessages />', function () {
     fetchMock.get(/\/system\/messages/, [data])
     fetchMock.get(/\/system\/messages/, [data])
     render(<SystemMessages />)
     render(<SystemMessages />)
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
     screen.getByText(data.content)
     screen.getByText(data.content)
     expect(screen.queryByRole('button', { name: /close/i })).to.be.null
     expect(screen.queryByRole('button', { name: /close/i })).to.be.null
@@ -36,7 +36,7 @@ describe('<SystemMessages />', function () {
     fetchMock.get(/\/system\/messages/, [data])
     fetchMock.get(/\/system\/messages/, [data])
     render(<SystemMessages />)
     render(<SystemMessages />)
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
     screen.getByText(data.content)
     screen.getByText(data.content)
     const closeBtn = screen.getByRole('button', { name: /close/i })
     const closeBtn = screen.getByRole('button', { name: /close/i })
@@ -60,7 +60,7 @@ describe('<SystemMessages />', function () {
     window.metaAttributesCache.set('ol-currentUrl', currentUrl)
     window.metaAttributesCache.set('ol-currentUrl', currentUrl)
     render(<SystemMessages />)
     render(<SystemMessages />)
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
     const link = screen.getByRole('link', { name: /click here/i })
     const link = screen.getByRole('link', { name: /click here/i })
     expect(link.getAttribute('href')).to.equal(`${data.url}${currentUrl}`)
     expect(link.getAttribute('href')).to.equal(`${data.url}${currentUrl}`)

+ 5 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/archive-project-button.test.tsx

@@ -67,8 +67,11 @@ describe('<ArchiveProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(archiveProjectMock.called(`/project/${project.id}/archive`)).to
-          .be.true
+        expect(
+          archiveProjectMock.callHistory.called(
+            `/project/${project.id}/archive`
+          )
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 1 - 1
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx

@@ -28,7 +28,7 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     locationStub.restore()
     locationStub.restore()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     sendMBSpy.restore()
     sendMBSpy.restore()
   })
   })
 
 

+ 5 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/copy-project-button.test.tsx

@@ -66,8 +66,11 @@ describe('<CopyProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(copyProjectMock.called(`/project/${copyableProject.id}/clone`))
-          .to.be.true
+        expect(
+          copyProjectMock.callHistory.called(
+            `/project/${copyableProject.id}/clone`
+          )
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 2 - 1
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/delete-project-button.test.tsx

@@ -69,7 +69,8 @@ describe('<DeleteProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(deleteProjectMock.called(`/project/${project.id}`)).to.be.true
+        expect(deleteProjectMock.callHistory.called(`/project/${project.id}`))
+          .to.be.true
     )
     )
   })
   })
 })
 })

+ 3 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/leave-project-button.test.tsx

@@ -75,8 +75,9 @@ describe('<LeaveProjectButtton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(leaveProjectMock.called(`/project/${project.id}/leave`)).to.be
-          .true
+        expect(
+          leaveProjectMock.callHistory.called(`/project/${project.id}/leave`)
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 3 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/rename-project-button.test.tsx

@@ -63,8 +63,9 @@ describe('<RenameProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(renameProjectMock.called(`/project/${project.id}/rename`)).to.be
-          .true
+        expect(
+          renameProjectMock.callHistory.called(`/project/${project.id}/rename`)
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 3 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/trash-project-button.test.tsx

@@ -57,8 +57,9 @@ describe('<TrashProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(trashProjectMock.called(`/project/${project.id}/trash`)).to.be
-          .true
+        expect(
+          trashProjectMock.callHistory.called(`/project/${project.id}/trash`)
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 5 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/unarchive-project-button.test.tsx

@@ -57,8 +57,11 @@ describe('<UnarchiveProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(unarchiveProjectMock.called(`/project/${project.id}/archive`)).to
-          .be.true
+        expect(
+          unarchiveProjectMock.callHistory.called(
+            `/project/${project.id}/archive`
+          )
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 3 - 2
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/untrash-project-button.test.tsx

@@ -49,8 +49,9 @@ describe('<UntrashProjectButton />', function () {
 
 
     await waitFor(
     await waitFor(
       () =>
       () =>
-        expect(untrashProjectMock.called(`/project/${project.id}/trash`)).to.be
-          .true
+        expect(
+          untrashProjectMock.callHistory.called(`/project/${project.id}/trash`)
+        ).to.be.true
     )
     )
   })
   })
 })
 })

+ 6 - 3
services/web/test/frontend/features/project-list/components/table/cells/inline-tags.test.tsx

@@ -60,9 +60,12 @@ describe('<InlineTags />', function () {
     await fireEvent.click(removeButton)
     await fireEvent.click(removeButton)
     await waitFor(() =>
     await waitFor(() =>
       expect(
       expect(
-        fetchMock.called(`/tag/789fff789fff/project/${copyableProject.id}`, {
-          method: 'DELETE',
-        })
+        fetchMock.callHistory.called(
+          `/tag/789fff789fff/project/${copyableProject.id}`,
+          {
+            method: 'DELETE',
+          }
+        )
       )
       )
     )
     )
     expect(screen.queryByText('My Test Tag')).to.not.exist
     expect(screen.queryByText('My Test Tag')).to.not.exist

+ 7 - 7
services/web/test/frontend/features/project-list/components/table/project-list-table.test.tsx

@@ -11,11 +11,11 @@ describe('<ProjectListTable />', function () {
   beforeEach(function () {
   beforeEach(function () {
     window.metaAttributesCache.set('ol-tags', [])
     window.metaAttributesCache.set('ol-tags', [])
     window.metaAttributesCache.set('ol-user_id', userId)
     window.metaAttributesCache.set('ol-user_id', userId)
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders the table', function () {
   it('renders the table', function () {
@@ -65,7 +65,7 @@ describe('<ProjectListTable />', function () {
     this.timeout(10000)
     this.timeout(10000)
 
 
     renderWithProjectListContext(<ProjectListTable />)
     renderWithProjectListContext(<ProjectListTable />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
     const rows = screen.getAllByRole('row')
     const rows = screen.getAllByRole('row')
     rows.shift() // remove first row since it's the header
     rows.shift() // remove first row since it's the header
@@ -138,7 +138,7 @@ describe('<ProjectListTable />', function () {
 
 
   it('selects all projects when header checkbox checked', async function () {
   it('selects all projects when header checkbox checked', async function () {
     renderWithProjectListContext(<ProjectListTable />)
     renderWithProjectListContext(<ProjectListTable />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     const checkbox = screen.getByLabelText('Select all projects')
     const checkbox = screen.getByLabelText('Select all projects')
     fireEvent.click(checkbox)
     fireEvent.click(checkbox)
     const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
     const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
@@ -149,7 +149,7 @@ describe('<ProjectListTable />', function () {
 
 
   it('unselects all projects when select all checkbox uchecked', async function () {
   it('unselects all projects when select all checkbox uchecked', async function () {
     renderWithProjectListContext(<ProjectListTable />)
     renderWithProjectListContext(<ProjectListTable />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     const checkbox = screen.getByLabelText('Select all projects')
     const checkbox = screen.getByLabelText('Select all projects')
     fireEvent.click(checkbox)
     fireEvent.click(checkbox)
     fireEvent.click(checkbox)
     fireEvent.click(checkbox)
@@ -160,7 +160,7 @@ describe('<ProjectListTable />', function () {
 
 
   it('unselects select all projects checkbox when one project is unchecked', async function () {
   it('unselects select all projects checkbox when one project is unchecked', async function () {
     renderWithProjectListContext(<ProjectListTable />)
     renderWithProjectListContext(<ProjectListTable />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     const checkbox = screen.getByLabelText('Select all projects')
     const checkbox = screen.getByLabelText('Select all projects')
     fireEvent.click(checkbox)
     fireEvent.click(checkbox)
     let allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
     let allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
@@ -173,7 +173,7 @@ describe('<ProjectListTable />', function () {
 
 
   it('only checks the checked project', async function () {
   it('only checks the checked project', async function () {
     renderWithProjectListContext(<ProjectListTable />)
     renderWithProjectListContext(<ProjectListTable />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     const checkbox = screen.getByLabelText(`Select ${currentProjects[0].name}`)
     const checkbox = screen.getByLabelText(`Select ${currentProjects[0].name}`)
     fireEvent.click(checkbox)
     fireEvent.click(checkbox)
     const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
     const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')

+ 1 - 1
services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx

@@ -65,7 +65,7 @@ describe('<ProjectTools />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     window.metaAttributesCache.clear()
     window.metaAttributesCache.clear()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('does not show the Rename option for a project owned by a different user', function () {
   it('does not show the Rename option for a project owned by a different user', function () {

+ 4 - 2
services/web/test/frontend/features/project-list/components/table/project-tools/rename-project-modal.test.tsx

@@ -58,7 +58,9 @@ describe('<RenameProjectModal />', function () {
     await waitFor(
     await waitFor(
       () =>
       () =>
         expect(
         expect(
-          renameProjectMock.called(`/project/${currentProjects[0].id}/rename`)
+          renameProjectMock.callHistory.called(
+            `/project/${currentProjects[0].id}/rename`
+          )
         ).to.be.true
         ).to.be.true
     )
     )
   })
   })
@@ -88,7 +90,7 @@ describe('<RenameProjectModal />', function () {
     const submitButton = within(modal).getByText('Rename') as HTMLButtonElement
     const submitButton = within(modal).getByText('Rename') as HTMLButtonElement
     fireEvent.click(submitButton)
     fireEvent.click(submitButton)
 
 
-    await waitFor(() => expect(postRenameMock.called()).to.be.true)
+    await waitFor(() => expect(postRenameMock.callHistory.called()).to.be.true)
 
 
     screen.getByText('Something went wrong. Please try again.')
     screen.getByText('Something went wrong. Please try again.')
   })
   })

+ 1 - 1
services/web/test/frontend/features/project-list/helpers/render-with-context.tsx

@@ -58,5 +58,5 @@ export function renderWithProjectListContext(
 }
 }
 
 
 export function resetProjectListContextFetch() {
 export function resetProjectListContextFetch() {
-  fetchMock.reset()
+  fetchMock.removeRoutes().clearHistory()
 }
 }

+ 27 - 27
services/web/test/frontend/features/settings/components/account-info-section.test.tsx

@@ -29,7 +29,7 @@ describe('<AccountInfoSection />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('submits all inputs', async function () {
   it('submits all inputs', async function () {
@@ -50,14 +50,14 @@ describe('<AccountInfoSection />', function () {
         name: 'Update',
         name: 'Update',
       })
       })
     )
     )
-    expect(updateMock.called()).to.be.true
-    expect(JSON.parse(updateMock.lastCall()![1]!.body as string)).to.deep.equal(
-      {
-        email: 'john@watson.co.uk',
-        first_name: 'John',
-        last_name: 'Watson',
-      }
-    )
+    expect(updateMock.callHistory.called()).to.be.true
+    expect(
+      JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
+    ).to.deep.equal({
+      email: 'john@watson.co.uk',
+      first_name: 'John',
+      last_name: 'Watson',
+    })
   })
   })
 
 
   it('disables button on invalid email', async function () {
   it('disables button on invalid email', async function () {
@@ -74,7 +74,7 @@ describe('<AccountInfoSection />', function () {
     expect(button.disabled).to.be.true
     expect(button.disabled).to.be.true
     fireEvent.click(button)
     fireEvent.click(button)
 
 
-    expect(updateMock.called()).to.be.false
+    expect(updateMock.callHistory.called()).to.be.false
   })
   })
 
 
   it('shows inflight state and success message', async function () {
   it('shows inflight state and success message', async function () {
@@ -156,12 +156,12 @@ describe('<AccountInfoSection />', function () {
         name: 'Update',
         name: 'Update',
       })
       })
     )
     )
-    expect(JSON.parse(updateMock.lastCall()![1]!.body as string)).to.deep.equal(
-      {
-        first_name: 'Sherlock',
-        last_name: 'Holmes',
-      }
-    )
+    expect(
+      JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
+    ).to.deep.equal({
+      first_name: 'Sherlock',
+      last_name: 'Holmes',
+    })
   })
   })
 
 
   it('disables email input', async function () {
   it('disables email input', async function () {
@@ -187,12 +187,12 @@ describe('<AccountInfoSection />', function () {
         name: 'Update',
         name: 'Update',
       })
       })
     )
     )
-    expect(JSON.parse(updateMock.lastCall()![1]!.body as string)).to.deep.equal(
-      {
-        first_name: 'Sherlock',
-        last_name: 'Holmes',
-      }
-    )
+    expect(
+      JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
+    ).to.deep.equal({
+      first_name: 'Sherlock',
+      last_name: 'Holmes',
+    })
   })
   })
 
 
   it('disables names input', async function () {
   it('disables names input', async function () {
@@ -215,10 +215,10 @@ describe('<AccountInfoSection />', function () {
         name: 'Update',
         name: 'Update',
       })
       })
     )
     )
-    expect(JSON.parse(updateMock.lastCall()![1]!.body as string)).to.deep.equal(
-      {
-        email: 'sherlock@holmes.co.uk',
-      }
-    )
+    expect(
+      JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
+    ).to.deep.equal({
+      email: 'sherlock@holmes.co.uk',
+    })
   })
   })
 })
 })

+ 20 - 13
services/web/test/frontend/features/settings/components/emails/add-email-input.test.tsx

@@ -19,7 +19,7 @@ describe('<AddEmailInput/>', function () {
 
 
   beforeEach(function () {
   beforeEach(function () {
     clearDomainCache()
     clearDomainCache()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('on initial render', function () {
   describe('on initial render', function () {
@@ -69,7 +69,7 @@ describe('<AddEmailInput/>', function () {
     })
     })
 
 
     it('should not make any request for institution domains', function () {
     it('should not make any request for institution domains', function () {
-      expect(fetchMock.called()).to.be.false
+      expect(fetchMock.callHistory.called()).to.be.false
     })
     })
 
 
     it('should submit on Enter if email looks valid', async function () {
     it('should submit on Enter if email looks valid', async function () {
@@ -118,11 +118,18 @@ describe('<AddEmailInput/>', function () {
     })
     })
 
 
     describe('when there is a domain match', function () {
     describe('when there is a domain match', function () {
-      beforeEach(function () {
+      beforeEach(async function () {
         fetchMock.get('express:/institutions/domains', testInstitutionData)
         fetchMock.get('express:/institutions/domains', testInstitutionData)
         fireEvent.change(screen.getByTestId('affiliations-email'), {
         fireEvent.change(screen.getByTestId('affiliations-email'), {
           target: { value: 'user@d' },
           target: { value: 'user@d' },
         })
         })
+        // Wait for component to process the change and update the shadow input
+        await waitFor(() => {
+          const shadowInput = screen.getByTestId(
+            'affiliations-email-shadow'
+          ) as HTMLInputElement
+          expect(shadowInput.value).to.equal('user@domain.edu')
+        })
       })
       })
 
 
       it('should render the text being typed along with the suggestion', async function () {
       it('should render the text being typed along with the suggestion', async function () {
@@ -146,7 +153,7 @@ describe('<AddEmailInput/>', function () {
         fireEvent.change(screen.getByTestId('affiliations-email'), {
         fireEvent.change(screen.getByTestId('affiliations-email'), {
           target: { value: 'user@domain.edu' },
           target: { value: 'user@domain.edu' },
         })
         })
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
         expect(
         expect(
           onChangeStub.calledWith(
           onChangeStub.calledWith(
             'user@domain.edu',
             'user@domain.edu',
@@ -231,7 +238,7 @@ describe('<AddEmailInput/>', function () {
       })
       })
 
 
       it('should cache the result and skip subsequent requests', async function () {
       it('should cache the result and skip subsequent requests', async function () {
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
 
 
         // clear input
         // clear input
         fireEvent.change(screen.getByTestId('affiliations-email'), {
         fireEvent.change(screen.getByTestId('affiliations-email'), {
@@ -242,7 +249,7 @@ describe('<AddEmailInput/>', function () {
           target: { value: 'user@d' },
           target: { value: 'user@d' },
         })
         })
 
 
-        expect(fetchMock.called()).to.be.false
+        expect(fetchMock.callHistory.called()).to.be.false
         expect(onChangeStub.calledWith('user@d')).to.equal(true)
         expect(onChangeStub.calledWith('user@d')).to.equal(true)
         await waitFor(() => {
         await waitFor(() => {
           const shadowInput = screen.getByTestId(
           const shadowInput = screen.getByTestId(
@@ -258,7 +265,7 @@ describe('<AddEmailInput/>', function () {
 
 
       afterEach(function () {
       afterEach(function () {
         clearDomainCache()
         clearDomainCache()
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
       })
       })
 
 
       it('should not render the suggestion with blocked domain', async function () {
       it('should not render the suggestion with blocked domain', async function () {
@@ -269,7 +276,7 @@ describe('<AddEmailInput/>', function () {
         fireEvent.change(screen.getByTestId('affiliations-email'), {
         fireEvent.change(screen.getByTestId('affiliations-email'), {
           target: { value: `user@${blockedDomain.split('.')[0]}` },
           target: { value: `user@${blockedDomain.split('.')[0]}` },
         })
         })
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
         expect(screen.queryByText(`user@${blockedDomain}`)).to.be.null
         expect(screen.queryByText(`user@${blockedDomain}`)).to.be.null
       })
       })
 
 
@@ -286,7 +293,7 @@ describe('<AddEmailInput/>', function () {
             value: `user@subdomain.${blockedDomain.split('.')[0]}`,
             value: `user@subdomain.${blockedDomain.split('.')[0]}`,
           },
           },
         })
         })
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
         expect(screen.queryByText(`user@subdomain.${blockedDomain}`)).to.be.null
         expect(screen.queryByText(`user@subdomain.${blockedDomain}`)).to.be.null
       })
       })
     })
     })
@@ -307,7 +314,7 @@ describe('<AddEmailInput/>', function () {
 
 
         // make sure the next suggestions are delayed
         // make sure the next suggestions are delayed
         clearDomainCache()
         clearDomainCache()
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get('express:/institutions/domains', 200, { delay: 1000 })
         fetchMock.get('express:/institutions/domains', 200, { delay: 1000 })
       })
       })
 
 
@@ -354,7 +361,7 @@ describe('<AddEmailInput/>', function () {
       })
       })
 
 
       // subsequent requests fail
       // subsequent requests fail
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
       fetchMock.get('express:/institutions/domains', 500)
       fetchMock.get('express:/institutions/domains', 500)
     })
     })
 
 
@@ -373,7 +380,7 @@ describe('<AddEmailInput/>', function () {
         expect(shadowInput.value).to.equal('')
         expect(shadowInput.value).to.equal('')
       })
       })
 
 
-      expect(fetchMock.called()).to.be.true // ensures `domainCache` hasn't been hit
+      expect(fetchMock.callHistory.called()).to.be.true // ensures `domainCache` hasn't been hit
     })
     })
   })
   })
 
 
@@ -384,7 +391,7 @@ describe('<AddEmailInput/>', function () {
       fireEvent.change(screen.getByTestId('affiliations-email'), {
       fireEvent.change(screen.getByTestId('affiliations-email'), {
         target: { value: 'user@other' },
         target: { value: 'user@other' },
       })
       })
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       const shadowInput = screen.getByTestId(
       const shadowInput = screen.getByTestId(
         'affiliations-email-shadow'
         'affiliations-email-shadow'
       ) as HTMLInputElement
       ) as HTMLInputElement

+ 5 - 5
services/web/test/frontend/features/settings/components/emails/emails-row.test.tsx

@@ -37,7 +37,7 @@ describe('<EmailsRow/>', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('with unaffiliated email data', function () {
   describe('with unaffiliated email data', function () {
@@ -86,9 +86,9 @@ describe('<EmailsRow/>', function () {
 
 
     describe('when the email is not yet linked to the institution', function () {
     describe('when the email is not yet linked to the institution', function () {
       beforeEach(async function () {
       beforeEach(async function () {
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [affiliatedEmail, unconfirmedUserData])
         fetchMock.get(/\/user\/emails/, [affiliatedEmail, unconfirmedUserData])
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
       })
       })
 
 
       it('prompts the user to link to their institutional account', function () {
       it('prompts the user to link to their institutional account', function () {
@@ -103,9 +103,9 @@ describe('<EmailsRow/>', function () {
     describe('when the email is already linked to the institution', function () {
     describe('when the email is already linked to the institution', function () {
       beforeEach(async function () {
       beforeEach(async function () {
         affiliatedEmail.samlProviderId = '1'
         affiliatedEmail.samlProviderId = '1'
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [affiliatedEmail, unconfirmedUserData])
         fetchMock.get(/\/user\/emails/, [affiliatedEmail, unconfirmedUserData])
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
       })
       })
 
 
       it('prompts the user to login using their institutional account', function () {
       it('prompts the user to login using their institutional account', function () {

+ 4 - 4
services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx

@@ -37,11 +37,11 @@ describe('email actions - make primary', function () {
     Object.assign(getMeta('ol-ExposedSettings'), {
     Object.assign(getMeta('ol-ExposedSettings'), {
       hasAffiliationsFeature: true,
       hasAffiliationsFeature: true,
     })
     })
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('disabled `make primary` button', function () {
   describe('disabled `make primary` button', function () {
@@ -236,11 +236,11 @@ describe('email actions - delete', function () {
     Object.assign(getMeta('ol-ExposedSettings'), {
     Object.assign(getMeta('ol-ExposedSettings'), {
       hasAffiliationsFeature: true,
       hasAffiliationsFeature: true,
     })
     })
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows loader when deleting and removes the row', async function () {
   it('shows loader when deleting and removes the row', async function () {

+ 31 - 27
services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx

@@ -56,7 +56,7 @@ const institutionDomainData = [
 ] as const
 ] as const
 
 
 function resetFetchMock() {
 function resetFetchMock() {
-  fetchMock.reset()
+  fetchMock.removeRoutes().clearHistory()
   fetchMock.get('express:/institutions/domains', [])
   fetchMock.get('express:/institutions/domains', [])
 }
 }
 
 
@@ -80,7 +80,7 @@ describe('<EmailsSection />', function () {
       hasSamlFeature: true,
       hasSamlFeature: true,
       samlInitPath: 'saml/init',
       samlInitPath: 'saml/init',
     })
     })
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
@@ -97,7 +97,7 @@ describe('<EmailsSection />', function () {
   it('renders input', async function () {
   it('renders input', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     render(<EmailsSection />)
     render(<EmailsSection />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: /add another email/i,
       name: /add another email/i,
@@ -112,7 +112,7 @@ describe('<EmailsSection />', function () {
     fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
     fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
     fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
     fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
     render(<EmailsSection />)
     render(<EmailsSection />)
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: /add another email/i,
       name: /add another email/i,
@@ -190,7 +190,7 @@ describe('<EmailsSection />', function () {
       { name: /add another email/i }
       { name: /add another email/i }
     )
     )
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
     fetchMock
     fetchMock
       .get('/user/emails?ensureAffiliation=true', [userEmailData])
       .get('/user/emails?ensureAffiliation=true', [userEmailData])
@@ -233,7 +233,7 @@ describe('<EmailsSection />', function () {
       { name: /add another email/i }
       { name: /add another email/i }
     )
     )
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
     fetchMock
     fetchMock
       .get('/user/emails?ensureAffiliation=true', [])
       .get('/user/emails?ensureAffiliation=true', [])
@@ -271,8 +271,8 @@ describe('<EmailsSection />', function () {
       name: /add another email/i,
       name: /add another email/i,
     })
     })
 
 
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
     fetchMock.get('express:/institutions/domains', institutionDomainData)
     fetchMock.get('express:/institutions/domains', institutionDomainData)
 
 
     await userEvent.click(button)
     await userEvent.click(button)
@@ -295,7 +295,7 @@ describe('<EmailsSection />', function () {
       name: /add another email/i,
       name: /add another email/i,
     })
     })
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
 
 
     await userEvent.click(button)
     await userEvent.click(button)
@@ -330,7 +330,7 @@ describe('<EmailsSection />', function () {
 
 
     expect(universityInput.disabled).to.be.false
     expect(universityInput.disabled).to.be.false
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
 
 
     // Select the university from dropdown
     // Select the university from dropdown
@@ -364,9 +364,11 @@ describe('<EmailsSection />', function () {
       })
       })
     )
     )
 
 
-    const [[, request]] = fetchMock.calls(/\/user\/emails/)
+    const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
 
 
-    expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.include({
+    expect(
+      JSON.parse(request?.options.body?.toString() || '{}')
+    ).to.deep.include({
       email: userEmailData.email,
       email: userEmailData.email,
       university: {
       university: {
         id: userEmailData.affiliation?.institution.id,
         id: userEmailData.affiliation?.institution.id,
@@ -393,7 +395,7 @@ describe('<EmailsSection />', function () {
       name: /add another email/i,
       name: /add another email/i,
     })
     })
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
 
 
     fetchMock.get('/institutions/list?country_code=de', [
     fetchMock.get('/institutions/list?country_code=de', [
@@ -446,7 +448,7 @@ describe('<EmailsSection />', function () {
       name: /add another email/i,
       name: /add another email/i,
     })
     })
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
 
 
     await userEvent.click(button)
     await userEvent.click(button)
@@ -481,7 +483,7 @@ describe('<EmailsSection />', function () {
 
 
     expect(universityInput.disabled).to.be.false
     expect(universityInput.disabled).to.be.false
 
 
-    await fetchMock.flush(true)
+    await fetchMock.callHistory.flush(true)
     resetFetchMock()
     resetFetchMock()
 
 
     // Enter the university manually
     // Enter the university manually
@@ -516,9 +518,11 @@ describe('<EmailsSection />', function () {
 
 
     await confirmCodeForEmail(userEmailData.email)
     await confirmCodeForEmail(userEmailData.email)
 
 
-    const [[, request]] = fetchMock.calls(/\/user\/emails/)
+    const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
 
 
-    expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.include({
+    expect(
+      JSON.parse(request?.options.body?.toString() || '{}')
+    ).to.deep.include({
       email: userEmailData.email,
       email: userEmailData.email,
       university: {
       university: {
         name: newUniversity,
         name: newUniversity,
@@ -554,8 +558,8 @@ describe('<EmailsSection />', function () {
       name: /add another email/i,
       name: /add another email/i,
     })
     })
 
 
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
     fetchMock.get(
     fetchMock.get(
       `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
       `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
       institutionDomainDataCopy
       institutionDomainDataCopy
@@ -569,8 +573,8 @@ describe('<EmailsSection />', function () {
     )
     )
 
 
     await userEvent.keyboard('{Tab}')
     await userEvent.keyboard('{Tab}')
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
 
 
     expect(
     expect(
       screen.queryByRole('textbox', {
       screen.queryByRole('textbox', {
@@ -627,8 +631,8 @@ describe('<EmailsSection />', function () {
       name: /add another email/i,
       name: /add another email/i,
     })
     })
 
 
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
     fetchMock.get(
     fetchMock.get(
       `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
       `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
       institutionDomainDataCopy
       institutionDomainDataCopy
@@ -642,8 +646,8 @@ describe('<EmailsSection />', function () {
     )
     )
 
 
     await userEvent.keyboard('{Tab}')
     await userEvent.keyboard('{Tab}')
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
 
 
     screen.getByText(institutionDomainDataCopy[0].university.name)
     screen.getByText(institutionDomainDataCopy[0].university.name)
 
 
@@ -679,8 +683,8 @@ describe('<EmailsSection />', function () {
 
 
     await confirmCodeForEmail('user@autocomplete.edu')
     await confirmCodeForEmail('user@autocomplete.edu')
 
 
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
 
 
     screen.getByText(userEmailDataCopy.affiliation.institution.name, {
     screen.getByText(userEmailDataCopy.affiliation.institution.name, {
       exact: false,
       exact: false,

+ 11 - 13
services/web/test/frontend/features/settings/components/emails/emails-section-institution-and-role.test.tsx

@@ -79,12 +79,14 @@ describe('user role and institution', function () {
     Object.assign(getMeta('ol-ExposedSettings'), {
     Object.assign(getMeta('ol-ExposedSettings'), {
       hasAffiliationsFeature: true,
       hasAffiliationsFeature: true,
     })
     })
-    fetchMock.reset()
-    fetchMock.get('/user/emails?ensureAffiliation=true', [])
+    fetchMock.removeRoutes().clearHistory()
+    fetchMock.get('/user/emails?ensureAffiliation=true', [], {
+      name: 'get user emails',
+    })
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders affiliation name with add role/department button', function () {
   it('renders affiliation name with add role/department button', function () {
@@ -122,13 +124,11 @@ describe('user role and institution', function () {
 
 
   it('fetches institution data and replaces departments dropdown on add/change', async function () {
   it('fetches institution data and replaces departments dropdown on add/change', async function () {
     const userEmailData = userData1
     const userEmailData = userData1
-    fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData], {
-      overwriteRoutes: true,
-    })
+    fetchMock.modifyRoute('get user emails', { response: [userEmailData] })
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
 
 
     const fakeDepartment = 'Fake department'
     const fakeDepartment = 'Fake department'
     const institution = userEmailData.affiliation.institution
     const institution = userEmailData.affiliation.institution
@@ -143,8 +143,8 @@ describe('user role and institution', function () {
       screen.getByRole('button', { name: /add role and department/i })
       screen.getByRole('button', { name: /add role and department/i })
     )
     )
 
 
-    await fetchMock.flush(true)
-    fetchMock.reset()
+    await fetchMock.callHistory.flush(true)
+    fetchMock.removeRoutes().clearHistory()
 
 
     fireEvent.click(screen.getByRole('textbox', { name: /department/i }))
     fireEvent.click(screen.getByRole('textbox', { name: /department/i }))
 
 
@@ -153,9 +153,7 @@ describe('user role and institution', function () {
 
 
   it('adds new role and department', async function () {
   it('adds new role and department', async function () {
     fetchMock
     fetchMock
-      .get('/user/emails?ensureAffiliation=true', [userData1], {
-        overwriteRoutes: true,
-      })
+      .modifyRoute('get user emails', { response: [userData1] })
       .get(/\/institutions\/list/, { departments: [] })
       .get(/\/institutions\/list/, { departments: [] })
       .post('/user/emails/endorse', 200)
       .post('/user/emails/endorse', 200)
     render(<EmailsSection />)
     render(<EmailsSection />)

+ 2 - 2
services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx

@@ -22,11 +22,11 @@ describe('<EmailsSection />', function () {
     Object.assign(getMeta('ol-ExposedSettings'), {
     Object.assign(getMeta('ol-ExposedSettings'), {
       hasAffiliationsFeature: true,
       hasAffiliationsFeature: true,
     })
     })
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders translated heading', function () {
   it('renders translated heading', function () {

+ 4 - 4
services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx

@@ -42,7 +42,7 @@ describe('<ReconfirmationInfo/>', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     this.locationStub.restore()
     this.locationStub.restore()
   })
   })
 
 
@@ -145,13 +145,13 @@ describe('<ReconfirmationInfo/>', function () {
         await waitFor(() => {
         await waitFor(() => {
           expect(confirmButton.disabled).to.be.true
           expect(confirmButton.disabled).to.be.true
         })
         })
-        expect(fetchMock.called()).to.be.true
+        expect(fetchMock.callHistory.called()).to.be.true
 
 
         // the confirmation text should now be displayed
         // the confirmation text should now be displayed
         await screen.findByText(/Please check your email inbox to confirm/)
         await screen.findByText(/Please check your email inbox to confirm/)
 
 
         // try the resend button
         // try the resend button
-        fetchMock.resetHistory()
+        fetchMock.clearHistory()
         const resendButton = await screen.findByRole('button', {
         const resendButton = await screen.findByRole('button', {
           name: 'Resend confirmation email',
           name: 'Resend confirmation email',
         })
         })
@@ -160,7 +160,7 @@ describe('<ReconfirmationInfo/>', function () {
 
 
         // commented out as it's already gone by this point
         // commented out as it's already gone by this point
         // await screen.findByText(/Sending/)
         // await screen.findByText(/Sending/)
-        expect(fetchMock.called()).to.be.true
+        expect(fetchMock.callHistory.called()).to.be.true
         await waitForElementToBeRemoved(() => screen.getByText('Sending…'))
         await waitForElementToBeRemoved(() => screen.getByText('Sending…'))
         await screen.findByRole('button', {
         await screen.findByRole('button', {
           name: 'Resend confirmation email',
           name: 'Resend confirmation email',

+ 1 - 1
services/web/test/frontend/features/settings/components/leave/modal-content.test.tsx

@@ -12,7 +12,7 @@ describe('<LeaveModalContent />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('disable delete button if form is not valid', function () {
   it('disable delete button if form is not valid', function () {

+ 6 - 6
services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx

@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
 import sinon from 'sinon'
 import sinon from 'sinon'
 import { fireEvent, screen, render, waitFor } from '@testing-library/react'
 import { fireEvent, screen, render, waitFor } from '@testing-library/react'
-import fetchMock, { FetchMockStatic } from 'fetch-mock'
+import fetchMock, { type FetchMock } from 'fetch-mock'
 
 
 import LeaveModalForm from '../../../../../../frontend/js/features/settings/components/leave/modal-form'
 import LeaveModalForm from '../../../../../../frontend/js/features/settings/components/leave/modal-form'
 import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
 import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
@@ -14,7 +14,7 @@ describe('<LeaveModalForm />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('validates form', async function () {
   it('validates form', async function () {
@@ -50,7 +50,7 @@ describe('<LeaveModalForm />', function () {
   describe('submits', async function () {
   describe('submits', async function () {
     let setInFlight: sinon.SinonStub
     let setInFlight: sinon.SinonStub
     let setIsFormValid: sinon.SinonStub
     let setIsFormValid: sinon.SinonStub
-    let deleteMock: FetchMockStatic
+    let deleteMock: FetchMock
     let assignStub: sinon.SinonStub
     let assignStub: sinon.SinonStub
 
 
     beforeEach(function () {
     beforeEach(function () {
@@ -68,7 +68,7 @@ describe('<LeaveModalForm />', function () {
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
       this.locationStub.restore()
       this.locationStub.restore()
     })
     })
 
 
@@ -85,7 +85,7 @@ describe('<LeaveModalForm />', function () {
 
 
       sinon.assert.calledOnce(setInFlight)
       sinon.assert.calledOnce(setInFlight)
       sinon.assert.calledWithMatch(setInFlight, true)
       sinon.assert.calledWithMatch(setInFlight, true)
-      expect(deleteMock.called()).to.be.true
+      expect(deleteMock.callHistory.called()).to.be.true
       await waitFor(() => {
       await waitFor(() => {
         sinon.assert.calledTwice(setInFlight)
         sinon.assert.calledTwice(setInFlight)
         sinon.assert.calledWithMatch(setInFlight, false)
         sinon.assert.calledWithMatch(setInFlight, false)
@@ -105,7 +105,7 @@ describe('<LeaveModalForm />', function () {
 
 
       fireEvent.submit(screen.getByLabelText('Email'))
       fireEvent.submit(screen.getByLabelText('Email'))
 
 
-      expect(deleteMock.called()).to.be.false
+      expect(deleteMock.callHistory.called()).to.be.false
       sinon.assert.notCalled(setInFlight)
       sinon.assert.notCalled(setInFlight)
     })
     })
   })
   })

+ 1 - 1
services/web/test/frontend/features/settings/components/leave/modal.test.tsx

@@ -13,7 +13,7 @@ describe('<LeaveModal />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('closes modal on cancel', async function () {
   it('closes modal on cancel', async function () {

+ 1 - 1
services/web/test/frontend/features/settings/components/leavers-survey-alert.test.tsx

@@ -21,7 +21,7 @@ describe('<LeaversSurveyAlert/>', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('should render before the expiration date', function () {
   it('should render before the expiration date', function () {

+ 1 - 1
services/web/test/frontend/features/settings/components/linking-section.test.tsx

@@ -52,7 +52,7 @@ describe('<LinkingSection />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows header', async function () {
   it('shows header', async function () {

+ 10 - 10
services/web/test/frontend/features/settings/components/password-section.test.tsx

@@ -17,7 +17,7 @@ describe('<PasswordSection />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows password managed externally message', async function () {
   it('shows password managed externally message', async function () {
@@ -45,14 +45,14 @@ describe('<PasswordSection />', function () {
     render(<PasswordSection />)
     render(<PasswordSection />)
     submitValidForm()
     submitValidForm()
 
 
-    expect(updateMock.called()).to.be.true
-    expect(JSON.parse(updateMock.lastCall()![1]!.body as string)).to.deep.equal(
-      {
-        currentPassword: 'foobar',
-        newPassword1: 'barbaz',
-        newPassword2: 'barbaz',
-      }
-    )
+    expect(updateMock.callHistory.called()).to.be.true
+    expect(
+      JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
+    ).to.deep.equal({
+      currentPassword: 'foobar',
+      newPassword1: 'barbaz',
+      newPassword2: 'barbaz',
+    })
   })
   })
 
 
   it('disables button on invalid form', async function () {
   it('disables button on invalid form', async function () {
@@ -64,7 +64,7 @@ describe('<PasswordSection />', function () {
         name: 'Change',
         name: 'Change',
       })
       })
     )
     )
-    expect(updateMock.called()).to.be.false
+    expect(updateMock.callHistory.called()).to.be.false
   })
   })
 
 
   it('validates inputs', async function () {
   it('validates inputs', async function () {

+ 1 - 1
services/web/test/frontend/features/settings/components/security-section.test.tsx

@@ -5,7 +5,7 @@ import fetchMock from 'fetch-mock'
 
 
 describe('<SecuritySection />', function () {
 describe('<SecuritySection />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows Group SSO rows in security section', async function () {
   it('shows Group SSO rows in security section', async function () {

+ 3 - 3
services/web/test/frontend/features/settings/context/sso-context.test.tsx

@@ -35,7 +35,7 @@ describe('SSOContext', function () {
       google: 'google-id',
       google: 'google-id',
     })
     })
     window.metaAttributesCache.set('ol-oauthProviders', mockOauthProviders)
     window.metaAttributesCache.set('ol-oauthProviders', mockOauthProviders)
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('should initialise subscriptions with their linked status', function () {
   it('should initialise subscriptions with their linked status', function () {
@@ -69,13 +69,13 @@ describe('SSOContext', function () {
     it('when the provider is not linked, should do nothing', function () {
     it('when the provider is not linked, should do nothing', function () {
       const { result } = renderSSOContext()
       const { result } = renderSSOContext()
       result.current.unlink('orcid')
       result.current.unlink('orcid')
-      expect(fetchMock.called()).to.be.false
+      expect(fetchMock.callHistory.called()).to.be.false
     })
     })
 
 
     it('supports unmounting the component while the request is inflight', async function () {
     it('supports unmounting the component while the request is inflight', async function () {
       const { result, unmount } = renderSSOContext()
       const { result, unmount } = renderSSOContext()
       result.current.unlink('google')
       result.current.unlink('google')
-      expect(fetchMock.called()).to.be.true
+      expect(fetchMock.callHistory.called()).to.be.true
       unmount()
       unmount()
     })
     })
   })
   })

+ 24 - 20
services/web/test/frontend/features/settings/context/user-email-context.test.tsx

@@ -1,6 +1,8 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
 import { cloneDeep } from 'lodash'
 import { cloneDeep } from 'lodash'
 import { renderHook } from '@testing-library/react-hooks'
 import { renderHook } from '@testing-library/react-hooks'
+import { waitFor } from '@testing-library/react'
+
 import {
 import {
   EmailContextType,
   EmailContextType,
   UserEmailsProvider,
   UserEmailsProvider,
@@ -26,7 +28,7 @@ const renderUserEmailsContext = () =>
 
 
 describe('UserEmailContext', function () {
 describe('UserEmailContext', function () {
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   describe('context bootstrap', function () {
   describe('context bootstrap', function () {
@@ -49,8 +51,8 @@ describe('UserEmailContext', function () {
     it('should load all user emails and update the initialisation state to "success"', async function () {
     it('should load all user emails and update the initialisation state to "success"', async function () {
       fetchMock.get(/\/user\/emails/, fakeUsersData)
       fetchMock.get(/\/user\/emails/, fakeUsersData)
       const { result } = renderUserEmailsContext()
       const { result } = renderUserEmailsContext()
-      await fetchMock.flush(true)
-      expect(fetchMock.calls()).to.have.lengthOf(1)
+      await fetchMock.callHistory.flush(true)
+      expect(fetchMock.callHistory.calls()).to.have.lengthOf(1)
       expect(result.current.state.data.byId).to.deep.equal({
       expect(result.current.state.data.byId).to.deep.equal({
         'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
         'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
         'baz@overleaf.com': unconfirmedUserData,
         'baz@overleaf.com': unconfirmedUserData,
@@ -66,10 +68,12 @@ describe('UserEmailContext', function () {
     it('when loading user email fails, it should update the initialisation state to "failed"', async function () {
     it('when loading user email fails, it should update the initialisation state to "failed"', async function () {
       fetchMock.get(/\/user\/emails/, 500)
       fetchMock.get(/\/user\/emails/, 500)
       const { result } = renderUserEmailsContext()
       const { result } = renderUserEmailsContext()
-      await fetchMock.flush()
+      await fetchMock.callHistory.flush()
 
 
-      expect(result.current.isInitializing).to.equal(false)
-      expect(result.current.isInitializingError).to.equal(true)
+      await waitFor(() => {
+        expect(result.current.isInitializing).to.equal(false)
+        expect(result.current.isInitializingError).to.equal(true)
+      })
     })
     })
 
 
     describe('state.isLoading', function () {
     describe('state.isLoading', function () {
@@ -94,12 +98,12 @@ describe('UserEmailContext', function () {
       fetchMock.get(/\/user\/emails/, fakeUsersData)
       fetchMock.get(/\/user\/emails/, fakeUsersData)
       const value = renderUserEmailsContext()
       const value = renderUserEmailsContext()
       result = value.result
       result = value.result
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
     })
     })
 
 
     describe('getEmails()', function () {
     describe('getEmails()', function () {
       beforeEach(async function () {
       beforeEach(async function () {
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
       })
       })
 
 
       it('should set `isLoading === true`', function () {
       it('should set `isLoading === true`', function () {
@@ -120,7 +124,7 @@ describe('UserEmailContext', function () {
         }
         }
         fetchMock.get(/\/user\/emails/, [emailData])
         fetchMock.get(/\/user\/emails/, [emailData])
         result.current.getEmails()
         result.current.getEmails()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
         expect(result.current.state.data.byId).to.deep.equal({
         expect(result.current.state.data.byId).to.deep.equal({
           'new@email.com': emailData,
           'new@email.com': emailData,
         })
         })
@@ -133,7 +137,7 @@ describe('UserEmailContext', function () {
           { ...professionalUserData, samlProviderId: 'saml_provider_2' },
           { ...professionalUserData, samlProviderId: 'saml_provider_2' },
         ])
         ])
         const { result } = renderUserEmailsContext()
         const { result } = renderUserEmailsContext()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
         expect(result.current.state.data.linkedInstitutionIds).to.deep.equal([
         expect(result.current.state.data.linkedInstitutionIds).to.deep.equal([
           'saml_provider_1',
           'saml_provider_1',
           'saml_provider_2',
           'saml_provider_2',
@@ -257,11 +261,11 @@ describe('UserEmailContext', function () {
         const affiliatedEmail2 = cloneDeep(professionalUserData)
         const affiliatedEmail2 = cloneDeep(professionalUserData)
         affiliatedEmail2.emailHasInstitutionLicence = true
         affiliatedEmail2.emailHasInstitutionLicence = true
 
 
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
         fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
 
 
         result.current.getEmails()
         result.current.getEmails()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         // `resetLeaversSurveyExpiration` always happens after deletion
         // `resetLeaversSurveyExpiration` always happens after deletion
         result.current.deleteEmail(affiliatedEmail1.email)
         result.current.deleteEmail(affiliatedEmail1.email)
@@ -281,11 +285,11 @@ describe('UserEmailContext', function () {
         const affiliatedEmail2 = cloneDeep(professionalUserData)
         const affiliatedEmail2 = cloneDeep(professionalUserData)
         affiliatedEmail2.emailHasInstitutionLicence = true
         affiliatedEmail2.emailHasInstitutionLicence = true
 
 
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
         fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
 
 
         result.current.getEmails()
         result.current.getEmails()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         // `resetLeaversSurveyExpiration` always happens after deletion
         // `resetLeaversSurveyExpiration` always happens after deletion
         result.current.deleteEmail(affiliatedEmail1.email)
         result.current.deleteEmail(affiliatedEmail1.email)
@@ -303,11 +307,11 @@ describe('UserEmailContext', function () {
         affiliatedEmail1.email = 'institution-test@example.com'
         affiliatedEmail1.email = 'institution-test@example.com'
         affiliatedEmail1.affiliation.pastReconfirmDate = true
         affiliatedEmail1.affiliation.pastReconfirmDate = true
 
 
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [confirmedUserData, affiliatedEmail1])
         fetchMock.get(/\/user\/emails/, [confirmedUserData, affiliatedEmail1])
 
 
         result.current.getEmails()
         result.current.getEmails()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         // `resetLeaversSurveyExpiration` always happens after deletion
         // `resetLeaversSurveyExpiration` always happens after deletion
         result.current.deleteEmail(affiliatedEmail1.email)
         result.current.deleteEmail(affiliatedEmail1.email)
@@ -323,11 +327,11 @@ describe('UserEmailContext', function () {
         emailWithInstitutionLicense.email = 'institution-licensed@example.com'
         emailWithInstitutionLicense.email = 'institution-licensed@example.com'
         emailWithInstitutionLicense.emailHasInstitutionLicence = false
         emailWithInstitutionLicense.emailHasInstitutionLicence = false
 
 
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
         fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
 
 
         result.current.getEmails()
         result.current.getEmails()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         // `resetLeaversSurveyExpiration` always happens after deletion
         // `resetLeaversSurveyExpiration` always happens after deletion
         result.current.deleteEmail(emailWithInstitutionLicense.email)
         result.current.deleteEmail(emailWithInstitutionLicense.email)
@@ -342,11 +346,11 @@ describe('UserEmailContext', function () {
         emailWithInstitutionLicense.email = 'institution-licensed@example.com'
         emailWithInstitutionLicense.email = 'institution-licensed@example.com'
         emailWithInstitutionLicense.affiliation.pastReconfirmDate = false
         emailWithInstitutionLicense.affiliation.pastReconfirmDate = false
 
 
-        fetchMock.reset()
+        fetchMock.removeRoutes().clearHistory()
         fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
         fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
 
 
         result.current.getEmails()
         result.current.getEmails()
-        await fetchMock.flush(true)
+        await fetchMock.callHistory.flush(true)
 
 
         // `resetLeaversSurveyExpiration` always happens after deletion
         // `resetLeaversSurveyExpiration` always happens after deletion
         result.current.deleteEmail(emailWithInstitutionLicense.email)
         result.current.deleteEmail(emailWithInstitutionLicense.email)

+ 52 - 49
services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx

@@ -102,7 +102,7 @@ describe('<ShareProjectModal/>', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     this.locationStub.restore()
     this.locationStub.restore()
-    fetchMock.restore()
+    fetchMock.removeRoutes().clearHistory()
     cleanUpContext()
     cleanUpContext()
   })
   })
 
 
@@ -414,7 +414,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     await waitFor(() => expect(closeButton.disabled).to.be.true)
     await waitFor(() => expect(closeButton.disabled).to.be.true)
 
 
-    expect(fetchMock.done()).to.be.true
+    expect(fetchMock.callHistory.done()).to.be.true
     expect(closeButton.disabled).to.be.false
     expect(closeButton.disabled).to.be.false
   })
   })
 
 
@@ -447,7 +447,7 @@ describe('<ShareProjectModal/>', function () {
     fireEvent.click(revokeButton)
     fireEvent.click(revokeButton)
     await waitFor(() => expect(closeButton.disabled).to.be.true)
     await waitFor(() => expect(closeButton.disabled).to.be.true)
 
 
-    expect(fetchMock.done()).to.be.true
+    expect(fetchMock.callHistory.done()).to.be.true
     expect(closeButton.disabled).to.be.false
     expect(closeButton.disabled).to.be.false
   })
   })
 
 
@@ -484,10 +484,10 @@ describe('<ShareProjectModal/>', function () {
 
 
     await waitFor(() => expect(closeButton.disabled).to.be.true)
     await waitFor(() => expect(closeButton.disabled).to.be.true)
 
 
-    const { body } = fetchMock.lastOptions()
+    const { body } = fetchMock.callHistory.calls().at(-1).options
     expect(JSON.parse(body)).to.deep.equal({ privilegeLevel: 'readAndWrite' })
     expect(JSON.parse(body)).to.deep.equal({ privilegeLevel: 'readAndWrite' })
 
 
-    expect(fetchMock.done()).to.be.true
+    expect(fetchMock.callHistory.done()).to.be.true
     expect(closeButton.disabled).to.be.false
     expect(closeButton.disabled).to.be.false
   })
   })
 
 
@@ -525,10 +525,12 @@ describe('<ShareProjectModal/>', function () {
     })
     })
     fireEvent.click(removeButton)
     fireEvent.click(removeButton)
 
 
-    const url = fetchMock.lastUrl()
-    expect(url).to.equal('/project/test-project/users/member-viewer')
+    const url = fetchMock.callHistory.calls().at(-1).url
+    expect(url).to.equal(
+      'https://www.test-overleaf.com/project/test-project/users/member-viewer'
+    )
 
 
-    expect(fetchMock.done()).to.be.true
+    expect(fetchMock.callHistory.done()).to.be.true
   })
   })
 
 
   it('changes member privileges to owner with confirmation', async function () {
   it('changes member privileges to owner with confirmation', async function () {
@@ -573,10 +575,10 @@ describe('<ShareProjectModal/>', function () {
     fireEvent.click(confirmButton)
     fireEvent.click(confirmButton)
     await waitFor(() => expect(confirmButton.disabled).to.be.true)
     await waitFor(() => expect(confirmButton.disabled).to.be.true)
 
 
-    const { body } = fetchMock.lastOptions()
+    const { body } = fetchMock.callHistory.calls().at(-1).options
     expect(JSON.parse(body)).to.deep.equal({ user_id: 'member-viewer' })
     expect(JSON.parse(body)).to.deep.equal({ user_id: 'member-viewer' })
 
 
-    expect(fetchMock.done()).to.be.true
+    expect(fetchMock.callHistory.done()).to.be.true
   })
   })
 
 
   it('sends invites to input email addresses', async function () {
   it('sends invites to input email addresses', async function () {
@@ -593,7 +595,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     // loading contacts
     // loading contacts
     await waitFor(() => {
     await waitFor(() => {
-      expect(fetchMock.called('express:/user/contacts')).to.be.true
+      expect(fetchMock.callHistory.called('express:/user/contacts')).to.be.true
     })
     })
 
 
     // displaying a list of matching contacts
     // displaying a list of matching contacts
@@ -604,26 +606,29 @@ describe('<ShareProjectModal/>', function () {
 
 
     // sending invitations
     // sending invitations
 
 
-    fetchMock.post('express:/project/:projectId/invite', (url, req) => {
-      const data = JSON.parse(req.body)
-
-      if (data.email === 'a@b.c') {
-        return {
-          status: 400,
-          body: { errorReason: 'invalid_email' },
+    fetchMock.post(
+      'express:/project/:projectId/invite',
+      ({ args: [url, req] }) => {
+        const data = JSON.parse(req.body)
+
+        if (data.email === 'a@b.c') {
+          return {
+            status: 400,
+            body: { errorReason: 'invalid_email' },
+          }
         }
         }
-      }
 
 
-      return {
-        status: 200,
-        body: {
-          invite: {
-            ...data,
-            _id: data.email,
+        return {
+          status: 200,
+          body: {
+            invite: {
+              ...data,
+              _id: data.email,
+            },
           },
           },
-        },
+        }
       }
       }
-    })
+    )
 
 
     fireEvent.paste(inputElement, {
     fireEvent.paste(inputElement, {
       clipboardData: {
       clipboardData: {
@@ -643,22 +648,24 @@ describe('<ShareProjectModal/>', function () {
     let calls
     let calls
     await waitFor(
     await waitFor(
       () => {
       () => {
-        calls = fetchMock.calls('express:/project/:projectId/invite')
+        calls = fetchMock.callHistory.calls(
+          'express:/project/:projectId/invite'
+        )
         expect(calls).to.have.length(4)
         expect(calls).to.have.length(4)
       },
       },
       { timeout: 5000 } // allow time for delay between each request
       { timeout: 5000 } // allow time for delay between each request
     )
     )
 
 
-    expect(calls[0][1].body).to.equal(
+    expect(calls[0].args[1].body).to.equal(
       JSON.stringify({ email: 'test@example.com', privileges: 'readOnly' })
       JSON.stringify({ email: 'test@example.com', privileges: 'readOnly' })
     )
     )
-    expect(calls[1][1].body).to.equal(
+    expect(calls[1].args[1].body).to.equal(
       JSON.stringify({ email: 'foo@example.com', privileges: 'readOnly' })
       JSON.stringify({ email: 'foo@example.com', privileges: 'readOnly' })
     )
     )
-    expect(calls[2][1].body).to.equal(
+    expect(calls[2].args[1].body).to.equal(
       JSON.stringify({ email: 'bar@example.com', privileges: 'readOnly' })
       JSON.stringify({ email: 'bar@example.com', privileges: 'readOnly' })
     )
     )
-    expect(calls[3][1].body).to.equal(
+    expect(calls[3].args[1].body).to.equal(
       JSON.stringify({ email: 'a@b.c', privileges: 'readOnly' })
       JSON.stringify({ email: 'a@b.c', privileges: 'readOnly' })
     )
     )
 
 
@@ -752,7 +759,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     // loading contacts
     // loading contacts
     await waitFor(() => {
     await waitFor(() => {
-      expect(fetchMock.called('express:/user/contacts')).to.be.true
+      expect(fetchMock.callHistory.called('express:/user/contacts')).to.be.true
     })
     })
 
 
     const [inputElement] = await screen.findAllByLabelText('Add people')
     const [inputElement] = await screen.findAllByLabelText('Add people')
@@ -766,19 +773,15 @@ describe('<ShareProjectModal/>', function () {
       })
       })
       fireEvent.blur(inputElement)
       fireEvent.blur(inputElement)
 
 
-      fetchMock.postOnce(
-        'express:/project/:projectId/invite',
-        {
-          status: 400,
-          body: { errorReason },
-        },
-        { overwriteRoutes: true }
-      )
+      fetchMock.postOnce('express:/project/:projectId/invite', {
+        status: 400,
+        body: { errorReason },
+      })
 
 
       expect(submitButton.disabled).to.be.false
       expect(submitButton.disabled).to.be.false
       await userEvent.click(submitButton)
       await userEvent.click(submitButton)
-      await fetchMock.flush(true)
-      expect(fetchMock.done()).to.be.true
+      await fetchMock.callHistory.flush(true)
+      expect(fetchMock.callHistory.done()).to.be.true
     }
     }
 
 
     await respondWithError('cannot_invite_non_user')
     await respondWithError('cannot_invite_non_user')
@@ -820,7 +823,7 @@ describe('<ShareProjectModal/>', function () {
     fireEvent.click(enableButton)
     fireEvent.click(enableButton)
     await waitFor(() => expect(enableButton.disabled).to.be.true)
     await waitFor(() => expect(enableButton.disabled).to.be.true)
 
 
-    const { body: tokenBody } = fetchMock.lastOptions()
+    const { body: tokenBody } = fetchMock.callHistory.calls().at(-1).options
     expect(JSON.parse(tokenBody)).to.deep.equal({
     expect(JSON.parse(tokenBody)).to.deep.equal({
       publicAccessLevel: 'tokenBased',
       publicAccessLevel: 'tokenBased',
     })
     })
@@ -840,7 +843,7 @@ describe('<ShareProjectModal/>', function () {
     fireEvent.click(disableButton)
     fireEvent.click(disableButton)
     await waitFor(() => expect(disableButton.disabled).to.be.true)
     await waitFor(() => expect(disableButton.disabled).to.be.true)
 
 
-    const { body: privateBody } = fetchMock.lastOptions()
+    const { body: privateBody } = fetchMock.callHistory.calls().at(-1).options
     expect(JSON.parse(privateBody)).to.deep.equal({
     expect(JSON.parse(privateBody)).to.deep.equal({
       publicAccessLevel: 'private',
       publicAccessLevel: 'private',
     })
     })
@@ -865,7 +868,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     // Wait for contacts to load
     // Wait for contacts to load
     await waitFor(() => {
     await waitFor(() => {
-      expect(fetchMock.called('express:/user/contacts')).to.be.true
+      expect(fetchMock.callHistory.called('express:/user/contacts')).to.be.true
     })
     })
 
 
     // Enter a prefix that matches a contact
     // Enter a prefix that matches a contact
@@ -919,7 +922,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     // Wait for contacts to load
     // Wait for contacts to load
     await waitFor(() => {
     await waitFor(() => {
-      expect(fetchMock.called('express:/user/contacts')).to.be.true
+      expect(fetchMock.callHistory.called('express:/user/contacts')).to.be.true
     })
     })
 
 
     // Enter a prefix that matches a contact
     // Enter a prefix that matches a contact
@@ -954,7 +957,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     // Wait for contacts to load
     // Wait for contacts to load
     await waitFor(() => {
     await waitFor(() => {
-      expect(fetchMock.called('express:/user/contacts')).to.be.true
+      expect(fetchMock.callHistory.called('express:/user/contacts')).to.be.true
     })
     })
 
 
     // Enter a prefix that matches a contact
     // Enter a prefix that matches a contact
@@ -988,7 +991,7 @@ describe('<ShareProjectModal/>', function () {
 
 
     // Wait for contacts to load
     // Wait for contacts to load
     await waitFor(() => {
     await waitFor(() => {
-      expect(fetchMock.called('express:/user/contacts')).to.be.true
+      expect(fetchMock.callHistory.called('express:/user/contacts')).to.be.true
     })
     })
 
 
     // Enter a prefix that matches a contact
     // Enter a prefix that matches a contact

+ 2 - 2
services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx

@@ -51,7 +51,7 @@ describe('<GroupSubscriptionMemberships />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders all group subscriptions not managed', function () {
   it('renders all group subscriptions not managed', function () {
@@ -129,7 +129,7 @@ describe('<GroupSubscriptionMemberships />', function () {
 
 
       fireEvent.click(this.leaveNowButton)
       fireEvent.click(this.leaveNowButton)
 
 
-      expect(leaveGroupApiMock.called()).to.be.true
+      expect(leaveGroupApiMock.callHistory.called()).to.be.true
       await waitFor(() => {
       await waitFor(() => {
         expect(reloadStub).to.have.been.called
         expect(reloadStub).to.have.been.called
       })
       })

+ 10 - 7
services/web/test/frontend/features/subscription/components/dashboard/managed-institutions.test.tsx

@@ -37,7 +37,7 @@ describe('<ManagedInstitutions />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders all managed institutions', function () {
   it('renders all managed institutions', function () {
@@ -98,7 +98,9 @@ describe('<ManagedInstitutions />', function () {
 
 
     const unsubscribeLink = screen.getByText('Unsubscribe')
     const unsubscribeLink = screen.getByText('Unsubscribe')
     await fireEvent.click(unsubscribeLink)
     await fireEvent.click(unsubscribeLink)
-    await waitFor(() => expect(fetchMock.called(unsubscribeUrl)).to.be.true)
+    await waitFor(
+      () => expect(fetchMock.callHistory.called(unsubscribeUrl)).to.be.true
+    )
 
 
     await waitFor(() => {
     await waitFor(() => {
       expect(screen.getByText('Subscribe')).to.exist
       expect(screen.getByText('Subscribe')).to.exist
@@ -122,13 +124,14 @@ describe('<ManagedInstitutions />', function () {
       </SplitTestProvider>
       </SplitTestProvider>
     )
     )
 
 
-    const subscribeLink = screen.getByText('Subscribe')
+    const subscribeLink = await screen.findByText('Subscribe')
+
     await fireEvent.click(subscribeLink)
     await fireEvent.click(subscribeLink)
-    await waitFor(() => expect(fetchMock.called(subscribeUrl)).to.be.true)
+    await waitFor(
+      () => expect(fetchMock.callHistory.called(subscribeUrl)).to.be.true
+    )
 
 
-    await waitFor(() => {
-      expect(screen.getByText('Unsubscribe')).to.exist
-    })
+    await screen.findByText('Unsubscribe')
   })
   })
 
 
   it('renders nothing when there are no institutions', function () {
   it('renders nothing when there are no institutions', function () {

+ 1 - 1
services/web/test/frontend/features/subscription/components/dashboard/managed-publishers.test.tsx

@@ -28,7 +28,7 @@ describe('<ManagedPublishers />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('renders all managed publishers', function () {
   it('renders all managed publishers', function () {

+ 1 - 1
services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx

@@ -59,7 +59,7 @@ describe('<PauseSubscriptionModal />', function () {
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     this.locationStub.restore()
     this.locationStub.restore()
     this.replaceStateStub.restore()
     this.replaceStateStub.restore()
   })
   })

+ 4 - 4
services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx

@@ -106,18 +106,18 @@ describe('<PersonalSubscription />', function () {
       fetchMock.postOnce(reactivateSubscriptionUrl, 400)
       fetchMock.postOnce(reactivateSubscriptionUrl, 400)
       fireEvent.click(reactivateBtn)
       fireEvent.click(reactivateBtn)
       expect(reactivateBtn.disabled).to.be.true
       expect(reactivateBtn.disabled).to.be.true
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       expect(reactivateBtn.disabled).to.be.false
       expect(reactivateBtn.disabled).to.be.false
       expect(reloadStub).not.to.have.been.called
       expect(reloadStub).not.to.have.been.called
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
 
 
       // 2nd click - success
       // 2nd click - success
       fetchMock.postOnce(reactivateSubscriptionUrl, 200)
       fetchMock.postOnce(reactivateSubscriptionUrl, 200)
       fireEvent.click(reactivateBtn)
       fireEvent.click(reactivateBtn)
-      await fetchMock.flush(true)
+      await fetchMock.callHistory.flush(true)
       expect(reloadStub).to.have.been.calledOnce
       expect(reloadStub).to.have.been.calledOnce
       expect(reactivateBtn.disabled).to.be.true
       expect(reactivateBtn.disabled).to.be.true
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('renders the expired dash', function () {
     it('renders the expired dash', function () {

+ 1 - 1
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

@@ -208,7 +208,7 @@ describe('<ActiveSubscription />', function () {
 
 
     afterEach(function () {
     afterEach(function () {
       this.locationStub.restore()
       this.locationStub.restore()
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     function showConfirmCancelUI() {
     function showConfirmCancelUI() {

+ 1 - 1
services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx

@@ -38,7 +38,7 @@ describe('<ChangePlanModal />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     cleanUpContext()
     cleanUpContext()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
     this.locationStub.restore()
     this.locationStub.restore()
   })
   })
 
 

+ 1 - 1
services/web/test/frontend/features/subscription/components/group-invite/has-individual-recurly-subscription.test.tsx

@@ -7,7 +7,7 @@ import fetchMock from 'fetch-mock'
 describe('group invite', function () {
 describe('group invite', function () {
   describe('user has a personal subscription', function () {
   describe('user has a personal subscription', function () {
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     it('shows option to cancel subscription', async function () {
     it('shows option to cancel subscription', async function () {

+ 1 - 1
services/web/test/frontend/features/subscription/components/group-invite/join-group.test.tsx

@@ -10,7 +10,7 @@ describe('join group', function () {
     window.metaAttributesCache.set('ol-inviteToken', inviteToken)
     window.metaAttributesCache.set('ol-inviteToken', inviteToken)
   })
   })
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   it('shows option to join subscription', async function () {
   it('shows option to join subscription', async function () {

+ 1 - 1
services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx

@@ -90,5 +90,5 @@ export function renderWithSubscriptionDashContext(
 export function cleanUpContext() {
 export function cleanUpContext() {
   // @ts-ignore
   // @ts-ignore
   delete global.recurly
   delete global.recurly
-  fetchMock.reset()
+  fetchMock.removeRoutes().clearHistory()
 }
 }

+ 1 - 1
services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.jsx

@@ -7,7 +7,7 @@ import WordCountModal from '../../../../../frontend/js/features/word-count-modal
 
 
 describe('<WordCountModal />', function () {
 describe('<WordCountModal />', function () {
   afterEach(function () {
   afterEach(function () {
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   const contextProps = {
   const contextProps = {

+ 3 - 14
services/web/test/frontend/infrastructure/fetch-json.test.js

@@ -1,6 +1,5 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
 import fetchMock from 'fetch-mock'
 import fetchMock from 'fetch-mock'
-import { Response } from 'node-fetch'
 import {
 import {
   deleteJSON,
   deleteJSON,
   FetchError,
   FetchError,
@@ -12,11 +11,11 @@ import {
 
 
 describe('fetchJSON', function () {
 describe('fetchJSON', function () {
   before(function () {
   before(function () {
-    fetchMock.restore()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
-    fetchMock.restore()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   const headers = {
   const headers = {
@@ -138,17 +137,7 @@ describe('fetchJSON', function () {
   })
   })
 
 
   it('handles 5xx responses without a status message', async function () {
   it('handles 5xx responses without a status message', async function () {
-    // It's hard to make a Response object with statusText=null,
-    // so we need to do some monkey-work to make it happen
-    const response = new Response('weird scary error', {
-      ok: false,
-      status: 599,
-    })
-    Object.defineProperty(response, 'statusText', {
-      get: () => null,
-      set: () => {},
-    })
-    fetchMock.get('/test', response)
+    fetchMock.get('/test', { status: 599 })
 
 
     return expect(getJSON('/test'))
     return expect(getJSON('/test'))
       .to.eventually.be.rejectedWith('Unexpected Error: 599')
       .to.eventually.be.rejectedWith('Unexpected Error: 599')

+ 33 - 33
services/web/test/frontend/infrastructure/project-snapshot.test.ts

@@ -139,7 +139,7 @@ describe('ProjectSnapshot', function () {
     mockLatestChunk()
     mockLatestChunk()
     mockBlobs(['main.tex', 'hello.txt'])
     mockBlobs(['main.tex', 'hello.txt'])
     await snapshot.refresh()
     await snapshot.refresh()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   }
   }
 
 
   describe('after initialization', function () {
   describe('after initialization', function () {
@@ -176,7 +176,7 @@ describe('ProjectSnapshot', function () {
     mockChanges()
     mockChanges()
     mockBlobs(['goodbye.txt'])
     mockBlobs(['goodbye.txt'])
     await snapshot.refresh()
     await snapshot.refresh()
-    fetchMock.reset()
+    fetchMock.removeRoutes().clearHistory()
   }
   }
 
 
   describe('after refresh', function () {
   describe('after refresh', function () {
@@ -184,7 +184,7 @@ describe('ProjectSnapshot', function () {
     beforeEach(refreshSnapshot)
     beforeEach(refreshSnapshot)
 
 
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     describe('getDocPaths()', function () {
     describe('getDocPaths()', function () {
@@ -214,7 +214,7 @@ describe('ProjectSnapshot', function () {
 
 
   describe('concurrency', function () {
   describe('concurrency', function () {
     afterEach(function () {
     afterEach(function () {
-      fetchMock.reset()
+      fetchMock.removeRoutes().clearHistory()
     })
     })
 
 
     specify('two concurrent inits', async function () {
     specify('two concurrent inits', async function () {
@@ -226,9 +226,9 @@ describe('ProjectSnapshot', function () {
       await Promise.all([snapshot.refresh(), snapshot.refresh()])
       await Promise.all([snapshot.refresh(), snapshot.refresh()])
 
 
       // The first request initializes, the second request loads changes
       // The first request initializes, the second request loads changes
-      expect(fetchMock.calls('flush')).to.have.length(2)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(2)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
     })
     })
 
 
     specify('three concurrent inits', async function () {
     specify('three concurrent inits', async function () {
@@ -245,9 +245,9 @@ describe('ProjectSnapshot', function () {
 
 
       // The first request initializes, the second and third are combined and
       // The first request initializes, the second and third are combined and
       // load changes
       // load changes
-      expect(fetchMock.calls('flush')).to.have.length(2)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(2)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
     })
     })
 
 
     specify('two concurrent inits - first fails', async function () {
     specify('two concurrent inits - first fails', async function () {
@@ -262,9 +262,9 @@ describe('ProjectSnapshot', function () {
 
 
       // The first init fails, but the second succeeds
       // The first init fails, but the second succeeds
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
-      expect(fetchMock.calls('flush')).to.have.length(2)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(0)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(2)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(0)
     })
     })
 
 
     specify('three concurrent inits - second fails', async function () {
     specify('three concurrent inits - second fails', async function () {
@@ -285,10 +285,10 @@ describe('ProjectSnapshot', function () {
       // The first init succeeds, the two queued requests fail, the last request
       // The first init succeeds, the two queued requests fail, the last request
       // succeeds
       // succeeds
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
-      expect(fetchMock.calls('flush')).to.have.length(3)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
-      expect(fetchMock.calls('changes-2')).to.have.length(0)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-2')).to.have.length(0)
     })
     })
 
 
     specify('two concurrent load changes', async function () {
     specify('two concurrent load changes', async function () {
@@ -304,10 +304,10 @@ describe('ProjectSnapshot', function () {
       await Promise.all([snapshot.refresh(), snapshot.refresh()])
       await Promise.all([snapshot.refresh(), snapshot.refresh()])
 
 
       // One init, two load changes
       // One init, two load changes
-      expect(fetchMock.calls('flush')).to.have.length(3)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
-      expect(fetchMock.calls('changes-2')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-2')).to.have.length(1)
     })
     })
 
 
     specify('three concurrent load changes', async function () {
     specify('three concurrent load changes', async function () {
@@ -327,10 +327,10 @@ describe('ProjectSnapshot', function () {
       ])
       ])
 
 
       // One init, two load changes (the two last are queued and combined)
       // One init, two load changes (the two last are queued and combined)
-      expect(fetchMock.calls('flush')).to.have.length(3)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
-      expect(fetchMock.calls('changes-2')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-2')).to.have.length(1)
     })
     })
 
 
     specify('two concurrent load changes - first fails', async function () {
     specify('two concurrent load changes - first fails', async function () {
@@ -350,10 +350,10 @@ describe('ProjectSnapshot', function () {
 
 
       // One init, one load changes fails, the second succeeds
       // One init, one load changes fails, the second succeeds
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
-      expect(fetchMock.calls('flush')).to.have.length(3)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
-      expect(fetchMock.calls('changes-2')).to.have.length(0)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-2')).to.have.length(0)
     })
     })
 
 
     specify('three concurrent load changes - second fails', async function () {
     specify('three concurrent load changes - second fails', async function () {
@@ -378,10 +378,10 @@ describe('ProjectSnapshot', function () {
       // One init, one load changes succeeds, the second and third are combined
       // One init, one load changes succeeds, the second and third are combined
       // and fail, the last request succeeds
       // and fail, the last request succeeds
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
       expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
-      expect(fetchMock.calls('flush')).to.have.length(4)
-      expect(fetchMock.calls('latest-chunk')).to.have.length(1)
-      expect(fetchMock.calls('changes-1')).to.have.length(1)
-      expect(fetchMock.calls('changes-2')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('flush')).to.have.length(4)
+      expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
+      expect(fetchMock.callHistory.calls('changes-2')).to.have.length(1)
     })
     })
   })
   })
 })
 })

+ 4 - 4
services/web/test/frontend/shared/hooks/use-abort-controller.test.tsx

@@ -13,7 +13,7 @@ describe('useAbortController', function () {
   }
   }
 
 
   beforeEach(function () {
   beforeEach(function () {
-    fetchMock.restore()
+    fetchMock.removeRoutes().clearHistory()
 
 
     status = {
     status = {
       loading: false,
       loading: false,
@@ -23,7 +23,7 @@ describe('useAbortController', function () {
   })
   })
 
 
   after(function () {
   after(function () {
-    fetchMock.restore()
+    fetchMock.removeRoutes().clearHistory()
   })
   })
 
 
   function AbortableRequest({ url }: { url: string }) {
   function AbortableRequest({ url }: { url: string }) {
@@ -80,8 +80,8 @@ describe('useAbortController', function () {
 
 
     unmount()
     unmount()
 
 
-    await fetchMock.flush(true)
-    expect(fetchMock.done()).to.be.true
+    await fetchMock.callHistory.flush(true)
+    expect(fetchMock.callHistory.done()).to.be.true
 
 
     // wait for Promises to be resolved
     // wait for Promises to be resolved
     await new Promise(resolve => setTimeout(resolve, 0))
     await new Promise(resolve => setTimeout(resolve, 0))