Browse Source

Merge pull request #24313 from overleaf/td-ac-bs5-frontend-tests

Update front-end tests to use Bootstrap 5

GitOrigin-RevId: abaa09f8c0639d64d6ade97468ab16204e5de97b
Tim Down 1 year ago
parent
commit
c2da12939e
56 changed files with 356 additions and 262 deletions
  1. 1 0
      services/web/frontend/extracted-translations.json
  2. 1 0
      services/web/frontend/js/features/dictionary/components/dictionary-modal.tsx
  3. 1 0
      services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx
  4. 1 0
      services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx
  5. 1 0
      services/web/frontend/js/features/settings/components/password-section.tsx
  6. 2 1
      services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx
  7. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx
  8. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx
  9. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx
  10. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx
  11. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx
  12. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx
  13. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx
  14. 1 0
      services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx
  15. 6 0
      services/web/frontend/js/shared/components/loading-spinner.tsx
  16. 2 2
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-dictionary.test.tsx
  17. 2 2
      services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.jsx
  18. 17 17
      services/web/test/frontend/features/project-list/components/current-plan-widget.test.tsx
  19. 0 12
      services/web/test/frontend/features/project-list/components/new-project-button.test.tsx
  20. 4 2
      services/web/test/frontend/features/project-list/components/notifications.test.tsx
  21. 49 19
      services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
  22. 3 0
      services/web/test/frontend/features/project-list/components/project-list-title.tsx
  23. 1 1
      services/web/test/frontend/features/project-list/components/sidebar/add-affiliation.test.tsx
  24. 4 4
      services/web/test/frontend/features/project-list/components/survey-widget.test.tsx
  25. 5 3
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/archive-project-button.test.tsx
  26. 2 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx
  27. 6 4
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/copy-project-button.test.tsx
  28. 5 3
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/delete-project-button.test.tsx
  29. 2 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx
  30. 5 3
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/leave-project-button.test.tsx
  31. 3 1
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/rename-project-button.test.tsx
  32. 5 3
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/trash-project-button.test.tsx
  33. 2 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/unarchive-project-button.test.tsx
  34. 2 2
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/untrash-project-button.test.tsx
  35. 2 2
      services/web/test/frontend/features/project-list/components/table/project-tools/buttons/archive-projects.button.test.tsx
  36. 2 2
      services/web/test/frontend/features/project-list/components/table/project-tools/buttons/download-projects-button.test.tsx
  37. 2 2
      services/web/test/frontend/features/project-list/components/table/project-tools/buttons/trash-projects.button.test.tsx
  38. 6 2
      services/web/test/frontend/features/project-list/components/table/project-tools/rename-project-modal.test.tsx
  39. 3 1
      services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx
  40. 1 1
      services/web/test/frontend/features/settings/components/account-info-section.test.tsx
  41. 5 8
      services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx
  42. 2 2
      services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx
  43. 1 1
      services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx
  44. 5 5
      services/web/test/frontend/features/settings/components/emails/sso-alert.test.tsx
  45. 1 1
      services/web/test/frontend/features/settings/components/linking-section.test.tsx
  46. 8 7
      services/web/test/frontend/features/settings/components/linking/integration-widget.test.tsx
  47. 16 7
      services/web/test/frontend/features/settings/components/linking/sso-widget.test.tsx
  48. 1 1
      services/web/test/frontend/features/settings/components/password-section.test.tsx
  49. 1 1
      services/web/test/frontend/features/settings/components/security-section.test.tsx
  50. 9 3
      services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx
  51. 1 1
      services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
  52. 5 5
      services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx
  53. 22 12
      services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
  54. 32 28
      services/web/test/frontend/features/subscription/components/group-invite/accepted-invite.test.tsx
  55. 90 85
      services/web/test/frontend/features/subscription/components/group-invite/group-invite.test.tsx
  56. 1 0
      services/web/test/frontend/helpers/reset-meta.ts

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -836,6 +836,7 @@
   "join_project": "",
   "join_team_explanation": "",
   "joined_team": "",
+  "joining": "",
   "justify": "",
   "kb_suggestions_enquiry": "",
   "keep_current_plan": "",

+ 1 - 0
services/web/frontend/js/features/dictionary/components/dictionary-modal.tsx

@@ -15,6 +15,7 @@ function DictionaryModal({ show, handleHide }: DictionaryModalProps) {
       show={show}
       onHide={handleHide}
       id="dictionary-modal"
+      data-testid="dictionary-modal"
       size="sm"
     >
       <DictionaryModalContent handleHide={handleHide} />

+ 1 - 0
services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx

@@ -96,6 +96,7 @@ function CommonNotification({ notification }: CommonNotificationProps) {
               <OLButton
                 variant="secondary"
                 isLoading={isLoading}
+                loadingLabel={t('joining')}
                 disabled={isLoading}
                 onClick={() => handleAcceptInvite(notification)}
               >

+ 1 - 0
services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx

@@ -145,6 +145,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
               type="submit"
               disabled={!role || !department}
               isLoading={isLoading}
+              loadingLabel={t('saving')}
               bs3Props={{
                 loading: isLoading
                   ? `${t('saving')}…`

+ 1 - 0
services/web/frontend/js/features/settings/components/password-section.tsx

@@ -203,6 +203,7 @@ function PasswordForm() {
           variant="primary"
           disabled={!isFormValid}
           isLoading={isLoading}
+          loadingLabel={`${t('saving')}…`}
           bs3Props={{
             loading: isLoading ? `${t('saving')}…` : t('change'),
           }}

+ 2 - 1
services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx

@@ -70,13 +70,14 @@ export default function LeaveGroupModal() {
           onClick={handleConfirmLeaveGroup}
           disabled={inflight}
           isLoading={inflight}
+          loadingLabel={t('processing_uppercase') + '…'}
           bs3Props={{
             loading: inflight
               ? t('processing_uppercase') + '…'
               : t('leave_now'),
           }}
         >
-          {t('processing_uppercase')}
+          {t('leave_now')}
         </OLButton>
       </OLModalFooter>
     </OLModal>

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx

@@ -50,6 +50,7 @@ function PersonalSubscriptionRecurlySyncEmail() {
                   type="submit"
                   disabled={isLoading}
                   isLoading={isLoading}
+                  loadingLabel={t('updating')}
                   bs3Props={{
                     loading: isLoading ? t('updating') + '…' : t('update'),
                   }}

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx

@@ -34,6 +34,7 @@ function ConfirmCancelSubscriptionButton({
   return (
     <OLButton
       isLoading={isLoading}
+      loadingLabel={t('processing_uppercase') + '…'}
       disabled={disabled}
       onClick={onClick}
       variant={showNoThanks ? 'link' : undefined}

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx

@@ -40,6 +40,7 @@ export default function DowngradePlanButton({
       onClick={handleDowngradePlan}
       disabled={isButtonDisabled}
       isLoading={isLoading}
+      loadingLabel={t('processing_uppercase') + '…'}
       bs3Props={{
         loading: isLoading ? t('processing_uppercase') + '…' : buttonText,
       }}

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx

@@ -33,6 +33,7 @@ export default function ExtendTrialButton({
       onClick={handleExtendTrial}
       disabled={isButtonDisabled}
       isLoading={isLoading}
+      loadingLabel={t('processing_uppercase') + '…'}
       bs3Props={{
         loading: isLoading ? t('processing_uppercase') + '…' : buttonText,
       }}

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx

@@ -105,6 +105,7 @@ export function CancelAiAddOnModal() {
           variant="danger"
           disabled={inflight}
           isLoading={inflight}
+          loadingLabel={t('processing_uppercase') + '…'}
           onClick={handleConfirmChange}
           bs3Props={{
             loading: inflight

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx

@@ -332,6 +332,7 @@ export function ChangeToGroupModal() {
               }
               onClick={upgrade}
               isLoading={inflight}
+              loadingLabel={t('processing_uppercase') + '…'}
               bs3Props={{
                 loading: inflight
                   ? t('processing_uppercase') + '…'

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx

@@ -110,6 +110,7 @@ export function ConfirmChangePlanModal() {
           variant="primary"
           disabled={inflight}
           isLoading={inflight}
+          loadingLabel={t('processing_uppercase') + '…'}
           onClick={handleConfirmChange}
           bs3Props={{
             loading: inflight

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx

@@ -91,6 +91,7 @@ export function KeepCurrentPlanModal() {
           variant="primary"
           disabled={inflight}
           isLoading={inflight}
+          loadingLabel={t('processing_uppercase') + '…'}
           onClick={confirmCancelPendingPlanChange}
           bs3Props={{
             loading: inflight

+ 6 - 0
services/web/frontend/js/shared/components/loading-spinner.tsx

@@ -25,6 +25,12 @@ function LoadingSpinner({
   const [show, setShow] = useState(false)
 
   useEffect(() => {
+    // Ensure that spinner is displayed immediately if delay is 0
+    if (delay === 0) {
+      setShow(true)
+      return
+    }
+
     const timer = setTimeout(() => {
       setShow(true)
     }, delay)

+ 2 - 2
services/web/test/frontend/features/editor-left-menu/components/settings/settings-dictionary.test.tsx

@@ -19,7 +19,7 @@ describe('<SettingsDictionary />', function () {
     const button = screen.getByText('Edit')
     fireEvent.click(button)
 
-    const modal = screen.getAllByRole('dialog')[0]
+    const modal = screen.getByTestId('dictionary-modal')
 
     within(modal).getByRole('heading', { name: 'Edit Dictionary' })
     within(modal).getByText('Your custom dictionary is empty.')
@@ -28,6 +28,6 @@ describe('<SettingsDictionary />', function () {
       name: 'Close',
     })
     fireEvent.click(closeButton)
-    expect(screen.queryByRole('dialog')).to.be.null
+    expect(screen.getByTestId('dictionary-modal')).to.not.be.null
   })
 })

+ 2 - 2
services/web/test/frontend/features/editor-navigation-toolbar/components/online-users-widget.test.jsx

@@ -26,11 +26,11 @@ describe('<OnlineUsersWidget />', function () {
       screen.getByText('a')
     })
 
-    it('displays user name in a tooltip', function () {
+    it('displays user name in a tooltip', async function () {
       render(<OnlineUsersWidget {...defaultProps} />)
       const icon = screen.getByText('t')
       fireEvent.mouseOver(icon)
-      screen.getByRole('tooltip', { name: 'test_user' })
+      await screen.findByRole('tooltip', { name: 'test_user' })
     })
 
     it('calls "goToUser" when the user initial is clicked', function () {

+ 17 - 17
services/web/test/frontend/features/project-list/components/current-plan-widget.test.tsx

@@ -41,13 +41,13 @@ describe('<CurrentPlanWidget />', function () {
       render(<CurrentPlanWidget />)
     })
 
-    it('shows text and tooltip on mouseover', function () {
+    it('shows text and tooltip on mouseover', async function () {
       const link = screen.getByRole('link', {
         name: /plan is paused/i,
       })
       fireEvent.mouseOver(link)
 
-      screen.getByRole('tooltip', { name: pausedTooltipMessage })
+      await screen.findByRole('tooltip', { name: pausedTooltipMessage })
     })
   })
 
@@ -60,17 +60,17 @@ describe('<CurrentPlanWidget />', function () {
       render(<CurrentPlanWidget />)
     })
 
-    it('shows text and tooltip on mouseover', function () {
+    it('shows text and tooltip on mouseover', async function () {
       const link = screen.getByRole('link', {
         name: /you’re on the free plan/i,
       })
       fireEvent.mouseOver(link)
 
-      screen.getByRole('tooltip', { name: freePlanTooltipMessage })
+      await screen.findByRole('tooltip', { name: freePlanTooltipMessage })
     })
 
     it('clicks on upgrade button', function () {
-      const upgradeLink = screen.getByRole('link', { name: /upgrade/i })
+      const upgradeLink = screen.getByRole('button', { name: /upgrade/i })
       fireEvent.click(upgradeLink)
       expect(sendMBSpy).to.be.calledOnce
       expect(sendMBSpy).calledWith('upgrade-button-click', {
@@ -156,7 +156,7 @@ describe('<CurrentPlanWidget />', function () {
         })
       })
 
-      it('shows text and tooltip on mouseover', function () {
+      it('shows text and tooltip on mouseover', async function () {
         render(<CurrentPlanWidget />)
 
         const link = screen.getByRole('link', {
@@ -164,10 +164,10 @@ describe('<CurrentPlanWidget />', function () {
         })
         fireEvent.mouseOver(link)
 
-        screen.getByRole('tooltip', {
+        await screen.findByRole('tooltip', {
           name: new RegExp(`on the ${subscription.plan.name}`, 'i'),
         })
-        screen.getByRole('tooltip', { name: paidPlanTooltipMessage })
+        await screen.findByRole('tooltip', { name: paidPlanTooltipMessage })
       })
     })
 
@@ -189,7 +189,7 @@ describe('<CurrentPlanWidget />', function () {
         })
       })
 
-      it('shows text and tooltip on mouseover (without subscription team name)', function () {
+      it('shows text and tooltip on mouseover (without subscription team name)', async function () {
         render(<CurrentPlanWidget />)
 
         const link = screen.getByRole('link', {
@@ -198,16 +198,16 @@ describe('<CurrentPlanWidget />', function () {
         fireEvent.mouseOver(link)
 
         expect(subscription.subscription.teamName).to.be.undefined
-        screen.getByRole('tooltip', {
+        await screen.findByRole('tooltip', {
           name: new RegExp(
             `on the ${subscription.plan.name} plan as a member of a group subscription`,
             'i'
           ),
         })
-        screen.getByRole('tooltip', { name: paidPlanTooltipMessage })
+        await screen.findByRole('tooltip', { name: paidPlanTooltipMessage })
       })
 
-      it('shows text and tooltip on mouseover (with subscription team name)', function () {
+      it('shows text and tooltip on mouseover (with subscription team name)', async function () {
         const newSubscription = {
           ...subscription,
           subscription: {
@@ -227,18 +227,18 @@ describe('<CurrentPlanWidget />', function () {
         })
         fireEvent.mouseOver(link)
 
-        screen.getByRole('tooltip', {
+        await screen.findByRole('tooltip', {
           name: new RegExp(
             `on the ${newSubscription.plan.name} plan as a member of a group subscription, ${newSubscription.subscription.teamName}`,
             'i'
           ),
         })
-        screen.getByRole('tooltip', { name: paidPlanTooltipMessage })
+        await screen.findByRole('tooltip', { name: paidPlanTooltipMessage })
       })
     })
 
     describe('commons', function () {
-      it('shows text and tooltip on mouseover', function () {
+      it('shows text and tooltip on mouseover', async function () {
         const subscription = {
           type: 'commons',
           plan: {
@@ -260,13 +260,13 @@ describe('<CurrentPlanWidget />', function () {
         })
         fireEvent.mouseOver(link)
 
-        screen.getByRole('tooltip', {
+        await screen.findByRole('tooltip', {
           name: new RegExp(
             `on the ${subscription.plan.name} plan because of your affiliation with ${subscription.subscription.name}`,
             'i'
           ),
         })
-        screen.getByRole('tooltip', { name: paidPlanTooltipMessage })
+        await screen.findByRole('tooltip', { name: paidPlanTooltipMessage })
       })
     })
   })

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

@@ -4,20 +4,8 @@ import fetchMock from 'fetch-mock'
 import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button'
 import { renderWithProjectListContext } from '../helpers/render-with-context'
 import getMeta from '@/utils/meta'
-import * as bootstrapUtils from '@/features/utils/bootstrap-5'
-import sinon, { type SinonStub } from 'sinon'
 
 describe('<NewProjectButton />', function () {
-  let isBootstrap5Stub: SinonStub
-
-  before(function () {
-    isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true)
-  })
-
-  after(function () {
-    isBootstrap5Stub.restore()
-  })
-
   beforeEach(function () {
     fetchMock.reset()
   })

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

@@ -141,7 +141,9 @@ describe('<UserNotifications />', function () {
 
       expect(joinBtn.disabled).to.be.true
 
-      await waitForElementToBeRemoved(() => screen.getByText('Loading'))
+      await waitForElementToBeRemoved(() =>
+        screen.getByRole('button', { name: /joining/i })
+      )
 
       expect(acceptMock.called()).to.be.true
       screen.getByText(/joined/i)
@@ -185,7 +187,7 @@ describe('<UserNotifications />', function () {
       fireEvent.click(joinBtn)
 
       await waitForElementToBeRemoved(() =>
-        screen.getByRole('button', { name: /loading/i })
+        screen.getByRole('button', { name: /joining/i })
       )
 
       expect(fetchMock.called()).to.be.true

+ 49 - 19
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -1,4 +1,10 @@
-import { fireEvent, screen, waitFor, within } from '@testing-library/react'
+import {
+  fireEvent,
+  screen,
+  waitFor,
+  waitForElementToBeRemoved,
+  within,
+} from '@testing-library/react'
 import { expect } from 'chai'
 import fetchMock from 'fetch-mock'
 import sinon from 'sinon'
@@ -157,7 +163,9 @@ describe('<ProjectListRoot />', function () {
           const archiveButton = within(actionsToolbar).getByLabelText('Archive')
           fireEvent.click(archiveButton)
 
-          const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+          const confirmBtn = screen.getByRole('button', {
+            name: 'Confirm',
+          }) as HTMLButtonElement
           fireEvent.click(confirmBtn)
           expect(confirmBtn.disabled).to.be.true
 
@@ -187,7 +195,9 @@ describe('<ProjectListRoot />', function () {
           const archiveButton = within(actionsToolbar).getByLabelText('Trash')
           fireEvent.click(archiveButton)
 
-          const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+          const confirmBtn = screen.getByRole('button', {
+            name: 'Confirm',
+          }) as HTMLButtonElement
           fireEvent.click(confirmBtn)
           expect(confirmBtn.disabled).to.be.true
 
@@ -266,14 +276,15 @@ describe('<ProjectListRoot />', function () {
             status: 200,
           })
 
-          const unarchiveButton =
-            within(actionsToolbar).getByText<HTMLButtonElement>('Restore')
+          const unarchiveButton = within(actionsToolbar).getByRole('button', {
+            name: 'Restore',
+          })
           fireEvent.click(unarchiveButton)
 
           await fetchMock.flush(true)
           expect(fetchMock.done()).to.be.true
 
-          screen.getByText('No projects')
+          await screen.findByText('No projects')
         })
 
         it('only unarchive the selected projects', async function () {
@@ -340,7 +351,7 @@ describe('<ProjectListRoot />', function () {
           await fetchMock.flush(true)
           expect(fetchMock.done()).to.be.true
 
-          screen.getByText('No projects')
+          await screen.findByText('No projects')
         })
 
         it('only untrashes the selected projects', async function () {
@@ -369,7 +380,9 @@ describe('<ProjectListRoot />', function () {
             within(actionsToolbar).getByLabelText<HTMLButtonElement>('Archive')
           fireEvent.click(archiveButton)
 
-          const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
+          const confirmButton = screen.getByRole('button', {
+            name: 'Confirm',
+          }) as HTMLButtonElement
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
 
@@ -427,7 +440,9 @@ describe('<ProjectListRoot />', function () {
           })
           fireEvent.click(leaveButton)
 
-          const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
+          const confirmButton = screen.getByRole('button', {
+            name: 'Confirm',
+          }) as HTMLButtonElement
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
 
@@ -484,7 +499,9 @@ describe('<ProjectListRoot />', function () {
           })
           fireEvent.click(deleteButton)
 
-          const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
+          const confirmButton = screen.getByRole('button', {
+            name: 'Confirm',
+          }) as HTMLButtonElement
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
 
@@ -550,7 +567,9 @@ describe('<ProjectListRoot />', function () {
           })
           fireEvent.click(deleteLeaveButton)
 
-          const confirmButton = screen.getByText<HTMLButtonElement>('Confirm')
+          const confirmButton = screen.getByRole('button', {
+            name: 'Confirm',
+          }) as HTMLButtonElement
           fireEvent.click(confirmButton)
           expect(confirmButton.disabled).to.be.true
 
@@ -616,7 +635,11 @@ describe('<ProjectListRoot />', function () {
           const trashBtns = screen.getAllByRole('button', { name: 'Trash' })
           for (const [index, trashBtn] of trashBtns.entries()) {
             fireEvent.click(trashBtn)
-            fireEvent.click(screen.getByText<HTMLButtonElement>('Confirm'))
+            fireEvent.click(
+              screen.getByRole('button', {
+                name: 'Confirm',
+              })
+            )
             await waitFor(() => {
               expect(
                 trashProjectMock.called(
@@ -843,8 +866,9 @@ describe('<ProjectListRoot />', function () {
             )
 
             // same name
-            let confirmButton =
-              within(modal).getByText<HTMLButtonElement>('Rename')
+            let confirmButton = within(modal).getByRole('button', {
+              name: 'Rename',
+            }) as HTMLButtonElement
             expect(confirmButton.disabled).to.be.true
 
             // no name
@@ -852,7 +876,9 @@ describe('<ProjectListRoot />', function () {
             fireEvent.change(input, {
               target: { value: '' },
             })
-            confirmButton = within(modal).getByText<HTMLButtonElement>('Rename')
+            confirmButton = within(modal).getByRole('button', {
+              name: 'Rename',
+            }) as HTMLButtonElement
             expect(confirmButton.disabled).to.be.true
           })
 
@@ -887,8 +913,9 @@ describe('<ProjectListRoot />', function () {
               target: { value: newProjectName },
             })
 
-            const confirmButton =
-              within(modal).getByText<HTMLButtonElement>('Rename')
+            const confirmButton = within(modal).getByRole('button', {
+              name: 'Rename',
+            }) as HTMLButtonElement
             expect(confirmButton.disabled).to.be.false
             fireEvent.click(confirmButton)
 
@@ -1033,7 +1060,7 @@ describe('<ProjectListRoot />', function () {
           selectedMatchesDisplayed(2)
         })
 
-        it('shows correct list after closing modal, changing selecting, and reopening modal', async function () {
+        it('shows correct list after closing modal, changing selection, and reopening modal', async function () {
           selectedMatchesDisplayed(2)
 
           const modal = screen.getAllByRole('dialog', { hidden: false })[0]
@@ -1041,7 +1068,10 @@ describe('<ProjectListRoot />', function () {
             name: 'Cancel',
           })
           fireEvent.click(cancelButton)
-          expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
+
+          await waitForElementToBeRemoved(
+            screen.getByRole('dialog', { hidden: false })
+          )
 
           await screen.findAllByRole('checkbox')
           fireEvent.click(allCheckboxes[3])

+ 3 - 0
services/web/test/frontend/features/project-list/components/project-list-title.tsx

@@ -4,6 +4,9 @@ import { Tag } from '../../../../../app/src/Features/Tags/types'
 import ProjectListTitle from '../../../../../frontend/js/features/project-list/components/title/project-list-title'
 
 describe('<ProjectListTitle />', function () {
+  beforeEach(function () {
+    window.metaAttributesCache.set('ol-bootstrapVersion', 5)
+  })
   type TestCase = {
     filter: Filter
     selectedTag: Tag | undefined

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

@@ -31,7 +31,7 @@ describe('Add affiliation widget', function () {
     await waitFor(() => expect(fetchMock.called('/api/project')))
 
     screen.getByText(/are you affiliated with an institution/i)
-    const addAffiliationLink = screen.getByRole('link', {
+    const addAffiliationLink = screen.getByRole('button', {
       name: /add affiliation/i,
     })
     expect(addAffiliationLink.getAttribute('href')).to.equal('/user/settings')

+ 4 - 4
services/web/test/frontend/features/project-list/components/survey-widget.test.tsx

@@ -36,7 +36,7 @@ describe('<SurveyWidgetDsNav />', function () {
       screen.getByText(this.preText)
       screen.getByText(this.linkText)
 
-      const link = screen.getByRole('link', {
+      const link = screen.getByRole('button', {
         name: 'Take survey',
       }) as HTMLAnchorElement
       expect(link.href).to.equal(this.url)
@@ -51,7 +51,7 @@ describe('<SurveyWidgetDsNav />', function () {
       const text = screen.queryByText(this.preText)
       expect(text).to.be.null
 
-      const link = screen.queryByRole('link')
+      const link = screen.queryByRole('button')
       expect(link).to.be.null
 
       const dismissed = localStorage.getItem('dismissed-my-survey')
@@ -80,7 +80,7 @@ describe('<SurveyWidgetDsNav />', function () {
       const text = screen.queryByText(this.preText)
       expect(text).to.be.null
 
-      const link = screen.queryByRole('link')
+      const link = screen.queryByRole('button')
       expect(link).to.be.null
     })
   })
@@ -98,7 +98,7 @@ describe('<SurveyWidgetDsNav />', function () {
       const text = screen.queryByText(this.preText)
       expect(text).to.be.null
 
-      const link = screen.queryByRole('link')
+      const link = screen.queryByRole('button')
       expect(link).to.be.null
     })
   })

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

@@ -16,13 +16,13 @@ describe('<ArchiveProjectButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(
       <ArchiveProjectButtonTooltip project={archiveableProject} />
     )
     const btn = screen.getByRole('button', { name: 'Archive' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Archive' })
+    await screen.findByRole('tooltip', { name: 'Archive' })
   })
 
   it('opens the modal when clicked', function () {
@@ -59,7 +59,9 @@ describe('<ArchiveProjectButton />', function () {
     screen.getByText('Archive Projects')
     screen.getByText('You are about to archive the following projects:')
     screen.getByText('Archiving projects won’t affect your collaborators.')
-    const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+    const confirmBtn = screen.getByRole('button', {
+      name: 'Confirm',
+    }) as HTMLButtonElement
     fireEvent.click(confirmBtn)
     expect(confirmBtn.disabled).to.be.true
 

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

@@ -32,10 +32,10 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
     sendMBSpy.restore()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     const btn = screen.getByRole('button', { name: 'Download PDF' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Download PDF' })
+    await screen.findByRole('tooltip', { name: 'Download PDF' })
   })
 
   it('downloads the project PDF when clicked', async function () {

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

@@ -17,13 +17,13 @@ describe('<CopyProjectButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(
       <CopyProjectButtonTooltip project={copyableProject} />
     )
     const btn = screen.getByRole('button', { name: 'Copy' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Copy' })
+    await screen.findByRole('tooltip', { name: 'Copy' })
   })
 
   it('does not render the button when project is archived', function () {
@@ -51,14 +51,16 @@ describe('<CopyProjectButton />', function () {
     renderWithProjectListContext(
       <CopyProjectButtonTooltip project={copyableProject} />
     )
+
     const btn = screen.getByRole('button', { name: 'Copy' })
+
     fireEvent.click(btn)
     screen.getByText('Copy Project')
     screen.getByLabelText('New Name')
     screen.getByDisplayValue(`${copyableProject.name} (Copy)`)
-    const copyBtn = screen.getByRole<HTMLButtonElement>('button', {
+    const copyBtn = screen.getAllByRole<HTMLButtonElement>('button', {
       name: 'Copy',
-    })
+    })[1]
     fireEvent.click(copyBtn)
     expect(copyBtn.disabled).to.be.true
 

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

@@ -17,14 +17,14 @@ describe('<DeleteProjectButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     window.metaAttributesCache.set('ol-user_id', trashedProject.owner?.id)
     renderWithProjectListContext(
       <DeleteProjectButtonTooltip project={trashedProject} />
     )
     const btn = screen.getByRole('button', { name: 'Delete' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Delete' })
+    await screen.findByRole('tooltip', { name: 'Delete' })
   })
 
   it('does not render button when trashed and not owner', function () {
@@ -61,7 +61,9 @@ describe('<DeleteProjectButton />', function () {
     screen.getByText('Delete Projects')
     screen.getByText('You are about to delete the following projects:')
     screen.getByText('This action cannot be undone.')
-    const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+    const confirmBtn = screen.getByRole('button', {
+      name: 'Confirm',
+    }) as HTMLButtonElement
     fireEvent.click(confirmBtn)
     expect(confirmBtn.disabled).to.be.true
 

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

@@ -23,10 +23,10 @@ describe('<DownloadProjectButton />', function () {
     this.locationStub.restore()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     const btn = screen.getByRole('button', { name: 'Download .zip file' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Download .zip file' })
+    await screen.findByRole('tooltip', { name: 'Download .zip file' })
   })
 
   it('downloads the project when clicked', async function () {

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

@@ -18,13 +18,13 @@ describe('<LeaveProjectButtton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(
       <LeaveProjectButtonTooltip project={trashedAndNotOwnedProject} />
     )
     const btn = screen.getByRole('button', { name: 'Leave' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Leave' })
+    await screen.findByRole('tooltip', { name: 'Leave' })
   })
 
   it('does not render button when owner', function () {
@@ -67,7 +67,9 @@ describe('<LeaveProjectButtton />', function () {
     screen.getByText('Leave Projects')
     screen.getByText('You are about to leave the following projects:')
     screen.getByText('This action cannot be undone.')
-    const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+    const confirmBtn = screen.getByRole('button', {
+      name: 'Confirm',
+    }) as HTMLButtonElement
     fireEvent.click(confirmBtn)
     expect(confirmBtn.disabled).to.be.true
 

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

@@ -51,7 +51,9 @@ describe('<RenameProjectButton />', function () {
     const btn = screen.getByRole('button')
     fireEvent.click(btn)
     screen.getByText('Rename Project')
-    const confirmBtn = screen.getByText('Rename') as HTMLButtonElement
+    const confirmBtn = screen.getByRole('button', {
+      name: 'Rename',
+    }) as HTMLButtonElement
     expect(confirmBtn.disabled).to.be.true
     const nameInput = screen.getByDisplayValue(ownedProject.name)
     fireEvent.change(nameInput, { target: { value: 'new name' } })

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

@@ -16,13 +16,13 @@ describe('<TrashProjectButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(
       <TrashProjectButtonTooltip project={archivedProject} />
     )
     const btn = screen.getByRole('button', { name: 'Trash' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Trash' })
+    await screen.findByRole('tooltip', { name: 'Trash' })
   })
 
   it('does not render the button when project is trashed', function () {
@@ -49,7 +49,9 @@ describe('<TrashProjectButton />', function () {
     screen.getByText('Trash Projects')
     screen.getByText('You are about to trash the following projects:')
     screen.getByText('Trashing projects won’t affect your collaborators.')
-    const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+    const confirmBtn = screen.getByRole('button', {
+      name: 'Confirm',
+    }) as HTMLButtonElement
     fireEvent.click(confirmBtn)
     expect(confirmBtn.disabled).to.be.true
 

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

@@ -17,13 +17,13 @@ describe('<UnarchiveProjectButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(
       <UnarchiveProjectButtonTooltip project={archivedProject} />
     )
     const btn = screen.getByRole('button', { name: 'Restore' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Restore' })
+    await screen.findByRole('tooltip', { name: 'Restore' })
   })
 
   it('does not render the button when project is trashed', function () {

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

@@ -16,13 +16,13 @@ describe('<UntrashProjectButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(
       <UntrashProjectButtonTooltip project={trashedProject} />
     )
     const btn = screen.getByRole('button', { name: 'Restore' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Restore' })
+    await screen.findByRole('tooltip', { name: 'Restore' })
   })
 
   it('does not render the button when project is current', function () {

+ 2 - 2
services/web/test/frontend/features/project-list/components/table/project-tools/buttons/archive-projects.button.test.tsx

@@ -10,11 +10,11 @@ describe('<ArchiveProjectsButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(<ArchiveProjectsButton />)
     const btn = screen.getByRole('button', { name: 'Archive' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Archive' })
+    await screen.findByRole('tooltip', { name: 'Archive' })
   })
 
   it('opens the modal when clicked', function () {

+ 2 - 2
services/web/test/frontend/features/project-list/components/table/project-tools/buttons/download-projects-button.test.tsx

@@ -10,10 +10,10 @@ describe('<DownloadProjectsButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(<DownloadProjectsButton />)
     const btn = screen.getByRole('button', { name: 'Download' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Download' })
+    await screen.findByRole('tooltip', { name: 'Download' })
   })
 })

+ 2 - 2
services/web/test/frontend/features/project-list/components/table/project-tools/buttons/trash-projects.button.test.tsx

@@ -10,11 +10,11 @@ describe('<TrashProjectsButton />', function () {
     resetProjectListContextFetch()
   })
 
-  it('renders tooltip for button', function () {
+  it('renders tooltip for button', async function () {
     renderWithProjectListContext(<TrashProjectsButton />)
     const btn = screen.getByRole('button', { name: 'Trash' })
     fireEvent.mouseOver(btn)
-    screen.getByRole('tooltip', { name: 'Trash' })
+    await screen.findByRole('tooltip', { name: 'Trash' })
   })
 
   it('opens the modal when clicked', function () {

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

@@ -32,10 +32,14 @@ describe('<RenameProjectModal />', function () {
       />
     )
     screen.getByText('Rename Project')
-    const input = screen.getByLabelText('New Name') as HTMLButtonElement
+    const input = screen.getByRole('textbox', {
+      name: 'New Name',
+    }) as HTMLInputElement
     expect(input.value).to.equal(currentProjects[0].name)
 
-    const submitButton = screen.getByText('Rename') as HTMLButtonElement
+    const submitButton = screen.getByRole('button', {
+      name: 'Rename',
+    }) as HTMLButtonElement
     expect(submitButton.disabled).to.be.true
 
     fireEvent.change(input, {

+ 3 - 1
services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx

@@ -32,7 +32,9 @@ describe('<ProjectsActionModal />', function () {
         showModal
       />
     )
-    const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
+    const confirmBtn = screen.getByRole('button', {
+      name: 'Confirm',
+    }) as HTMLButtonElement
     fireEvent.click(confirmBtn)
     expect(confirmBtn.disabled).to.be.true
     // verify action handled

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

@@ -90,7 +90,7 @@ describe('<AccountInfoSection />', function () {
         name: 'Update',
       })
     )
-    await screen.findByText('Saving…')
+    await screen.findByRole('button', { name: 'Loading' })
 
     finishUpdateCall(200)
     await screen.findByRole('button', {

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

@@ -172,11 +172,7 @@ describe('email actions - make primary', function () {
       fireEvent.click(
         withinModal.getByRole('button', { name: 'Change primary email' })
       )
-      expect(screen.queryByRole('dialog')).to.be.null
-
-      await waitForElementToBeRemoved(() =>
-        screen.getByRole('button', { name: /Processing/i, hidden: true })
-      )
+      await waitForElementToBeRemoved(() => screen.getByRole('dialog'))
     }
 
     it('shows confirmation modal and closes it', async function () {
@@ -201,13 +197,14 @@ describe('email actions - make primary', function () {
       withinModal.getByRole('button', { name: 'Change primary email' })
 
       fireEvent.click(withinModal.getByRole('button', { name: /cancel/i }))
-      expect(screen.queryByRole('dialog')).to.be.null
+
+      await waitForElementToBeRemoved(screen.getByRole('dialog'))
     })
 
     it('shows loader and removes button', async function () {
       fetchMock
         .get('/user/emails?ensureAffiliation=true', [userEmailData])
-        .post('/user/emails/default?delete-primary-unconfirmed', 200)
+        .post('/user/emails/default?delete-unconfirmed-primary', 200)
       render(<EmailsSection />)
 
       await confirmPrimaryEmail()
@@ -223,7 +220,7 @@ describe('email actions - make primary', function () {
     it('shows error', async function () {
       fetchMock
         .get('/user/emails?ensureAffiliation=true', [userEmailData])
-        .post('/user/emails/default?delete-primary-unconfirmed', 503)
+        .post('/user/emails/default?delete-unconfirmed-primary', 503)
       render(<EmailsSection />)
 
       await confirmPrimaryEmail()

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

@@ -69,7 +69,7 @@ async function confirmCodeForEmail(email: string) {
   })
   fireEvent.click(submitCodeBtn)
   await waitForElementToBeRemoved(() =>
-    screen.getByRole('button', { name: /confirming/i })
+    screen.getByRole('button', { name: /Loading/i })
   )
 }
 
@@ -216,7 +216,7 @@ describe('<EmailsSection />', function () {
 
     await waitForElementToBeRemoved(() =>
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: /Loading/i,
       })
     )
 

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

@@ -134,7 +134,7 @@ describe('<EmailsSection />', function () {
       screen.queryByText(/an error has occurred while performing your request/i)
     ).to.be.null
 
-    await screen.findByRole('button', {
+    await screen.findAllByRole('button', {
       name: /resend confirmation code/i,
     })
   })

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

@@ -3,7 +3,7 @@ import { expect } from 'chai'
 import { SSOAlert } from '../../../../../../frontend/js/features/settings/components/emails/sso-alert'
 
 describe('<SSOAlert/>', function () {
-  describe('when thereis no institutional linking information', function () {
+  describe('when there is no institutional linking information', function () {
     it('should be empty', function () {
       render(<SSOAlert />)
       expect(screen.queryByRole('alert')).to.be.null
@@ -49,11 +49,11 @@ describe('<SSOAlert/>', function () {
       )
       render(<SSOAlert />)
       const closeButtons = screen.getAllByRole('button', {
-        name: 'Close alert',
+        name: 'Close',
       })
       fireEvent.click(closeButtons[0])
       fireEvent.click(closeButtons[1])
-      expect(screen.queryByRole('button', { name: 'Close alert' })).to.be.null
+      expect(screen.queryByRole('button', { name: 'Close' })).to.be.null
     })
   })
 
@@ -87,10 +87,10 @@ describe('<SSOAlert/>', function () {
     it('the alert should be closeable', function () {
       render(<SSOAlert />)
       const closeButton = screen.getByRole('button', {
-        name: 'Close alert',
+        name: 'Close',
       })
       fireEvent.click(closeButton)
-      expect(screen.queryByRole('button', { name: 'Close alert' })).to.be.null
+      expect(screen.queryByRole('button', { name: 'Close' })).to.be.null
     })
   })
 })

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

@@ -78,7 +78,7 @@ describe('<LinkingSection />', function () {
     )
     const helpLink = screen.getByRole('link', { name: 'Learn more' })
     expect(helpLink.getAttribute('href')).to.equal('/blog/434')
-    const linkButton = screen.getByRole('link', { name: 'Link' })
+    const linkButton = screen.getByRole('button', { name: 'Link' })
     expect(linkButton.getAttribute('href')).to.equal('/auth/orcid?intent=link')
   })
 

+ 8 - 7
services/web/test/frontend/features/settings/components/linking/integration-widget.test.tsx

@@ -1,6 +1,6 @@
 import { expect } from 'chai'
 import sinon from 'sinon'
-import { screen, fireEvent, render } from '@testing-library/react'
+import { screen, fireEvent, render, within } from '@testing-library/react'
 import { IntegrationLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/integration-widget'
 import * as eventTracking from '@/infrastructure/event-tracking'
 
@@ -32,7 +32,7 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
     })
 
     it('should render an upgrade link and track clicks', function () {
-      const upgradeLink = screen.getByRole('link', { name: 'Upgrade' })
+      const upgradeLink = screen.getByRole('button', { name: 'Upgrade' })
       expect(upgradeLink.getAttribute('href')).to.equal(
         '/user/subscription/plans'
       )
@@ -51,7 +51,7 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
 
     it('should render a link to initiate integration linking', function () {
       expect(
-        screen.getByRole('link', { name: 'Link' }).getAttribute('href')
+        screen.getByRole('button', { name: 'Link' }).getAttribute('href')
       ).to.equal('/link')
     })
 
@@ -90,10 +90,11 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
 
     it('should open a modal with a link to confirm integration unlinking', function () {
       fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
-      screen.getByText('confirm unlink')
-      screen.getByText('you will be unlinked')
-      screen.getByRole('button', { name: 'Cancel' })
-      screen.getByRole('button', { name: 'Unlink' })
+      const withinModal = within(screen.getByRole('dialog'))
+      withinModal.getByText('confirm unlink')
+      withinModal.getByText('you will be unlinked')
+      withinModal.getByRole('button', { name: 'Cancel' })
+      withinModal.getByRole('button', { name: 'Unlink' })
     })
 
     it('should cancel unlinking when clicking "cancel" in the confirmation modal', async function () {

+ 16 - 7
services/web/test/frontend/features/settings/components/linking/sso-widget.test.tsx

@@ -1,6 +1,12 @@
 import { expect } from 'chai'
 import sinon from 'sinon'
-import { screen, fireEvent, render, waitFor } from '@testing-library/react'
+import {
+  screen,
+  fireEvent,
+  render,
+  waitFor,
+  within,
+} from '@testing-library/react'
 import { FetchError } from '../../../../../../frontend/js/infrastructure/fetch-json'
 import { SSOLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/sso-widget'
 
@@ -27,7 +33,7 @@ describe('<SSOLinkingWidget />', function () {
     it('should render a link to `linkPath`', function () {
       render(<SSOLinkingWidget {...defaultProps} linked={false} />)
       expect(
-        screen.getByRole('link', { name: 'Link' }).getAttribute('href')
+        screen.getByRole('button', { name: 'Link' }).getAttribute('href')
       ).to.equal('/integration/link?intent=link')
     })
   })
@@ -75,7 +81,7 @@ describe('<SSOLinkingWidget />', function () {
         <SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
       )
       fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
-      confirmBtn = screen.getByRole('button', {
+      confirmBtn = within(screen.getByRole('dialog')).getByRole('button', {
         name: 'Unlink',
         hidden: false,
       })
@@ -109,10 +115,13 @@ describe('<SSOLinkingWidget />', function () {
         <SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
       )
       fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
-      const confirmBtn = screen.getByRole('button', {
-        name: 'Unlink',
-        hidden: false,
-      })
+      const confirmBtn = within(screen.getByRole('dialog')).getByRole(
+        'button',
+        {
+          name: 'Unlink',
+          hidden: false,
+        }
+      )
       fireEvent.click(confirmBtn)
     })
 

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

@@ -145,7 +145,7 @@ describe('<PasswordSection />', function () {
     render(<PasswordSection />)
     submitValidForm()
 
-    await screen.findByText('Saving…')
+    await screen.findByRole('button', { name: 'Saving…' })
 
     finishUpdateCall({
       status: 200,

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

@@ -22,7 +22,7 @@ describe('<SecuritySection />', function () {
     render(<SecuritySection />)
 
     expect(screen.getAllByText('Single Sign-On (SSO)').length).to.equal(2)
-    const link = screen.getByRole('link', {
+    const link = screen.getByRole('button', {
       name: /Set up SSO/i,
     })
     expect(link).to.exist

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

@@ -92,7 +92,9 @@ describe('<GroupSubscriptionMemberships />', function () {
         </SplitTestProvider>
       )
 
-      const leaveGroupButton = screen.getByText('Leave group')
+      const leaveGroupButton = screen.getByRole('button', {
+        name: 'Leave group',
+      })
       fireEvent.click(leaveGroupButton)
 
       this.confirmModal = screen.getByRole('dialog')
@@ -100,8 +102,12 @@ describe('<GroupSubscriptionMemberships />', function () {
         'Are you sure you want to leave this group?'
       )
 
-      this.cancelButton = within(this.confirmModal).getByText('Cancel')
-      this.leaveNowButton = within(this.confirmModal).getByText('Leave now')
+      this.cancelButton = within(this.confirmModal).getByRole('button', {
+        name: 'Cancel',
+      })
+      this.leaveNowButton = within(this.confirmModal).getByRole('button', {
+        name: 'Leave now',
+      })
     })
 
     afterEach(function () {

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

@@ -89,7 +89,7 @@ describe('<PersonalSubscription />', function () {
 
       screen.getByText('No further payments will be taken.', { exact: false })
 
-      screen.getByRole('link', { name: 'View your invoices' })
+      screen.getByRole('button', { name: 'View your invoices' })
       screen.getByRole('button', { name: 'Reactivate your subscription' })
     })
 

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

@@ -323,7 +323,7 @@ describe('<ActiveSubscription />', function () {
       fireEvent.click(button)
 
       const cancelButton = screen.getByRole('button', {
-        name: 'Loading',
+        name: 'Processing…',
       }) as HTMLButtonElement
       expect(cancelButton.disabled).to.be.true
 
@@ -368,7 +368,7 @@ describe('<ActiveSubscription />', function () {
           name: cancelButtonText,
         })
         screen.getByRole('button', {
-          name: 'Loading',
+          name: 'Processing…',
         })
       })
 
@@ -385,7 +385,7 @@ describe('<ActiveSubscription />', function () {
         expect(buttons[0].getAttribute('disabled')).to.equal('')
         expect(buttons[1].getAttribute('disabled')).to.equal('')
         screen.getByRole('button', {
-          name: 'Loading',
+          name: 'Processing…',
         })
         screen.getByRole('button', {
           name: extendTrialButtonText,
@@ -456,7 +456,7 @@ describe('<ActiveSubscription />', function () {
           name: cancelButtonText,
         })
         screen.getByRole('button', {
-          name: 'Loading',
+          name: 'Processing…',
         })
       })
 
@@ -473,7 +473,7 @@ describe('<ActiveSubscription />', function () {
         expect(buttons[0].getAttribute('disabled')).to.equal('')
         expect(buttons[1].getAttribute('disabled')).to.equal('')
         screen.getByRole('button', {
-          name: 'Loading',
+          name: 'Processing…',
         })
         screen.getByRole('button', {
           name: downgradeButtonText,

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

@@ -118,10 +118,14 @@ describe('<ChangePlanModal />', function () {
       })
       fireEvent.click(buttons[0])
 
-      await screen.findByText('Are you sure you want to change plan to', {
-        exact: false,
-      })
-      screen.getByRole('button', { name: 'Change plan' })
+      const confirmModal = screen.getByRole('dialog')
+      await within(confirmModal).findByText(
+        'Are you sure you want to change plan to',
+        {
+          exact: false,
+        }
+      )
+      within(confirmModal).getByRole('button', { name: 'Change plan' })
 
       expect(
         screen.queryByText(
@@ -197,10 +201,13 @@ describe('<ChangePlanModal />', function () {
       await screen.findByText('Are you sure you want to change plan to', {
         exact: false,
       })
-      const buttonConfirm = screen.getByRole('button', { name: 'Change plan' })
+      const buttonConfirm = within(screen.getByRole('dialog')).getByRole(
+        'button',
+        { name: 'Change plan' }
+      )
       fireEvent.click(buttonConfirm)
 
-      screen.getByText('processing', { exact: false })
+      screen.getByRole('button', { name: 'Processing…' })
 
       // page is reloaded on success
       await waitFor(() => {
@@ -230,10 +237,13 @@ describe('<ChangePlanModal />', function () {
       await screen.findByText('Are you sure you want to change plan to', {
         exact: false,
       })
-      const buttonConfirm = screen.getByRole('button', { name: 'Change plan' })
+      const buttonConfirm = within(screen.getByRole('dialog')).getByRole(
+        'button',
+        { name: 'Change plan' }
+      )
       fireEvent.click(buttonConfirm)
 
-      screen.getByText('processing', { exact: false })
+      screen.getByRole('button', { name: 'Processing…' })
 
       await screen.findByText('Sorry, something went wrong. ', { exact: false })
       await screen.findByText('Please try again. ', { exact: false })
@@ -286,7 +296,7 @@ describe('<ChangePlanModal />', function () {
       })
       fireEvent.click(buttonConfirm)
 
-      screen.getByText('processing', { exact: false })
+      screen.getByRole('button', { name: 'Processing…' })
 
       // page is reloaded on success
       await waitFor(() => {
@@ -304,7 +314,7 @@ describe('<ChangePlanModal />', function () {
       })
       fireEvent.click(buttonConfirm)
 
-      screen.getByText('processing', { exact: false })
+      screen.getByRole('button', { name: 'Processing…' })
       await screen.findByText('Sorry, something went wrong. ', { exact: false })
       await screen.findByText('Please try again. ', { exact: false })
       await screen.findByText('If the problem continues please contact us.', {
@@ -518,7 +528,7 @@ describe('<ChangePlanModal />', function () {
       const buttonConfirm = screen.getByRole('button', { name: 'Upgrade now' })
       fireEvent.click(buttonConfirm)
 
-      screen.getByText('processing', { exact: false })
+      screen.getByRole('button', { name: 'Processing…' })
 
       // // page is reloaded on success
       await waitFor(() => {
@@ -539,7 +549,7 @@ describe('<ChangePlanModal />', function () {
       const buttonConfirm = screen.getByRole('button', { name: 'Upgrade now' })
       fireEvent.click(buttonConfirm)
 
-      screen.getByText('processing', { exact: false })
+      screen.getByRole('button', { name: 'Processing…' })
 
       await screen.findByText('Sorry, something went wrong. ', { exact: false })
       await screen.findByText('Please try again. ', { exact: false })

+ 32 - 28
services/web/test/frontend/features/subscription/components/group-invite/accepted-invite.test.tsx

@@ -2,34 +2,38 @@ import { render, screen } from '@testing-library/react'
 import AcceptedInvite from '../../../../../../frontend/js/features/subscription/components/group-invite/accepted-invite'
 import { expect } from 'chai'
 
-describe('accepted group invite', function () {
-  it('renders', async function () {
-    window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
-    render(<AcceptedInvite />)
-    await screen.findByText(
-      'You have joined the group subscription managed by example@overleaf.com'
-    )
-  })
+for (const bsVersion of [3, 5])
+  describe('accepted group invite' + ` bs${bsVersion}`, function () {
+    beforeEach(function () {
+      window.metaAttributesCache.set('ol-bootstrapVersion', bsVersion)
+    })
+    it('renders', async function () {
+      window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
+      render(<AcceptedInvite />)
+      await screen.findByText(
+        'You have joined the group subscription managed by example@overleaf.com'
+      )
+    })
 
-  it('links to SSO enrollment page for SSO groups', async function () {
-    window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
-    window.metaAttributesCache.set('ol-groupSSOActive', true)
-    window.metaAttributesCache.set('ol-subscriptionId', 'group123')
-    render(<AcceptedInvite />)
-    const linkBtn = (await screen.findByRole('link', {
-      name: 'Done',
-    })) as HTMLLinkElement
-    expect(linkBtn.href).to.equal(
-      'https://www.test-overleaf.com/subscription/group123/sso_enrollment'
-    )
-  })
+    it('links to SSO enrollment page for SSO groups', async function () {
+      window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
+      window.metaAttributesCache.set('ol-groupSSOActive', true)
+      window.metaAttributesCache.set('ol-subscriptionId', 'group123')
+      render(<AcceptedInvite />)
+      const linkBtn = (await screen.findByRole('link', {
+        name: 'Done',
+      })) as HTMLLinkElement
+      expect(linkBtn.href).to.equal(
+        'https://www.test-overleaf.com/subscription/group123/sso_enrollment'
+      )
+    })
 
-  it('links to dash for non-SSO groups', async function () {
-    window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
-    render(<AcceptedInvite />)
-    const linkBtn = (await screen.findByRole('link', {
-      name: 'Done',
-    })) as HTMLLinkElement
-    expect(linkBtn.href).to.equal('https://www.test-overleaf.com/project')
+    it('links to dash for non-SSO groups', async function () {
+      window.metaAttributesCache.set('ol-inviterName', 'example@overleaf.com')
+      render(<AcceptedInvite />)
+      const linkBtn = (await screen.findByRole('link', {
+        name: 'Done',
+      })) as HTMLLinkElement
+      expect(linkBtn.href).to.equal('https://www.test-overleaf.com/project')
+    })
   })
-})

+ 90 - 85
services/web/test/frontend/features/subscription/components/group-invite/group-invite.test.tsx

@@ -2,37 +2,80 @@ import { render, screen } from '@testing-library/react'
 import { expect } from 'chai'
 import GroupInvite from '../../../../../../frontend/js/features/subscription/components/group-invite/group-invite'
 
-describe('group invite', function () {
-  const inviterName = 'example@overleaf.com'
-  beforeEach(function () {
-    window.metaAttributesCache.set('ol-inviterName', inviterName)
-  })
-
-  it('renders header', async function () {
-    render(<GroupInvite />)
-    await screen.findByText(inviterName)
-    screen.getByText(`has invited you to join a group subscription on Overleaf`)
-    expect(screen.queryByText('Email link expired, please request a new one.'))
-      .to.be.null
-  })
-
-  describe('when user has personal subscription', function () {
+for (const bsVersion of [3, 5])
+  describe('group invite' + ` bs${bsVersion}`, function () {
+    window.metaAttributesCache.set('ol-bootstrapVersion', bsVersion)
+    const inviterName = 'example@overleaf.com'
     beforeEach(function () {
-      window.metaAttributesCache.set(
-        'ol-hasIndividualRecurlySubscription',
-        true
-      )
+      window.metaAttributesCache.set('ol-inviterName', inviterName)
     })
 
-    it('renders cancel personal subscription view', async function () {
+    it('renders header', async function () {
       render(<GroupInvite />)
-      await screen.findByText(
-        'You already have an individual subscription, would you like us to cancel this first before joining the group licence?'
+      await screen.findByText(inviterName)
+      screen.getByText(
+        `has invited you to join a group subscription on Overleaf`
       )
+      expect(
+        screen.queryByText('Email link expired, please request a new one.')
+      ).to.be.null
     })
 
-    describe('and in a managed group', function () {
-      // note: this should not be possible but managed user view takes priority over all
+    describe('when user has personal subscription', function () {
+      beforeEach(function () {
+        window.metaAttributesCache.set(
+          'ol-hasIndividualRecurlySubscription',
+          true
+        )
+      })
+
+      it('renders cancel personal subscription view', async function () {
+        render(<GroupInvite />)
+        await screen.findByText(
+          'You already have an individual subscription, would you like us to cancel this first before joining the group licence?'
+        )
+      })
+
+      describe('and in a managed group', function () {
+        // note: this should not be possible but managed user view takes priority over all
+        beforeEach(function () {
+          window.metaAttributesCache.set(
+            'ol-currentManagedUserAdminEmail',
+            'example@overleaf.com'
+          )
+          window.metaAttributesCache.set('ol-cannot-join-subscription', true)
+        })
+
+        it('renders managed user cannot join view', async function () {
+          render(<GroupInvite />)
+          await screen.findByText('You can’t join this group subscription')
+          screen.getByText(
+            'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you can’t join additional group subscriptions',
+            { exact: false }
+          )
+          screen.getByRole('link', { name: 'Read more about Managed Users.' })
+        })
+      })
+    })
+
+    describe('when user does not have a personal subscription', function () {
+      beforeEach(function () {
+        window.metaAttributesCache.set(
+          'ol-hasIndividualRecurlySubscription',
+          false
+        )
+        window.metaAttributesCache.set('ol-inviteToken', 'token123')
+      })
+
+      it('does not render cancel personal subscription view', async function () {
+        render(<GroupInvite />)
+        await screen.findByText(
+          'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
+        )
+      })
+    })
+
+    describe('when the user is already a managed user in another group', function () {
       beforeEach(function () {
         window.metaAttributesCache.set(
           'ol-currentManagedUserAdminEmail',
@@ -43,7 +86,11 @@ describe('group invite', function () {
 
       it('renders managed user cannot join view', async function () {
         render(<GroupInvite />)
-        await screen.findByText('You can’t join this group subscription')
+        await screen.findByText(inviterName)
+        screen.getByText(
+          `has invited you to join a group subscription on Overleaf`
+        )
+        screen.getByText('You can’t join this group subscription')
         screen.getByText(
           'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you can’t join additional group subscriptions',
           { exact: false }
@@ -51,70 +98,28 @@ describe('group invite', function () {
         screen.getByRole('link', { name: 'Read more about Managed Users.' })
       })
     })
-  })
-
-  describe('when user does not have a personal subscription', function () {
-    beforeEach(function () {
-      window.metaAttributesCache.set(
-        'ol-hasIndividualRecurlySubscription',
-        false
-      )
-      window.metaAttributesCache.set('ol-inviteToken', 'token123')
-    })
-
-    it('does not render cancel personal subscription view', async function () {
-      render(<GroupInvite />)
-      await screen.findByText(
-        'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
-      )
-    })
-  })
-
-  describe('when the user is already a managed user in another group', function () {
-    beforeEach(function () {
-      window.metaAttributesCache.set(
-        'ol-currentManagedUserAdminEmail',
-        'example@overleaf.com'
-      )
-      window.metaAttributesCache.set('ol-cannot-join-subscription', true)
-    })
 
-    it('renders managed user cannot join view', async function () {
-      render(<GroupInvite />)
-      await screen.findByText(inviterName)
-      screen.getByText(
-        `has invited you to join a group subscription on Overleaf`
-      )
-      screen.getByText('You can’t join this group subscription')
-      screen.getByText(
-        'Your Overleaf account is managed by your current group admin (example@overleaf.com). This means you can’t join additional group subscriptions',
-        { exact: false }
-      )
-      screen.getByRole('link', { name: 'Read more about Managed Users.' })
-    })
-  })
-
-  describe('expired', function () {
-    beforeEach(function () {
-      window.metaAttributesCache.set('ol-expired', true)
-    })
+    describe('expired', function () {
+      beforeEach(function () {
+        window.metaAttributesCache.set('ol-expired', true)
+      })
 
-    it('shows error notification when expired', async function () {
-      render(<GroupInvite />)
-      await screen.findByText('Email link expired, please request a new one.')
+      it('shows error notification when expired', async function () {
+        render(<GroupInvite />)
+        await screen.findByText('Email link expired, please request a new one.')
+      })
     })
-  })
 
-  describe('join view', function () {
-    beforeEach(function () {
-      window.metaAttributesCache.set('ol-inviteToken', 'token123')
-    })
+    describe('join view', function () {
+      beforeEach(function () {
+        window.metaAttributesCache.set('ol-inviteToken', 'token123')
+      })
 
-    it('shows view to join group', async function () {
-      render(<GroupInvite />)
-      await screen.findByText(
-        'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
-      )
+      it('shows view to join group', async function () {
+        render(<GroupInvite />)
+        await screen.findByText(
+          'Please click the button below to join the group subscription and enjoy the benefits of an upgraded Overleaf account'
+        )
+      })
     })
   })
-})

+ 1 - 0
services/web/test/frontend/helpers/reset-meta.ts

@@ -2,6 +2,7 @@ export function resetMeta() {
   window.metaAttributesCache = new Map()
   window.metaAttributesCache.set('ol-projectHistoryBlobsEnabled', true)
   window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' })
+  window.metaAttributesCache.set('ol-bootstrapVersion', 5)
   window.metaAttributesCache.set('ol-ExposedSettings', {
     appName: 'Overleaf',
     maxEntitiesPerProject: 10,