瀏覽代碼

Update selectors to improve CE test stability (#28096)

* Update selectors to improve test stability

* Update selectors to improve test stability

* Use plain string matchers

* Fix test

* [monorepo] use plain string matchers everywhere

* [web] remove Kb/ prefix from title of learn wiki links

---------

Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: 12e13c39822795338a3bee20236454f9948e6221
Rebeka Dekany 11 月之前
父節點
當前提交
97d8f73749
共有 24 個文件被更改,包括 737 次插入477 次删除
  1. 1 1
      server-ce/test/accounts.spec.ts
  2. 110 70
      server-ce/test/admin.spec.ts
  3. 57 37
      server-ce/test/create-and-compile-project.spec.ts
  4. 98 46
      server-ce/test/editor.spec.ts
  5. 1 1
      server-ce/test/filestore-migration.spec.ts
  6. 73 44
      server-ce/test/git-bridge.spec.ts
  7. 1 1
      server-ce/test/helpers/compile.ts
  8. 13 7
      server-ce/test/helpers/project.ts
  9. 66 30
      server-ce/test/history.spec.ts
  10. 20 11
      server-ce/test/learn-wiki.spec.ts
  11. 28 18
      server-ce/test/project-list.spec.ts
  12. 31 22
      server-ce/test/project-sharing.spec.ts
  13. 83 62
      server-ce/test/sandboxed-compiles.spec.ts
  14. 19 21
      server-ce/test/templates.spec.ts
  15. 3 3
      server-ce/test/upgrading.spec.ts
  16. 1 1
      services/web/frontend/extracted-translations.json
  17. 14 3
      services/web/frontend/js/features/settings/components/linking/integration-widget.tsx
  18. 1 1
      services/web/locales/en.json
  19. 3 3
      services/web/test/frontend/features/history/components/change-list.spec.tsx
  20. 2 2
      services/web/test/frontend/features/settings/components/account-info-section.test.tsx
  21. 68 69
      services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx
  22. 3 3
      services/web/test/frontend/features/settings/components/linking-section.test.tsx
  23. 13 5
      services/web/test/frontend/features/settings/components/linking/integration-widget.test.tsx
  24. 28 16
      services/web/test/frontend/features/settings/components/linking/sso-widget.test.tsx

+ 1 - 1
server-ce/test/accounts.spec.ts

@@ -10,7 +10,7 @@ describe('Accounts', function () {
     login('user@example.com')
     cy.visit('/project')
     cy.findByRole('menuitem', { name: 'Account' }).click()
-    cy.findByText('Log Out').click()
+    cy.findByRole('menuitem', { name: 'Log Out' }).click()
     cy.url().should('include', '/login')
     cy.visit('/project')
     cy.url().should('include', '/login')

+ 110 - 70
server-ce/test/admin.spec.ts

@@ -14,7 +14,7 @@ describe('admin panel', function () {
   function registrationTests() {
     it('via GUI and opening URL manually', () => {
       const user = `${uuid()}@example.com`
-      cy.get('input[name="email"]').type(user + '{enter}')
+      cy.findByLabelText('Emails to register new users').type(user + '{enter}')
 
       cy.get('td')
         .contains(/\/user\/activate/)
@@ -26,7 +26,7 @@ describe('admin panel', function () {
 
     it('via GUI and email', () => {
       const user = `${uuid()}@example.com`
-      cy.get('input[name="email"]').type(user + '{enter}')
+      cy.findByLabelText('Emails to register new users').type(user + '{enter}')
 
       let url: string
       cy.get('td')
@@ -93,8 +93,8 @@ describe('admin panel', function () {
       beforeEach(() => {
         login(admin)
         cy.visit('/project')
-        cy.get('nav').findByText('Admin').click()
-        cy.get('nav').findByText('Manage Users').click()
+        cy.findByRole('menuitem', { name: 'Admin' }).click()
+        cy.findByRole('menuitem', { name: 'Manage Users' }).click()
       })
       registrationTests()
     })
@@ -139,17 +139,17 @@ describe('admin panel', function () {
       beforeEach(() => {
         login(admin)
         cy.visit('/project')
-        cy.get('nav').findByText('Admin').click()
-        cy.get('nav').findByText('Manage Site').click()
+        cy.findByRole('menuitem', { name: 'Admin' }).click()
+        cy.findByRole('menuitem', { name: 'Manage Site' }).click()
       })
 
       it('publish and clear admin messages', () => {
         const message = 'Admin Message ' + uuid()
 
         cy.log('create system message')
-        cy.get('[role="tab"]').contains('System Messages').click()
-        cy.get('input[name="content"]').type(message)
-        cy.get('button').contains('Post Message').click()
+        cy.findByRole('tab', { name: 'System Messages' }).click()
+        cy.findByLabelText('Message').type(message)
+        cy.findByRole('button', { name: 'Post Message' }).click()
         cy.findByText(message)
 
         login(user1)
@@ -159,10 +159,10 @@ describe('admin panel', function () {
         cy.log('clear system messages')
         login(admin)
         cy.visit('/project')
-        cy.get('nav').findByText('Admin').click()
-        cy.get('nav').findByText('Manage Site').click()
-        cy.get('[role="tab"]').contains('System Messages').click()
-        cy.get('button').contains('Clear all messages').click()
+        cy.findByRole('menuitem', { name: 'Admin' }).click()
+        cy.findByRole('menuitem', { name: 'Manage Site' }).click()
+        cy.findByRole('tab', { name: 'System Messages' }).click()
+        cy.findByRole('button', { name: 'Clear all messages' }).click()
 
         cy.log('verify system messages are no longer displayed')
         login(user1)
@@ -175,16 +175,16 @@ describe('admin panel', function () {
       beforeEach(() => {
         login(admin)
         cy.visit('/project')
-        cy.get('nav').findByText('Admin').click()
-        cy.get('nav').findByText('Manage Users').click()
+        cy.findByRole('menuitem', { name: 'Admin' }).click()
+        cy.findByRole('menuitem', { name: 'Manage Users' }).click()
       })
 
       it('displays expected tabs', () => {
         const tabs = ['Users', 'License Usage']
-        cy.get('[role="tab"]').each((el, index) => {
-          cy.wrap(el).findByText(tabs[index]).click()
+        cy.findAllByRole('tab').should('have.length', tabs.length)
+        tabs.forEach(tabName => {
+          cy.findByRole('tab', { name: tabName }).click()
         })
-        cy.get('[role="tab"]').should('have.length', tabs.length)
       })
 
       it('license usage tab', () => {
@@ -202,10 +202,12 @@ describe('admin panel', function () {
       })
 
       it('user list RegExp search', () => {
-        cy.get('input[name="isRegExpSearch"]').click()
-        cy.get('input[name="email"]').type('user[0-9]{enter}')
-        cy.findByText(user2)
-        cy.findByText(user1).should('not.exist')
+        cy.findByLabelText('RegExp').click()
+        cy.findByPlaceholderText('Search users by email or id…').type(
+          'user[0-9]{enter}'
+        )
+        cy.findByRole('link', { name: user2 })
+        cy.findByRole('link', { name: user1 }).should('not.exist')
       })
     })
 
@@ -213,10 +215,12 @@ describe('admin panel', function () {
       beforeEach(() => {
         login(admin)
         cy.visit('/project')
-        cy.get('nav').findByText('Admin').click()
-        cy.get('nav').findByText('Manage Users').click()
-        cy.get('input[name="email"]').type(user1 + '{enter}')
-        cy.findByText(user1).click()
+        cy.findByRole('menuitem', { name: 'Admin' }).click()
+        cy.findByRole('menuitem', { name: 'Manage Users' }).click()
+        cy.findByPlaceholderText('Search users by email or id…').type(
+          user1 + '{enter}'
+        )
+        cy.findByRole('link', { name: user1 }).click()
         cy.url().should('match', /\/admin\/user\/[a-fA-F0-9]{24}/)
       })
 
@@ -228,15 +232,15 @@ describe('admin panel', function () {
           'Audit Log',
           'Sessions',
         ]
-        cy.get('[role="tab"]').each((el, index) => {
-          cy.wrap(el).findByText(tabs[index]).click()
+        cy.findAllByRole('tab').should('have.length', tabs.length)
+        tabs.forEach(tabName => {
+          cy.findByRole('tab', { name: tabName }).click()
         })
-        cy.get('[role="tab"]').should('have.length', tabs.length)
       })
 
       describe('user info tab', () => {
         beforeEach(() => {
-          cy.get('[role="tab"]').contains('User Info').click()
+          cy.findByRole('tab', { name: 'User Info' }).click()
         })
 
         it('displays required sections', () => {
@@ -246,33 +250,61 @@ describe('admin panel', function () {
         })
 
         it('should not display SaaS-only sections', () => {
-          cy.findByText('Referred User Count').should('not.exist')
-          cy.findByText('Split Test Assignments').should('not.exist')
-          cy.findByText('Experimental Features').should('not.exist')
-          cy.findByText('Service Integration').should('not.exist')
-          cy.findByText('SSO Integrations').should('not.exist')
-          cy.findByText('Security').should('not.exist')
+          cy.findByLabelText('Referred User Count').should('not.exist')
+          cy.findByRole('heading', { name: /Split Test Assignments/ }).should(
+            'not.exist'
+          )
+          cy.findByRole('heading', { name: 'Experimental Features' }).should(
+            'not.exist'
+          )
+          cy.findByRole('heading', { name: 'Service Integration' }).should(
+            'not.exist'
+          )
+          cy.findByRole('heading', { name: 'SSO Integrations' }).should(
+            'not.exist'
+          )
+          cy.findByRole('heading', { name: 'Security' }).should('not.exist')
         })
       })
 
       it('transfer project ownership', () => {
         cy.log("access project admin through owners' project list")
-        cy.get('[role="tab"]').contains('Projects').click()
-        cy.get(`a[href="/admin/project/${testProjectId}"]`).click()
-
-        cy.findByText('Transfer Ownership').click()
-        cy.get('button[type="submit"]').should('be.disabled')
-        cy.get('input[name="user_id"]').type(user2)
-        cy.get('button[type="submit"]').should('not.be.disabled')
-        cy.get('button[type="submit"]').click()
-        cy.findByText('Transfer project to this user?')
-        cy.get('button').contains('Confirm').click()
+        cy.findByRole('tablist').within(() => {
+          cy.findByRole('tab', { name: 'Projects' }).click()
+        })
+        cy.get(`a[href="/admin/project/${testProjectId}"]`)
+          .should('contain.text', 'Project information')
+          .click()
+
+        cy.findByRole('button', { name: 'Transfer Ownership' }).click()
+        cy.findByRole('dialog').within(() => {
+          cy.findByRole('heading', { name: 'Transfer Ownership of Project' })
+          cy.findByRole('button', { name: 'Find' }).should('be.disabled')
+          cy.findByRole('button', { name: 'Confirm' }).should('be.disabled')
+          cy.findByPlaceholderText('User ID or Email').type(user2)
+          cy.findByRole('button', { name: 'Find' }).should('not.be.disabled')
+          cy.findByRole('button', { name: 'Find' }).click()
+          cy.findByText('Transfer project to this user?')
+          cy.findByRole('cell', { name: 'ID' })
+          cy.findByRole('cell', { name: 'Name' })
+          cy.findByRole('cell', { name: 'Email' })
+          cy.findByRole('cell', { name: user2 })
+          cy.findByRole('button', { name: 'Confirm' }).should('not.be.disabled')
+          cy.findByRole('button', { name: 'Confirm' }).click()
+        })
 
         cy.log('check the project is displayed in the new owner projects tab')
-        cy.get('input[name="email"]').type(user2 + '{enter}')
-        cy.findByText(user2).click()
-        cy.get('[role="tab"]').contains('Projects').click()
-        cy.get(`a[href="/admin/project/${testProjectId}"]`)
+        cy.findByPlaceholderText('Search users by email or id…').type(
+          user2 + '{enter}'
+        )
+        cy.findByRole('link', { name: user2 }).click()
+        cy.findByRole('tablist').within(() => {
+          cy.findByRole('tab', { name: 'Projects' }).click()
+        })
+        cy.get(`a[href="/admin/project/${testProjectId}"]`).should(
+          'contain.text',
+          'Project information'
+        )
       })
     })
 
@@ -284,10 +316,10 @@ describe('admin panel', function () {
 
       it('displays expected tabs', () => {
         const tabs = ['Project Info', 'Deleted Docs', 'Audit Log']
-        cy.get('[role="tab"]').each((el, index) => {
-          cy.wrap(el).findByText(tabs[index]).click()
+        cy.findAllByRole('tab').should('have.length', tabs.length)
+        tabs.forEach(tabName => {
+          cy.findByRole('tab', { name: tabName }).click()
         })
-        cy.get('[role="tab"]').should('have.length', tabs.length)
       })
     })
 
@@ -297,46 +329,54 @@ describe('admin panel', function () {
 
       cy.log('select project to delete')
       findProjectRow(deletedProjectName).within(() =>
-        cy.get('input[type="checkbox"]').first().check()
+        cy
+          .findByRole('checkbox', { name: `Select ${deletedProjectName}` })
+          .first()
+          .check()
       )
-
       cy.log('delete project')
       findProjectRow(deletedProjectName).within(() =>
         cy.findByRole('button', { name: 'Trash' }).click()
       )
-      cy.get('button').contains('Confirm').click()
-      cy.findByText(deletedProjectName).should('not.exist')
+      cy.findByRole('button', { name: 'Confirm' }).click()
+      cy.findByRole('link', { name: deletedProjectName }).should('not.exist')
 
       cy.log('navigate to thrashed projects and delete the project')
-      cy.get('.project-list-sidebar-scroll').within(() => {
-        cy.findByText('Trashed projects').click()
+      cy.findByRole('navigation', {
+        name: 'Project categories and tags',
       })
+        .findByRole('button', { name: 'Trashed projects' })
+        .click()
       findProjectRow(deletedProjectName).within(() =>
         cy.findByRole('button', { name: 'Delete' }).click()
       )
-      cy.get('button').contains('Confirm').click()
-      cy.findByText(deletedProjectName).should('not.exist')
+      cy.findByRole('button', { name: 'Confirm' }).click()
+      cy.findByRole('link', { name: deletedProjectName }).should('not.exist')
 
       cy.log('login as an admin and navigate to the deleted project')
       login(admin)
       cy.visit('/admin/user')
-      cy.get('input[name="email"]').type(user1 + '{enter}')
-      cy.get('a').contains(user1).click()
-      cy.findByText('Deleted Projects').click()
-      cy.get('a').contains(deletedProjectName).click()
+      cy.findByPlaceholderText('Search users by email or id…').type(
+        user1 + '{enter}'
+      )
+      cy.findByRole('link', { name: user1 }).click()
+      cy.findByRole('tab', { name: 'Deleted Projects' }).click()
+      cy.findByRole('link', { name: deletedProjectName }).click()
 
       cy.log('undelete the project')
-      cy.findByText('Undelete').click()
-      cy.findByText('Undelete').should('not.exist')
+      cy.findByRole('button', { name: 'Undelete' }).click()
+      cy.findByRole('button', { name: 'Undelete' }).should('not.exist')
       cy.url().should('contain', `/admin/project/${projectToDeleteId}`)
 
       cy.log('login as the user and verify the project is restored')
       login(user1)
       cy.visit('/project')
-      cy.get('.project-list-sidebar-scroll').within(() => {
-        cy.findByText('Trashed projects').click()
+      cy.findByRole('navigation', {
+        name: 'Project categories and tags',
       })
-      cy.findByText(`${deletedProjectName} (Restored)`)
+        .findByRole('button', { name: 'Trashed projects' })
+        .click()
+      cy.findByRole('link', { name: `${deletedProjectName} (Restored)` })
     })
   })
 })

+ 57 - 37
server-ce/test/create-and-compile-project.spec.ts

@@ -6,46 +6,59 @@ import {
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { throttledRecompile } from './helpers/compile'
 
+const USER = 'user@example.com'
+const COLLABORATOR = 'collaborator@example.com'
+
 describe('Project creation and compilation', function () {
   if (isExcludedBySharding('CE_DEFAULT')) return
   startWith({})
-  ensureUserExists({ email: 'user@example.com' })
-  ensureUserExists({ email: 'collaborator@example.com' })
+  ensureUserExists({ email: USER })
+  ensureUserExists({ email: COLLABORATOR })
 
   it('users can create project and compile it', function () {
-    login('user@example.com')
+    login(USER)
     createProject('test-project')
     const recompile = throttledRecompile()
-    cy.findByText('\\maketitle').parent().click()
-    cy.findByText('\\maketitle').parent().type('\n\\section{{}Test Section}')
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
+      cy.findByText('\\maketitle').parent().click()
+      cy.findByText('\\maketitle').parent().type('\n\\section{{}Test Section}')
+    })
     recompile()
-    cy.get('.pdf-viewer').should('contain.text', 'Test Section')
+    cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
+      cy.findByLabelText(/Page.*1/i).should('be.visible')
+      cy.findByText('Test Section').should('be.visible')
+    })
   })
 
   it('create and edit markdown file', function () {
     const fileName = `test-${Date.now()}.md`
     const markdownContent = '# Markdown title'
-    login('user@example.com')
+    login(USER)
     createProject('test-project')
 
-    // FIXME: Add aria-label maybe? or at least data-test-id
-    cy.findByText('New file').click({ force: true })
+    cy.findByRole('navigation', { name: 'Project files and outline' })
+      .findByRole('button', { name: 'New file' })
+      .click()
     cy.findByRole('dialog').within(() => {
-      cy.get('input').clear()
-      cy.get('input').type(fileName)
-      cy.findByText('Create').click()
+      cy.findByLabelText('File Name').clear().type(fileName)
+      cy.findByRole('button', { name: 'Create' }).click()
     })
-    cy.findByText(fileName).click()
+    cy.findByRole('button', { name: fileName }).click()
     // wait until we've switched to the newly created empty file
-    cy.get('.cm-line').should('have.length', 1)
-    cy.get('.cm-line').type(markdownContent)
-    cy.findByText('main.tex').click()
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
+      'have.length',
+      1
+    )
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).type(
+      markdownContent
+    )
+    cy.findByRole('button', { name: 'main.tex' }).click()
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       '\\maketitle'
     )
-    cy.findByText(fileName).click()
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('button', { name: fileName }).click()
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       markdownContent
     )
@@ -54,7 +67,7 @@ describe('Project creation and compilation', function () {
   it('can link and display linked image from other project', function () {
     const sourceProjectName = `test-project-${Date.now()}`
     const targetProjectName = `${sourceProjectName}-target`
-    login('user@example.com')
+    login(USER)
 
     createProject(sourceProjectName, {
       type: 'Example project',
@@ -63,15 +76,17 @@ describe('Project creation and compilation', function () {
     createProject(targetProjectName)
 
     // link the image from `projectName` into this project
-    cy.findByText('New file').click({ force: true })
+    cy.findByRole('button', { name: 'New file' }).click()
     cy.findByRole('dialog').within(() => {
-      cy.findByText('From another project').click()
+      cy.findByRole('button', { name: 'From another project' }).click()
       cy.findByLabelText('Select a Project').select(sourceProjectName)
       cy.findByLabelText('Select a File').select('frog.jpg')
-      cy.findByText('Create').click()
+      cy.findByRole('button', { name: 'Create' }).click()
     })
-    cy.findByTestId('file-tree').findByText('frog.jpg').click()
-    cy.findByText('Another project')
+    cy.findByRole('navigation', { name: 'Project files and outline' })
+      .findByRole('treeitem', { name: 'frog.jpg' })
+      .click()
+    cy.findByRole('link', { name: 'Another project' })
       .should('have.attr', 'href')
       .then(href => {
         cy.get('@sourceProjectId').then(sourceProjectId => {
@@ -83,7 +98,7 @@ describe('Project creation and compilation', function () {
   it('can refresh linked files as collaborator', function () {
     const sourceProjectName = `test-project-${Date.now()}`
     const targetProjectName = `${sourceProjectName}-target`
-    login('user@example.com')
+    login(USER)
     createProject(sourceProjectName, {
       type: 'Example project',
       open: false,
@@ -91,31 +106,36 @@ describe('Project creation and compilation', function () {
     createProject(targetProjectName).as('targetProjectId')
 
     // link the image from `projectName` into this project
-    cy.findByText('New file').click({ force: true })
+    cy.findByRole('navigation', { name: 'Project files and outline' })
+      .findByRole('button', { name: 'New file' })
+      .click()
+
     cy.findByRole('dialog').within(() => {
-      cy.findByText('From another project').click()
+      cy.findByRole('button', { name: 'From another project' }).click()
       cy.findByLabelText('Select a Project').select(sourceProjectName)
       cy.findByLabelText('Select a File').select('frog.jpg')
-      cy.findByText('Create').click()
+      cy.findByRole('button', { name: 'Create' }).click()
     })
 
-    cy.findByText('Share').click()
+    cy.findByRole('navigation', { name: 'Project actions' }).within(() => {
+      cy.findByRole('button', { name: 'Share' }).click()
+    })
     cy.findByRole('dialog').within(() => {
-      cy.findByTestId('collaborator-email-input').type(
-        'collaborator@example.com,'
-      )
-      cy.findByText('Invite').click({ force: true })
+      cy.findByTestId('collaborator-email-input').type(COLLABORATOR + ',')
+      cy.findByRole('button', { name: 'Invite' }).click()
       cy.findByText('Invite not yet accepted.')
     })
 
-    login('collaborator@example.com')
+    login(COLLABORATOR)
     openProjectViaInviteNotification(targetProjectName)
     cy.get('@targetProjectId').then(targetProjectId => {
       cy.url().should('include', targetProjectId)
     })
 
-    cy.findByTestId('file-tree').findByText('frog.jpg').click()
-    cy.findByText('Another project')
+    cy.findByRole('navigation', { name: 'Project files and outline' })
+      .findByRole('treeitem', { name: 'frog.jpg' })
+      .click()
+    cy.findByRole('link', { name: 'Another project' })
       .should('have.attr', 'href')
       .then(href => {
         cy.get('@sourceProjectId').then(sourceProjectId => {

+ 98 - 46
server-ce/test/editor.spec.ts

@@ -10,11 +10,14 @@ import { v4 as uuid } from 'uuid'
 import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
 import { prepareWaitForNextCompileSlot } from './helpers/compile'
 
+const USER = 'user@example.com'
+const COLLABORATOR = 'collaborator@example.com'
+
 describe('editor', () => {
   if (isExcludedBySharding('PRO_DEFAULT_1')) return
   startWith({ pro: true })
-  ensureUserExists({ email: 'user@example.com' })
-  ensureUserExists({ email: 'collaborator@example.com' })
+  ensureUserExists({ email: USER })
+  ensureUserExists({ email: COLLABORATOR })
 
   let projectName: string
   let projectId: string
@@ -22,7 +25,7 @@ describe('editor', () => {
   let waitForCompileRateLimitCoolOff: (fn: () => void) => void
   beforeWithReRunOnTestRetry(function () {
     projectName = `project-${uuid()}`
-    login('user@example.com')
+    login(USER)
     createProject(projectName, { type: 'Example project', open: false }).then(
       id => (projectId = id)
     )
@@ -31,7 +34,7 @@ describe('editor', () => {
   })
 
   beforeEach(() => {
-    login('user@example.com')
+    login(USER)
     waitForCompileRateLimitCoolOff(() => {
       openProjectById(projectId)
     })
@@ -40,9 +43,16 @@ describe('editor', () => {
   describe('spelling', function () {
     function changeSpellCheckLanguageTo(lng: string) {
       cy.log(`change project language to '${lng}'`)
-      cy.get('button').contains('Menu').click()
-      cy.get('select[id=settings-menu-spellCheckLanguage]').select(lng)
-      cy.get('[id="left-menu"]').type('{esc}') // close left menu
+      cy.findByRole('navigation', {
+        name: 'Project actions',
+      })
+        .findByRole('button', { name: 'Menu' })
+        .click()
+
+      cy.findByRole('dialog').within(() => {
+        cy.findByLabelText('Spell check').select(lng)
+      })
+      cy.get('body').type('{esc}')
     }
 
     afterEach(function () {
@@ -70,20 +80,28 @@ describe('editor', () => {
       cy.findByText(word).should('not.have.class', 'ol-cm-spelling-error')
 
       cy.log('remove word from dictionary')
-      cy.get('button').contains('Menu').click()
-      cy.get('button#dictionary-settings').contains('Edit').click()
-      cy.get('[id="dictionary-modal"]').within(() => {
+      cy.findByRole('navigation', {
+        name: 'Project actions',
+      })
+        .findByRole('button', { name: 'Menu' })
+        .click()
+      cy.findByRole('dialog').within(() => {
+        cy.findByLabelText('Dictionary').click()
+      })
+      cy.findByTestId('dictionary-modal').within(() => {
         cy.findByText(word)
           .parent()
-          .within(() => cy.get('button').click())
+          .within(() =>
+            cy.findByRole('button', { name: 'Remove from dictionary' }).click()
+          )
 
         // the modal has 2 close buttons, this ensures the one with the visible label is
         // clicked, otherwise it would need `force: true`
-        cy.get('.btn').contains('Close').click()
+        cy.contains('button', /close/i).click()
       })
 
       cy.log('close left panel')
-      cy.get('[id="left-menu"]').type('{esc}')
+      cy.findByTestId('left-menu').type('{esc}')
 
       cy.log('rewrite word to force spelling error')
       cy.get('.cm-line').type('{selectAll}{del}' + word + '{enter}')
@@ -94,7 +112,11 @@ describe('editor', () => {
 
   describe('editor', () => {
     it('renders jpg', () => {
-      cy.findByTestId('file-tree').findByText('frog.jpg').click()
+      cy.findByRole('navigation', {
+        name: 'Project files and outline',
+      })
+        .findByRole('treeitem', { name: 'frog.jpg' })
+        .click()
       cy.get('[alt="frog.jpg"]')
         .should('be.visible')
         .and('have.prop', 'naturalWidth')
@@ -108,7 +130,7 @@ describe('editor', () => {
         force: true,
       })
       cy.get('button').contains('𝜉').click()
-      cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+      cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
         'contain.text',
         '\\xi'
       )
@@ -120,23 +142,27 @@ describe('editor', () => {
 
   describe('add new file to project', () => {
     beforeEach(() => {
-      cy.get('button').contains('New file').click({ force: true })
+      cy.findByRole('button', { name: 'New file' }).click()
     })
 
     testNewFileUpload()
 
     it('should not display import from URL', () => {
-      cy.findByText('From external URL').should('not.exist')
+      cy.findByRole('button', { name: 'From external URL' }).should('not.exist')
     })
   })
 
   describe('left menu', () => {
     beforeEach(() => {
-      cy.get('button').contains('Menu').click()
+      cy.findByRole('navigation', {
+        name: 'Project actions',
+      })
+        .findByRole('button', { name: 'Menu' })
+        .click()
     })
 
     it('can download project sources', () => {
-      cy.get('a').contains('Source').click()
+      cy.findByRole('link', { name: 'Source' }).click()
       const zipName = projectName.replaceAll('-', '_')
       cy.task('readFileInZip', {
         pathToZip: `cypress/downloads/${zipName}.zip`,
@@ -146,10 +172,12 @@ describe('editor', () => {
 
     it('can download project PDF', () => {
       cy.log('ensure project is compiled')
-      cy.get('.pdf-viewer').should('contain.text', 'Your Paper')
-
-      cy.get('.nav-downloads').within(() => {
-        cy.findByText('PDF').click()
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'contain.text',
+        'Your Paper'
+      )
+      cy.findByRole('dialog').within(() => {
+        cy.findByRole('link', { name: 'PDF' }).click()
         const pdfName = projectName.replaceAll('-', '_')
         cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should(
           'contain',
@@ -160,11 +188,13 @@ describe('editor', () => {
 
     it('word count', () => {
       cy.log('ensure project is compiled')
-      cy.get('.pdf-viewer').should('contain.text', 'Your Paper')
-
-      cy.findByText('Word Count').click()
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'contain.text',
+        'Your Paper'
+      )
+      cy.findByRole('button', { name: 'Word Count' }).click()
 
-      cy.get('#word-count-modal').within(() => {
+      cy.findByTestId('word-count-modal').within(() => {
         cy.findByText('Total Words:')
         cy.findByText('607')
         cy.findByText('Headers:')
@@ -179,51 +209,73 @@ describe('editor', () => {
 
   describe('layout selector', () => {
     it('show editor only and switch between editor and pdf', () => {
-      cy.get('.pdf-viewer').should('be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'be.visible'
+      )
       cy.get('.cm-editor').should('be.visible')
 
-      cy.findByText('Layout').click()
-      cy.findByText('Editor only').click()
+      cy.findByRole('button', { name: 'Layout' }).click()
+      cy.findByRole('menu').within(() => {
+        cy.findByRole('menuitem', { name: /Editor only/ }).click()
+      })
 
-      cy.get('.pdf-viewer').should('not.be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'not.be.visible'
+      )
       cy.get('.cm-editor').should('be.visible')
 
-      cy.findByText('Switch to PDF').click()
+      cy.findByRole('button', { name: 'Switch to PDF' }).click()
 
-      cy.get('.pdf-viewer').should('be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'be.visible'
+      )
       cy.get('.cm-editor').should('not.be.visible')
 
-      cy.findByText('Switch to editor').click()
+      cy.findByRole('button', { name: 'Switch to editor' }).click()
 
-      cy.get('.pdf-viewer').should('not.be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'not.be.visible'
+      )
       cy.get('.cm-editor').should('be.visible')
     })
 
     it('show PDF only and go back to Editor & PDF', () => {
-      cy.get('.pdf-viewer').should('be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'be.visible'
+      )
       cy.get('.cm-editor').should('be.visible')
 
-      cy.findByText('Layout').click()
-      cy.findByText('PDF only').click()
+      cy.findByRole('button', { name: 'Layout' }).click()
+      cy.findByRole('menu').within(() => {
+        cy.findByRole('menuitem', { name: /PDF only/ }).click()
+      })
 
-      cy.get('.pdf-viewer').should('be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'be.visible'
+      )
       cy.get('.cm-editor').should('not.be.visible')
 
-      cy.findByText('Layout').click()
-      cy.findByText('Editor & PDF').click()
+      cy.findByRole('button', { name: 'Layout' }).click()
+      cy.findByRole('menu').within(() => {
+        cy.findByRole('menuitem', { name: 'Editor & PDF' }).click()
+      })
 
-      cy.get('.pdf-viewer').should('be.visible')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'be.visible'
+      )
       cy.get('.cm-editor').should('be.visible')
     })
 
     it('PDF in a separate tab (tests editor only)', () => {
-      cy.get('.pdf-viewer').should('be.visible')
+      cy.findByTestId('pdf-viewer').should('be.visible')
       cy.get('.cm-editor').should('be.visible')
 
-      cy.findByText('Layout').click()
-      cy.findByText('PDF in separate tab').click()
+      cy.findByRole('button', { name: 'Layout' }).click()
+      cy.findByRole('menu').within(() => {
+        cy.findByRole('menuitem', { name: 'PDF in separate tab' }).click()
+      })
 
-      cy.get('.pdf-viewer').should('not.exist')
+      cy.findByTestId('pdf-viewer').should('not.exist')
       cy.get('.cm-editor').should('be.visible')
     })
   })

+ 1 - 1
server-ce/test/filestore-migration.spec.ts

@@ -106,7 +106,7 @@ describe('filestore migration', function () {
         name: /Create First Project|New Project/,
       }).click()
       cy.findByRole('link', { name: 'Example Project' }).click()
-      cy.findByLabelText(/Project name/i).type(projectName)
+      cy.findByLabelText('Project name').type(projectName)
       cy.findByRole('button', { name: 'Create' }).click()
       cy.url()
         .should('match', /\/project\/[a-fA-F0-9]{24}/)

+ 73 - 44
server-ce/test/git-bridge.spec.ts

@@ -14,6 +14,8 @@ import http from 'isomorphic-git/http/web'
 import LightningFS from '@isomorphic-git/lightning-fs'
 import { throttledRecompile } from './helpers/compile'
 
+const USER = 'user@example.com'
+
 describe('git-bridge', function () {
   const ENABLED_VARS = {
     GIT_BRIDGE_ENABLED: 'true',
@@ -35,18 +37,21 @@ describe('git-bridge', function () {
       pro: true,
       vars: ENABLED_VARS,
     })
-    ensureUserExists({ email: 'user@example.com' })
+    ensureUserExists({ email: USER })
 
     function clearAllTokens() {
-      cy.get('button.linking-git-bridge-revoke-button').each(el => {
-        cy.wrap(el).click()
-        cy.findByText('Delete token').click()
-      })
+      cy.findAllByRole('button', { name: 'Remove' })
+        .not('[disabled]')
+        .each($button => {
+          cy.wrap($button).click()
+          cy.findByRole('button', { name: 'Delete token' }).click()
+        })
+      cy.findByRole('dialog').should('not.exist')
     }
 
     function maybeClearAllTokens() {
       cy.visit('/user/settings')
-      cy.findByText('Git integration')
+      cy.findByRole('heading', { name: 'Git integration' })
       cy.get('button')
         .contains(/Generate token|Add another token/)
         .then(btn => {
@@ -57,66 +62,83 @@ describe('git-bridge', function () {
     }
 
     beforeEach(function () {
-      login('user@example.com')
+      login(USER)
     })
 
     it('should render the git-bridge UI in the settings', () => {
       maybeClearAllTokens()
       cy.visit('/user/settings')
-      cy.findByText('Git integration')
-      cy.get('button').contains('Generate token').click()
-      cy.get('code')
+      cy.findByRole('heading', { name: 'Git integration' })
+      cy.findByRole('button', {
+        name: 'Git integration Generate token',
+      }).click()
+      cy.findByLabelText('Git authentication token')
         .contains(/olp_[a-zA-Z0-9]{16}/)
+        .then(el => el.text())
         .as('newToken')
       cy.findAllByText('Close').last().click()
       cy.get('@newToken').then(token => {
         // There can be more than one token with the same prefix when retrying
         cy.findAllByText(
-          `${token.text().slice(0, 'olp_1234'.length)}${'*'.repeat(12)}`
+          `${token.slice(0, 'olp_1234'.length)}${'*'.repeat(12)}`
         ).should('have.length.at.least', 1)
       })
-      cy.get('button').contains('Generate token').should('not.exist')
-      cy.get('button').contains('Add another token').should('exist')
+      cy.findByRole('button', {
+        name: 'Git integration Generate token',
+      }).should('not.exist')
+      cy.findByRole('button', { name: 'Add another token' }).should('exist')
       clearAllTokens()
-      cy.get('button').contains('Generate token').should('exist')
-      cy.get('button').contains('Add another token').should('not.exist')
+      cy.findByRole('button', {
+        name: 'Git integration Generate token',
+      }).should('exist')
+      cy.findByRole('button', { name: 'Add another token' }).should('not.exist')
     })
 
     it('should render the git-bridge UI in the editor', function () {
       maybeClearAllTokens()
       createProject('git').as('projectId')
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
-      cy.findByText('Sync')
-      cy.findByText('Git').click()
+      cy.findByTestId('left-menu').within(() => {
+        cy.findByRole('heading', { name: 'Sync' })
+        cy.findByRole('button', { name: 'Git' }).click()
+      })
       cy.findByTestId('git-bridge-modal').within(() => {
         cy.get('@projectId').then(id => {
-          cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
+          cy.findByLabelText('Git clone project command').contains(
+            `git clone ${gitURL(id.toString())}`
+          )
         })
         cy.findByRole('button', {
-          name: /generate token/i,
+          name: 'Generate token',
         }).click()
-        cy.get('code').contains(/olp_[a-zA-Z0-9]{16}/)
+        cy.findByLabelText('Git authentication token').contains(
+          /olp_[a-zA-Z0-9]{16}/
+        )
       })
 
       // Re-open
       cy.url().then(url => cy.visit(url))
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
-      cy.findByText('Git').click()
+      cy.findByTestId('left-menu').within(() => {
+        cy.findByRole('button', { name: 'Git' }).click()
+      })
       cy.findByTestId('git-bridge-modal').within(() => {
         cy.get('@projectId').then(id => {
           cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
         })
-        cy.findByText('Generate token').should('not.exist')
+        cy.findByRole('button', {
+          name: 'Generate token',
+        }).should('not.exist')
         cy.findByText(/generate a new one in Account settings/)
-        cy.findByText('Go to settings')
+        cy.findByRole('link', { name: 'Go to settings' })
           .should('have.attr', 'target', '_blank')
           .and('have.attr', 'href', '/user/settings')
       })
@@ -197,18 +219,23 @@ describe('git-bridge', function () {
       const recompile = throttledRecompile()
 
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
-      cy.findByText('Sync')
-      cy.findByText('Git').click()
+      cy.findByTestId('left-menu').within(() => {
+        cy.findByRole('heading', { name: 'Sync' })
+        cy.findByRole('button', { name: 'Git' }).click()
+      })
       cy.get('@projectId').then(projectId => {
         cy.findByTestId('git-bridge-modal').within(() => {
-          cy.get('code').contains(`git clone ${gitURL(projectId.toString())}`)
+          cy.findByLabelText('Git clone project command').contains(
+            `git clone ${gitURL(projectId.toString())}`
+          )
         })
+        cy.findByRole('heading', { name: 'Clone with Git' })
         cy.findByRole('button', {
-          name: /generate token/i,
+          name: 'Generate token',
         }).click()
         cy.get('code')
           .contains(/olp_[a-zA-Z0-9]{16}/)
@@ -255,8 +282,8 @@ describe('git-bridge', function () {
               },
             }
             const authorOptions = {
-              author: { name: 'user', email: 'user@example.com' },
-              committer: { name: 'user', email: 'user@example.com' },
+              author: { name: 'user', email: USER },
+              committer: { name: 'user', email: USER },
             }
             const mainTex = `${dir}/main.tex`
 
@@ -372,24 +399,26 @@ Hello world
   })
 
   function checkDisabled() {
-    ensureUserExists({ email: 'user@example.com' })
+    ensureUserExists({ email: USER })
 
     it('should not render the git-bridge UI in the settings', () => {
-      login('user@example.com')
+      login(USER)
       cy.visit('/user/settings')
-      cy.findByText('Git integration').should('not.exist')
+      cy.findByRole('heading', { name: 'Git integration' }).should('not.exist')
     })
     it('should not render the git-bridge UI in the editor', function () {
-      login('user@example.com')
+      login(USER)
       createProject('maybe git')
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
-      cy.findByText('Word Count') // wait for lazy loading
-      cy.findByText('Sync').should('not.exist')
-      cy.findByText('Git').should('not.exist')
+      cy.findByTestId('left-menu').within(() => {
+        cy.findByRole('button', { name: 'Word Count' }) // wait for lazy loading
+        cy.findByRole('heading', { name: 'Sync' }).should('not.exist')
+        cy.findByRole('button', { name: 'Git' }).should('not.exist')
+      })
     })
   }
 

+ 1 - 1
server-ce/test/helpers/compile.ts

@@ -32,7 +32,7 @@ export function prepareWaitForNextCompileSlot() {
       queueReset()
       triggerCompile()
       cy.log('Wait for compile to finish')
-      cy.findByText('Recompile').should('be.visible')
+      cy.findByRole('button', { name: 'Recompile' }).should('be.visible')
     })
   }
   function recompile() {

+ 13 - 7
server-ce/test/helpers/project.ts

@@ -2,11 +2,13 @@ import { login } from './login'
 import { openEmail } from './email'
 import { v4 as uuid } from 'uuid'
 
+export const NEW_PROJECT_BUTTON_MATCHER = /new project/i
+
 export function createProject(
   name: string,
   {
     type = 'Blank project',
-    newProjectButtonMatcher = /new project/i,
+    newProjectButtonMatcher = NEW_PROJECT_BUTTON_MATCHER,
     open = true,
   }: {
     type?: 'Blank project' | 'Example project'
@@ -41,7 +43,7 @@ export function createProject(
   cy.findAllByText(type, { exact: false }).first().click()
   cy.findByRole('dialog').within(() => {
     cy.get('input').type(name)
-    cy.findByText('Create').click()
+    cy.findByRole('button', { name: 'Create' }).click()
   })
   if (open) {
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
@@ -104,7 +106,7 @@ function shareProjectByEmail(
   level: 'Viewer' | 'Editor'
 ) {
   openProjectByName(projectName)
-  cy.findByText('Share').click()
+  cy.findByRole('button', { name: 'Share' }).click()
   cy.findByRole('dialog').within(() => {
     cy.findByLabelText('Add email address', { selector: 'input' }).type(
       `${email},`
@@ -115,10 +117,10 @@ function shareProjectByEmail(
         cy.findByTestId('add-collaborator-select')
           .click()
           .then(() => {
-            cy.findByText(level).click()
+            cy.findByRole('option', { name: level }).click()
           })
       })
-    cy.findByText('Invite').click({ force: true })
+    cy.findByRole('button', { name: 'Invite' }).click()
     cy.findByText('Invite not yet accepted.')
   })
 }
@@ -203,7 +205,9 @@ export function waitForMainDocToLoad() {
 
 export function openFile(fileName: string, waitFor: string) {
   // force: The file-tree pane is too narrow to display the full name.
-  cy.findByTestId('file-tree').findByText(fileName).click({ force: true })
+  cy.findByRole('navigation', { name: 'Project files and outline' })
+    .findByRole('treeitem', { name: fileName })
+    .click({ force: true })
 
   // wait until we've switched to the selected file
   cy.findByText('Loading…').should('not.exist')
@@ -221,7 +225,9 @@ export function createNewFile() {
     cy.findByText('Create').click()
   })
   // force: The file-tree pane is too narrow to display the full name.
-  cy.findByTestId('file-tree').findByText(fileName).click({ force: true })
+  cy.findByTestId('file-tree')
+    .findByRole('treeitem', { name: fileName })
+    .click({ force: true })
 
   // wait until we've switched to the newly created empty file
   cy.findByText('Loading…').should('not.exist')

+ 66 - 30
server-ce/test/history.spec.ts

@@ -13,29 +13,49 @@ describe('History', function () {
 
   function addLabel(name: string) {
     cy.log(`add label ${JSON.stringify(name)}`)
-    cy.findByText('Labels').click()
-    cy.findAllByTestId('history-version-details')
-      .first()
-      .within(() => {
-        cy.get('button').click() // TODO: add test-id or aria-label
-        cy.findByText('Label this version').click()
+    // The input is not clickable due to being visually hidden, click its label instead
+    cy.findByRole('complementary', {
+      name: 'Project history and labels',
+    }).within(() => {
+      cy.findByRole('group', {
+        name: 'Show all of the project history or only labelled versions.',
+      }).within(() => {
+        cy.findByText('Labels').click()
       })
+      cy.findByRole('radio', { name: 'Labels' }).should('be.checked')
+      cy.findByRole('radio', { name: 'All history' }).should('not.be.checked')
+      cy.findAllByTestId('history-version-details')
+        .first()
+        .within(() => {
+          cy.findByRole('button', { name: 'More actions' }).click()
+          cy.findByRole('menuitem', { name: 'Label this version' }).click()
+        })
+    })
     cy.findByRole('dialog').within(() => {
-      cy.findByLabelText(/New label name/i)
-        .as('input')
-        .type(`${name}{enter}`)
+      cy.findByLabelText('New label name').type(`${name}{enter}`)
     })
   }
 
   function downloadVersion(name: string) {
     cy.log(`download version ${JSON.stringify(name)}`)
-    cy.findByText('Labels').click()
-    cy.findByText(name)
-      .closest('[data-testid="history-version-details"]')
-      .within(() => {
-        cy.get('.history-version-dropdown-menu-btn').click()
-        cy.findByText('Download this version').click()
+    // The input is not clickable due to being visually hidden, click its label instead
+    cy.findByRole('complementary', {
+      name: 'Project history and labels',
+    }).within(() => {
+      cy.findByRole('group', {
+        name: 'Show all of the project history or only labelled versions.',
+      }).within(() => {
+        cy.findByText('Labels').click()
       })
+      cy.findByRole('radio', { name: 'Labels' }).should('be.checked')
+      cy.findByRole('radio', { name: 'All history' }).should('not.be.checked')
+      cy.findByText(name)
+        .closest('[data-testid="history-version-details"]')
+        .within(() => {
+          cy.findByRole('button', { name: 'More actions' }).click()
+          cy.findByRole('menuitem', { name: 'Download this version' }).click()
+        })
+    })
   }
 
   const CLASS_ADDITION = 'ol-cm-addition-marker'
@@ -46,11 +66,13 @@ describe('History', function () {
     const recompile = throttledRecompile()
 
     cy.log('add content, including a line that will get removed soon')
-    cy.findByText('\\maketitle').parent().click()
-    cy.findByText('\\maketitle').parent().type('\n% added')
-    cy.findByText('\\maketitle').parent().type('\n% to be removed')
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
+      cy.findByText('\\maketitle').parent().click()
+      cy.findByText('\\maketitle').parent().type('\n% added')
+      cy.findByText('\\maketitle').parent().type('\n% to be removed')
+    })
     recompile()
-    cy.findByText('History').click()
+    cy.findByRole('button', { name: 'History' }).click()
 
     cy.log('expect to see additions in history')
     cy.get('.document-diff-container').within(() => {
@@ -61,10 +83,10 @@ describe('History', function () {
     addLabel('Before removal')
 
     cy.log('remove content')
-    cy.findByText('Back to editor').click()
+    cy.findByRole('button', { name: 'Back to editor' }).click()
     cy.findByText('% to be removed').parent().type('{end}{shift}{upArrow}{del}')
     recompile()
-    cy.findByText('History').click()
+    cy.findByRole('button', { name: 'History' }).click()
 
     cy.log('expect to see annotation for newly removed content in history')
     cy.get('.document-diff-container').within(() => {
@@ -75,18 +97,32 @@ describe('History', function () {
     addLabel('After removal')
 
     cy.log('add more content after labeling')
-    cy.findByText('Back to editor').click()
-    cy.findByText('\\maketitle').parent().click()
-    cy.findByText('\\maketitle').parent().type('\n% more')
+    cy.findByRole('button', { name: 'Back to editor' }).click()
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
+      cy.findByText('\\maketitle').parent().click()
+      cy.findByText('\\maketitle').parent().type('\n% more')
+    })
     recompile()
 
     cy.log('compare non current versions')
-    cy.findByText('History').click()
-    cy.findByText('Labels').click()
-    cy.findAllByTestId('compare-icon-version').last().click()
-    cy.findAllByTestId('compare-icon-version').filter(':visible').click()
-    cy.findByText('Compare up to this version').click()
-
+    cy.findByRole('button', { name: 'History' }).click()
+    // The input is not clickable due to being visually hidden, click its label instead
+    cy.findByRole('complementary', {
+      name: 'Project history and labels',
+    }).within(() => {
+      cy.findByRole('group', {
+        name: 'Show all of the project history or only labelled versions.',
+      }).within(() => {
+        cy.findByText('Labels').click()
+      })
+      cy.findByRole('radio', { name: 'Labels' }).should('be.checked')
+      cy.findByRole('radio', { name: 'All history' }).should('not.be.checked')
+      cy.findAllByTestId('compare-icon-version').last().click()
+      cy.findAllByTestId('compare-icon-version').filter(':visible').click()
+      cy.findByRole('menuitem', {
+        name: 'Compare up to this version',
+      }).click()
+    })
     cy.log(
       'expect to see annotation for removed content between the two versions'
     )

+ 20 - 11
server-ce/test/learn-wiki.spec.ts

@@ -27,40 +27,49 @@ describe('LearnWiki', function () {
     it('should add a documentation entry to the nav bar', () => {
       login(REGULAR_USER)
       cy.visit('/project')
-      cy.get('nav').findByText('Documentation')
+      cy.findByRole('menuitem', { name: 'Documentation' }).should(
+        'have.attr',
+        'href',
+        '/learn'
+      )
     })
 
     it('should display a tutorial link in the welcome page', () => {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
-      cy.findByText(LABEL_LEARN_LATEX)
+      cy.findByRole('link', { name: LABEL_LEARN_LATEX })
+        .should('have.attr', 'href', '/learn/latex/Learn_LaTeX_in_30_minutes')
+        .and('have.attr', 'target', '_blank')
+        .within(() => {
+          cy.get('img').should('have.attr', 'src').and('not.be.empty')
+        })
     })
 
     it('should render wiki page', () => {
       login(REGULAR_USER)
       cy.visit(UPLOADING_A_PROJECT_URL)
       // Wiki content
-      cy.get('.page').findByText('Uploading a project')
-      cy.get('.page').contains(/how to create an Overleaf project/)
-      cy.get('img[alt="Creating a new project on Overleaf"]')
+      cy.findByRole('heading', { name: 'Uploading a project' })
+      cy.contains(/how to create an Overleaf project/)
+      cy.findByRole('img', { name: 'Creating a new project on Overleaf' })
         .should('be.visible')
         .and((el: any) => {
           expect(el[0].naturalWidth, 'renders image').to.be.greaterThan(0)
         })
       // Wiki navigation
-      cy.get('.contents').findByText('Copying a project')
+      cy.findByRole('link', { name: 'Copying a project' }).should('exist')
     })
 
     it('should navigate back and forth', function () {
       login(REGULAR_USER)
       cy.visit(COPYING_A_PROJECT_URL)
-      cy.get('.page').findByText('Copying a project')
-      cy.get('.contents').findByText('Uploading a project').click()
+      cy.findByRole('heading', { name: 'Copying a project' })
+      cy.findByRole('link', { name: 'Uploading a project' }).click()
       cy.url().should('contain', UPLOADING_A_PROJECT_URL)
-      cy.get('.page').findByText('Uploading a project')
-      cy.get('.contents').findByText('Copying a project').click()
+      cy.findByRole('heading', { name: 'Uploading a project' })
+      cy.findByRole('link', { name: 'Copying a project' }).click()
       cy.url().should('contain', COPYING_A_PROJECT_URL)
-      cy.get('.page').findByText('Copying a project')
+      cy.findByRole('heading', { name: 'Copying a project' })
     })
   })
 

+ 28 - 18
server-ce/test/project-list.spec.ts

@@ -21,8 +21,10 @@ describe('Project List', () => {
     it("'Import from GitHub' is not displayed in the welcome page", () => {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
-      cy.findByText('Create a new project').click()
-      cy.findByText(/Import from GitHub/i).should('not.exist')
+      cy.findByRole('button', { name: 'Create a new project' }).click()
+      cy.findByRole('menuitem', { name: 'Import from GitHub' }).should(
+        'not.exist'
+      )
     })
   })
 
@@ -66,42 +68,50 @@ describe('Project List', () => {
     it('can assign and remove tags to projects', () => {
       const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors
       cy.log('select project')
-      cy.get(`[aria-label="Select ${projectName}"]`).click()
+      cy.findByRole('checkbox', { name: `Select ${projectName}` }).check()
 
       cy.log('add tag to project')
-      cy.get('button[aria-label="Tags"]').click()
-      cy.findByText('Create new tag').click()
-      cy.get('input[name="new-tag-form-name"]').type(`${tagName}{enter}`)
-      cy.get(`button[aria-label="Select tag ${tagName}"]`) // tag label in project row
+      cy.findByRole('button', { name: 'Tags' }).click()
+      cy.findByRole('menuitem', { name: 'Create new tag' }).click()
+      cy.findByRole('dialog').within(() => {
+        cy.findByRole('heading', { name: 'Create new tag' })
+        cy.findByLabelText('New tag name').type(`${tagName}{enter}`)
+      })
+      cy.findByRole('button', { name: `Select tag ${tagName}` }) // tag label in project row
 
       cy.log('remove tag')
-      cy.get(`button[aria-label="Remove tag ${tagName}"]`)
+      cy.findByRole('button', { name: `Remove tag ${tagName}` })
         .first()
-        .click({ force: true })
-      cy.get(`button[aria-label="Select tag ${tagName}"]`).should('not.exist')
+        .click()
+      cy.findByRole('button', { name: `Select tag ${tagName}` }).should(
+        'not.exist'
+      )
     })
 
     it('can filter by tag', () => {
       cy.log('create a separate project to filter')
       const nonTaggedProjectName = `project-${uuid()}`
-      login(REGULAR_USER)
       createProject(nonTaggedProjectName, { open: false })
 
       cy.log('select project')
-      cy.get(`[aria-label="Select ${projectName}"]`).click()
+      cy.findByRole('checkbox', { name: `Select ${projectName}` }).check()
 
       cy.log('add tag to project')
       const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors
-      cy.get('button[aria-label="Tags"]').click()
-      cy.findByText('Create new tag').click()
-      cy.get('input[name="new-tag-form-name"]').type(`${tagName}{enter}`)
+      cy.findByRole('button', { name: 'Tags' }).click()
+      cy.findByRole('menuitem', { name: 'Create new tag' }).click()
+
+      cy.findByRole('dialog').within(() => {
+        cy.findByRole('heading', { name: 'Create new tag' })
+        cy.findByLabelText('New tag name').type(`${tagName}{enter}`)
+      })
 
       cy.log(
         'check the non-tagged project is filtered out after clicking the tag'
       )
-      cy.findByText(nonTaggedProjectName).should('exist')
-      cy.get('button').contains(tagName).click({ force: true })
-      cy.findByText(nonTaggedProjectName).should('not.exist')
+      cy.findByRole('link', { name: nonTaggedProjectName }).should('exist')
+      cy.findByRole('button', { name: `Select tag ${tagName}` }).click()
+      cy.findByRole('link', { name: nonTaggedProjectName }).should('not.exist')
     })
   })
 })

+ 31 - 22
server-ce/test/project-sharing.spec.ts

@@ -43,7 +43,7 @@ describe('Project Sharing', function () {
     createProject(projectName)
 
     // Add chat message
-    cy.findByText('Chat').click()
+    cy.findByRole('button', { name: 'Chat' }).click()
     // wait for lazy loading of the chat pane
     cy.findByText('Send your first message to your collaborators')
     cy.get(
@@ -61,11 +61,11 @@ describe('Project Sharing', function () {
 
   function expectContentReadOnlyAccess() {
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       '\\maketitle'
     )
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'have.attr',
       'contenteditable',
       'false'
@@ -77,12 +77,12 @@ describe('Project Sharing', function () {
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
     const recompile = throttledRecompile()
     // wait for the editor to finish loading
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       '\\maketitle'
     )
     // the editor should be writable
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'have.attr',
       'contenteditable',
       'true'
@@ -90,14 +90,20 @@ describe('Project Sharing', function () {
     cy.findByText('\\maketitle').parent().click()
     cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
     // should have written
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       `\\section{${section}}`
     )
     // check PDF
     recompile()
-    cy.get('.pdf-viewer').should('contain.text', projectName)
-    cy.get('.pdf-viewer').should('contain.text', section)
+    cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
+      cy.findByLabelText(/Page.*1/i).should('be.visible')
+      cy.findByText(projectName).should('be.visible')
+    })
+    cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
+      cy.findByLabelText(/Page.*1/i).should('be.visible')
+      cy.contains(section)
+    })
   }
 
   function expectNoAccess() {
@@ -117,7 +123,7 @@ describe('Project Sharing', function () {
   }
 
   function expectChatAccess() {
-    cy.findByText('Chat').click()
+    cy.findByRole('button', { name: 'Chat' }).click()
     cy.findByText('New Chat Message')
   }
 
@@ -132,17 +138,17 @@ describe('Project Sharing', function () {
   }
 
   function expectNoChatAccess() {
-    cy.findByText('Layout') // wait for lazy loading
-    cy.findByText('Chat').should('not.exist')
+    cy.findByRole('button', { name: 'Layout' }) // wait for lazy loading
+    cy.findByRole('button', { name: 'Chat' }).should('not.exist')
   }
 
   function expectNoHistoryAccess() {
-    cy.findByText('Layout') // wait for lazy loading
-    cy.findByText('History').should('not.exist')
+    cy.findByRole('button', { name: 'Layout' }) // wait for lazy loading
+    cy.findByRole('button', { name: 'History' }).should('not.exist')
   }
 
   function expectCommentAccess() {
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       '\\maketitle'
     )
@@ -151,11 +157,11 @@ describe('Project Sharing', function () {
 
     cy.findByRole('button', { name: 'Add comment' }).should('be.visible')
 
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).click()
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
   }
 
   function expectNoCommentAccess() {
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       '\\maketitle'
     )
@@ -163,7 +169,7 @@ describe('Project Sharing', function () {
     cy.findByText('\\maketitle').parent().dblclick()
 
     cy.findByRole('button', { name: 'Add comment' }).should('not.exist')
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).click()
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
   }
 
   function expectFullReadOnlyAccess() {
@@ -200,12 +206,15 @@ describe('Project Sharing', function () {
   }
 
   function expectEditAuthoredAs(author: string) {
-    cy.findByText('History').click()
-    cy.findAllByTestId('history-version-metadata-users')
-      .first()
-      .should('contain.text', author) // might have other edits in the same group
+    cy.findByRole('button', { name: 'History' }).click()
+    cy.findByRole('complementary', {
+      name: 'Project history and labels',
+    }).within(() => {
+      cy.findAllByTestId('history-version-metadata-users')
+        .first()
+        .should('contain.text', author) // might have other edits in the same group
+    })
   }
-
   describe('via email', function () {
     const email = 'collaborator-email@example.com'
     ensureUserExists({ email })

+ 83 - 62
server-ce/test/sandboxed-compiles.spec.ts

@@ -30,30 +30,35 @@ describe('SandboxedCompiles', function () {
       createProject('sandboxed')
       const recompile = throttledRecompile()
       cy.log('wait for compile')
-      cy.get('.pdf-viewer').should('contain.text', 'sandboxed')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'contain.text',
+        'sandboxed'
+      )
 
       cy.log('Check which compiler version was used, expect 2023')
-      cy.get('[aria-label="View logs"]').click()
+      cy.findByRole('button', { name: 'View logs' }).click()
       cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2023\) /)
 
       cy.log('Switch TeXLive version from 2023 to 2022')
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
-      cy.findByText(LABEL_TEX_LIVE_VERSION)
-        .parent()
-        .findByText('2023')
-        .parent()
-        .select('2022')
-      cy.get('.left-menu-modal-backdrop').click()
+      cy.findByRole('dialog').within(() => {
+        cy.findByRole('option', { name: '2023' }).should('be.selected')
+        cy.findByRole('combobox', { name: LABEL_TEX_LIVE_VERSION }).select(
+          '2022'
+        )
+      })
+      cy.get('body').type('{esc}')
+      cy.findByRole('dialog').should('not.exist')
 
       cy.log('Trigger compile with other TeX Live version')
       recompile()
 
       cy.log('Check which compiler version was used, expect 2022')
-      cy.get('[aria-label="View logs"]').click()
+      cy.findByRole('button', { name: 'View logs' }).click()
       cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2022\) /)
     })
 
@@ -101,39 +106,43 @@ describe('SandboxedCompiles', function () {
         projectName = `Project ${uuid()}`
         createProject(projectName)
         const recompile = throttledRecompile()
-        cy.findByText('\\maketitle').parent().click()
-        cy.findByText('\\maketitle')
-          .parent()
-          .type(
-            `\n\\pagebreak\n\\section{{}Section A}\n\\pagebreak\n\\section{{}Section B}\n\\pagebreak`
-          )
+        cy.findByRole('textbox', { name: 'Source Editor editing' }).within(
+          () => {
+            cy.findByText('\\maketitle').parent().click()
+            cy.findByText('\\maketitle')
+              .parent()
+              .type(
+                `\n\\pagebreak\n\\section{{}Section A}\n\\pagebreak\n\\section{{}Section B}\n\\pagebreak`
+              )
+          }
+        )
         recompile()
         cy.log('wait for pdf-rendering')
-        cy.get('.pdf-viewer').within(() => {
-          cy.findByText(projectName)
-        })
+        cy.findByRole('region', { name: 'PDF preview and logs' }).findByText(
+          projectName
+        )
       })
 
       it('should sync to code', function () {
         cy.log('navigate to \\maketitle using double click in PDF')
-        cy.get('.pdf-viewer').within(() => {
-          cy.findByText(projectName).dblclick()
-        })
+        cy.findByRole('region', { name: 'PDF preview and logs' })
+          .findByText(projectName)
+          .dblclick()
         cy.get('.cm-activeLine').should('have.text', '\\maketitle')
 
         cy.log('navigate to Section A using double click in PDF')
-        cy.get('.pdf-viewer').within(() => {
-          cy.findByText('Section A').dblclick()
-        })
+        cy.findByRole('region', { name: 'PDF preview and logs' })
+          .findByText('Section A')
+          .dblclick()
         cy.get('.cm-activeLine').should('have.text', '\\section{Section A}')
 
         cy.log('navigate to Section B using arrow button')
-        cy.get('.pdfjs-viewer-inner')
+        cy.findByTestId('pdfjs-viewer-inner')
           .should('have.prop', 'scrollTop')
           .as('start')
-        cy.get('.pdf-viewer').within(() => {
-          cy.findByText('Section B').scrollIntoView()
-        })
+        cy.findByRole('region', { name: 'PDF preview and logs' })
+          .findByText('Section B')
+          .scrollIntoView()
         cy.get('@start').then((start: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', start)
         })
@@ -141,21 +150,27 @@ describe('SandboxedCompiles', function () {
         // Cypress appears to click on a button that references a stale position.
         // Adding a cy.wait() statement is the most reliable "fix" so far :/
         cy.wait(1000)
-        cy.get('[aria-label^="Go to PDF location in code"]').click()
+        cy.findByRole('button', {
+          name: 'Go to PDF location in code (Tip: double click on the PDF for best results)',
+        }).click()
         cy.get('.cm-activeLine').should('have.text', '\\section{Section B}')
       })
 
       it('should sync to pdf', function () {
         cy.log('zoom in')
-        cy.findByText('45%').click()
-        cy.findByText('400%').click()
+        cy.findByRole('button', { name: /^\d+%$/ }).click() // TODO: ARIA label
+        cy.findByRole('menuitem', { name: '400%' }).click()
         cy.log('scroll to top')
-        cy.get('.pdfjs-viewer-inner').scrollTo('top')
+        cy.findByTestId('pdfjs-viewer-inner').scrollTo('top')
         waitUntilScrollingFinished('.pdfjs-viewer-inner', -1).as('start')
 
         cy.log('navigate to title')
-        cy.findByText('\\maketitle').parent().click()
-        cy.get('[aria-label="Go to code location in PDF"]').click()
+        cy.findByRole('textbox', { name: 'Source Editor editing' }).within(
+          () => {
+            cy.findByText('\\maketitle').parent().click()
+          }
+        )
+        cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
         cy.get('@start').then((start: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', start)
             .as('title')
@@ -163,10 +178,10 @@ describe('SandboxedCompiles', function () {
         })
 
         cy.log('navigate to Section A')
-        cy.findByRole('textbox', { name: /Source Editor editing/i }).within(
-          () => cy.findByText('Section A').click()
+        cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() =>
+          cy.findByText('Section A').click()
         )
-        cy.get('[aria-label="Go to code location in PDF"]').click()
+        cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
         cy.get('@title').then((title: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', title)
             .as('sectionA')
@@ -174,10 +189,10 @@ describe('SandboxedCompiles', function () {
         })
 
         cy.log('navigate to Section B')
-        cy.findByRole('textbox', { name: /Source Editor editing/i }).within(
-          () => cy.findByText('Section B').click()
+        cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() =>
+          cy.findByText('Section B').click()
         )
-        cy.get('[aria-label="Go to code location in PDF"]').click()
+        cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
         cy.get('@sectionA').then((title: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', title)
             .as('sectionB')
@@ -192,14 +207,18 @@ describe('SandboxedCompiles', function () {
       login('user@example.com')
       createProject('test-project')
       const recompile = throttledRecompile()
-      cy.findByText('\\maketitle').parent().click()
-      cy.findByText('\\maketitle')
-        .parent()
-        .type('\n\\fakeCommand{} \n\\section{{}Test Section}')
+      cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
+        cy.findByText('\\maketitle').parent().click()
+        cy.findByText('\\maketitle')
+          .parent()
+          .type('\n\\fakeCommand{} \n\\section{{}Test Section}')
+      })
       recompile()
       recompile()
-      cy.get('.pdf-viewer').should('contain.text', 'Test Section')
-      cy.get('.logs-pane').should('not.contain.text', 'No PDF')
+      cy.findByRole('region', { name: 'PDF preview and logs' }).within(() => {
+        cy.findByText('Test Section').should('contain.text', 'Test Section')
+      })
+      cy.findByTestId('logs-pane').should('not.contain.text', 'No PDF')
     })
   }
 
@@ -208,30 +227,32 @@ describe('SandboxedCompiles', function () {
       createProject('XeLaTeX')
       const recompile = throttledRecompile()
       cy.log('wait for compile')
-      cy.get('.pdf-viewer').should('contain.text', 'XeLaTeX')
-
+      cy.findByRole('region', { name: 'PDF preview and logs' }).should(
+        'contain.text',
+        'XeLaTeX'
+      )
       cy.log('Check which compiler was used, expect pdfLaTeX')
-      cy.get('[aria-label="View logs"]').click()
+      cy.findByRole('button', { name: 'View logs' }).click()
       cy.findByText(/This is pdfTeX/)
 
       cy.log('Switch compiler to from pdfLaTeX to XeLaTeX')
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
-      cy.findByText('Compiler')
-        .parent()
-        .findByText('pdfLaTeX')
-        .parent()
-        .select('XeLaTeX')
-      cy.get('.left-menu-modal-backdrop').click()
+      cy.findByRole('dialog').within(() => {
+        cy.findByRole('option', { name: 'pdfLaTeX' }).should('be.selected')
+        cy.findByRole('combobox', { name: 'Compiler' }).select('XeLaTeX')
+      })
+      cy.get('body').type('{esc}')
+      cy.findByRole('dialog').should('not.exist')
 
       cy.log('Trigger compile with other compiler')
       recompile()
 
       cy.log('Check which compiler was used, expect XeLaTeX')
-      cy.get('[aria-label="View logs"]').click()
+      cy.findByRole('button', { name: 'View logs' }).click()
       cy.findByText(/This is XeTeX/)
     })
   }
@@ -252,9 +273,9 @@ describe('SandboxedCompiles', function () {
 
       cy.log('Check that there is no TeX Live version toggle')
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText(LABEL_TEX_LIVE_VERSION).should('not.exist')

+ 19 - 21
server-ce/test/templates.spec.ts

@@ -1,6 +1,6 @@
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
-import { createProject } from './helpers/project'
+import { createProject, NEW_PROJECT_BUTTON_MATCHER } from './helpers/project'
 
 const WITHOUT_PROJECTS_USER = 'user-without-projects@example.com'
 const ADMIN_USER = 'admin@example.com'
@@ -43,7 +43,9 @@ describe('Templates', () => {
     it('should show templates link on welcome page', () => {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/')
-      cy.findByText(LABEL_BROWSE_TEMPLATES).click()
+      cy.findByRole('link', { name: LABEL_BROWSE_TEMPLATES })
+        .should('have.attr', 'href', '/templates')
+        .click()
       cy.url().should('match', /\/templates$/)
     })
 
@@ -56,9 +58,9 @@ describe('Templates', () => {
       createProject(name, { type: 'Example project' }).as('templateProjectId')
 
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Manage Template').click()
 
@@ -139,9 +141,9 @@ describe('Templates', () => {
         cy.visit(`/project/${projectId}`)
       )
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Publish').click()
@@ -168,9 +170,9 @@ describe('Templates', () => {
       cy.url().should('match', /\/project\/[a-f0-9]{24}$/)
       cy.get('.project-name').should('contain.text', 'Your Paper') // might have (1) suffix
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Manage Template').should('not.exist')
@@ -191,9 +193,9 @@ describe('Templates', () => {
         cy.visit(`/project/${projectId}`)
       )
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Unpublish')
@@ -206,9 +208,9 @@ describe('Templates', () => {
         cy.visit(`/project/${projectId}`)
       )
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Unpublish').click()
@@ -218,9 +220,7 @@ describe('Templates', () => {
 
       // check for template links, after creating the first project
       cy.visit('/')
-      cy.findAllByRole('button')
-        .contains(/new project/i)
-        .click()
+      cy.findAllByRole('button', { name: NEW_PROJECT_BUTTON_MATCHER }).click()
       cy.findAllByText('All Templates')
         .first()
         .parent()
@@ -236,9 +236,9 @@ describe('Templates', () => {
       createProject('maybe templates')
 
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Manage Template').should('not.exist')
@@ -250,16 +250,14 @@ describe('Templates', () => {
 
       // check for template links, after creating the first project
       cy.visit('/')
-      cy.findAllByRole('button')
-        .contains(/new project/i)
-        .click()
+      cy.findAllByRole('button', { name: NEW_PROJECT_BUTTON_MATCHER }).click()
       cy.findAllByText('All Templates').should('not.exist')
     })
 
     it('should not show templates link on welcome page', () => {
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/')
-      cy.findByText(/new project/i) // wait for lazy loading
+      cy.findByText(NEW_PROJECT_BUTTON_MATCHER) // wait for lazy loading
       cy.findByText(LABEL_BROWSE_TEMPLATES).should('not.exist')
     })
   }

+ 3 - 3
server-ce/test/upgrading.spec.ts

@@ -56,9 +56,9 @@ describe('Upgrading', function () {
         cy.log('Trigger full flush')
         recompile()
         cy.findByRole('navigation', {
-          name: /Project Layout, Sharing, and Submission/i,
+          name: 'Project Layout, Sharing, and Submission',
         })
-          .findByRole('button', { name: /Menu/i })
+          .findByRole('button', { name: 'Menu' })
           .click()
         cy.findByText('Source').click()
         cy.get('.left-menu-modal-backdrop').click({ force: true })
@@ -121,7 +121,7 @@ describe('Upgrading', function () {
 
       cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       }).within(() => {
         cy.findByText(PROJECT_NAME)
       })

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

@@ -665,7 +665,7 @@
   "git_bridge_modal_review_access": "",
   "git_bridge_modal_see_once": "",
   "git_bridge_modal_use_previous_token": "",
-  "git_clone_project": "",
+  "git_clone_project_command": "",
   "git_clone_this_project": "",
   "git_integration": "",
   "git_integration_info": "",

+ 14 - 3
services/web/frontend/js/features/settings/components/linking/integration-widget.tsx

@@ -121,7 +121,9 @@ function ActionButton({
   titleId,
 }: ActionButtonProps) {
   const { t } = useTranslation()
+  const upgradeTextId = `${titleId}-upgrade`
   const linkTextId = `${titleId}-link`
+  const unlinkTextId = `${titleId}-unlink`
 
   if (!hasFeature) {
     return (
@@ -129,17 +131,19 @@ function ActionButton({
         variant="primary"
         href="/user/subscription/plans"
         onClick={() => trackUpgradeClick(integration)}
-        aria-labelledby={`${titleId} ${linkTextId}`}
+        aria-labelledby={`${titleId} ${upgradeTextId}`}
       >
-        <span id={linkTextId}>{t('upgrade')}</span>
+        <span id={upgradeTextId}>{t('upgrade')}</span>
       </OLButton>
     )
   } else if (linked) {
     return (
       <OLButton
         variant="danger-ghost"
+        aria-labelledby={`${unlinkTextId} ${titleId}`}
         onClick={handleUnlinkClick}
         disabled={disabled}
+        id={unlinkTextId}
       >
         {t('unlink')}
       </OLButton>
@@ -148,7 +152,12 @@ function ActionButton({
     return (
       <>
         {disabled ? (
-          <OLButton disabled variant="secondary">
+          <OLButton
+            disabled
+            variant="secondary"
+            aria-labelledby={`${linkTextId} ${titleId}`}
+            id={linkTextId}
+          >
             {t('link')}
           </OLButton>
         ) : (
@@ -156,6 +165,8 @@ function ActionButton({
             variant="secondary"
             href={linkPath}
             onClick={() => trackLinkingClick(integration)}
+            aria-labelledby={`${linkTextId} ${titleId}`}
+            id={linkTextId}
           >
             {t('link')}
           </OLButton>

+ 1 - 1
services/web/locales/en.json

@@ -868,7 +868,7 @@
   "git_bridge_modal_review_access": "<0>You have review access to this project.</0> This means you can pull from __appName__ but you can’t push any changes you make back to this project.",
   "git_bridge_modal_see_once": "You’ll only see this token once. To delete it or generate a new one, visit Account settings. For detailed instructions and troubleshooting, read our <0>help page</0>.",
   "git_bridge_modal_use_previous_token": "If you’re prompted for a password, you can use a previously generated Git authentication token. Or you can generate a new one in Account settings. For more support, read our <0>help page</0>.",
-  "git_clone_project": "Git clone project",
+  "git_clone_project_command": "Git clone project command",
   "git_clone_this_project": "Git clone this project.",
   "git_gitHub_dropbox_mendeley_papers_and_zotero_integrations": "Git, GitHub, Dropbox, Papers, Zotero, and Mendeley integrations",
   "git_integration": "Git integration",

+ 3 - 3
services/web/test/frontend/features/history/components/change-list.spec.tsx

@@ -148,7 +148,7 @@ describe('change list', function () {
         name: /Project history and labels/i,
       }).within(() => {
         cy.findByRole('group', {
-          name: /Show all of the project history/i,
+          name: 'Show all of the project history or only labelled versions.',
         }).within(() => {
           cy.findByText(/Labels/i).click()
         })
@@ -353,7 +353,7 @@ describe('change list', function () {
         name: /Project history and labels/i,
       }).within(() => {
         cy.findByRole('group', {
-          name: /Show all of the project history/i,
+          name: 'Show all of the project history or only labelled versions.',
         }).within(() => {
           cy.findByText(/Labels/i).click()
         })
@@ -716,7 +716,7 @@ describe('change list', function () {
         name: /Project history and labels/i,
       }).within(() => {
         cy.findByRole('group', {
-          name: /Show all of the project history/i,
+          name: 'Show all of the project history or only labelled versions.',
         }).within(() => {
           cy.findByText(/Labels/i).click()
         })

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

@@ -47,7 +47,7 @@ describe('<AccountInfoSection />', function () {
     })
     fireEvent.click(
       screen.getByRole('button', {
-        name: /update/i,
+        name: 'Update account info',
       })
     )
     expect(updateMock.callHistory.called()).to.be.true
@@ -68,7 +68,7 @@ describe('<AccountInfoSection />', function () {
       target: { value: 'john' },
     })
     const button = screen.getByRole('button', {
-      name: /update/i,
+      name: 'Update account info',
     }) as HTMLButtonElement
 
     expect(button.disabled).to.be.true

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

@@ -63,17 +63,16 @@ function resetFetchMock() {
 }
 
 async function confirmCodeForEmail(email: string) {
-  await screen.findByText(
-    `Enter the 6-digit confirmation code sent to ${email}.`
-  )
-  const inputCode = screen.getByLabelText(/6-digit confirmation code/i)
+  const inputCode = await screen.findByRole('textbox', {
+    name: `Enter the 6-digit confirmation code sent to ${email}.`,
+  })
   fireEvent.change(inputCode, { target: { value: '123456' } })
   const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
     name: 'Confirm',
   })
   fireEvent.click(submitCodeBtn)
   await waitForElementToBeRemoved(() =>
-    screen.getByRole('button', { name: /confirming/i })
+    screen.getByRole('button', { name: 'Confirming' })
   )
 }
 
@@ -96,7 +95,7 @@ describe('<EmailsSection />', function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     render(<EmailsSection />)
 
-    await screen.findByRole('button', { name: /add another email/i })
+    await screen.findByRole('button', { name: 'Add another email' })
   })
 
   it('renders input', async function () {
@@ -105,11 +104,11 @@ describe('<EmailsSection />', function () {
     await fetchMock.callHistory.flush(true)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     fireEvent.click(button)
 
-    await screen.findByLabelText(/email/i, { selector: 'input' })
+    await screen.findByRole('textbox', { name: 'Email' })
   })
 
   it('renders "Start adding your address" until a valid email is typed', async function () {
@@ -120,17 +119,17 @@ describe('<EmailsSection />', function () {
     await fetchMock.callHistory.flush(true)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     fireEvent.click(button)
 
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
 
     // initially the text is displayed and the "add email" button disabled
     screen.getByText('Start by adding your email address.')
     expect(
       screen.getByRole<HTMLButtonElement>('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       }).disabled
     ).to.be.true
 
@@ -141,7 +140,7 @@ describe('<EmailsSection />', function () {
     screen.getByText('Start by adding your email address.')
     expect(
       screen.getByRole<HTMLButtonElement>('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       }).disabled
     ).to.be.true
 
@@ -152,7 +151,7 @@ describe('<EmailsSection />', function () {
     expect(screen.queryByText('Start by adding your email address.')).to.be.null
     expect(
       screen.getByRole<HTMLButtonElement>('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       }).disabled
     ).to.be.false
   })
@@ -162,11 +161,11 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     fireEvent.click(button)
 
-    screen.getByRole('button', { name: /add new email/i })
+    screen.getByRole('button', { name: 'Add new email' })
   })
 
   it('prevent users from adding new emails when the limit is reached', async function () {
@@ -182,7 +181,7 @@ describe('<EmailsSection />', function () {
       'You can have a maximum of 10 email addresses on this account. To add another email address, please delete an existing one.'
     )
 
-    expect(screen.queryByRole('button', { name: /add another email/i })).to.not
+    expect(screen.queryByRole('button', { name: 'Add another email' })).to.not
       .exist
   })
 
@@ -192,7 +191,7 @@ describe('<EmailsSection />', function () {
 
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
       'button',
-      { name: /add another email/i }
+      { name: 'Add another email' }
     )
 
     await fetchMock.callHistory.flush(true)
@@ -203,14 +202,14 @@ describe('<EmailsSection />', function () {
       .post('/user/emails/confirm-secondary', 200)
 
     fireEvent.click(addAnotherEmailBtn)
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
 
     fireEvent.change(input, {
       target: { value: userEmailData.email },
     })
 
     const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
-      name: /add new email/i,
+      name: 'Add new email',
     })
 
     expect(submitBtn.disabled).to.be.false
@@ -221,7 +220,7 @@ describe('<EmailsSection />', function () {
 
     await waitForElementToBeRemoved(() =>
       screen.getByRole('button', {
-        name: /Loading/i,
+        name: 'Loading',
       })
     )
 
@@ -235,7 +234,7 @@ describe('<EmailsSection />', function () {
 
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
       'button',
-      { name: /add another email/i }
+      { name: 'Add another email' }
     )
 
     await fetchMock.callHistory.flush(true)
@@ -245,14 +244,14 @@ describe('<EmailsSection />', function () {
       .post('/user/emails/secondary', 400)
 
     fireEvent.click(addAnotherEmailBtn)
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
 
     fireEvent.change(input, {
       target: { value: userEmailData.email },
     })
 
     const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
-      name: /add new email/i,
+      name: 'Add new email',
     })
 
     expect(submitBtn.disabled).to.be.false
@@ -262,7 +261,7 @@ describe('<EmailsSection />', function () {
     expect(submitBtn.disabled).to.be.true
 
     await screen.findByText(
-      /Invalid Request. Please correct the data and try again./i
+      'Invalid Request. Please correct the data and try again.'
     )
     expect(submitBtn).to.not.be.null
     expect(submitBtn.disabled).to.be.false
@@ -273,7 +272,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
 
     await fetchMock.callHistory.flush(true)
@@ -282,7 +281,7 @@ describe('<EmailsSection />', function () {
 
     await userEvent.click(button)
 
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
     fireEvent.change(input, {
       target: { value: 'user@autocomplete.edu' },
     })
@@ -297,7 +296,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
 
     await fetchMock.callHistory.flush(true)
@@ -306,14 +305,14 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
 
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       userEmailData.email
     )
 
-    await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
+    await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
 
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /university/i,
+      name: 'University',
     })
 
     expect(universityInput.disabled).to.be.true
@@ -330,7 +329,7 @@ describe('<EmailsSection />', function () {
     // Select the country from dropdown
     await userEvent.type(
       screen.getByRole('combobox', {
-        name: /country/i,
+        name: 'Country',
       }),
       country
     )
@@ -347,10 +346,10 @@ describe('<EmailsSection />', function () {
       await screen.findByText(userEmailData.affiliation.institution.name)
     )
 
-    const roleInput = screen.getByRole('combobox', { name: /role/i })
+    const roleInput = screen.getByRole('combobox', { name: 'Role' })
     await userEvent.type(roleInput, userEmailData.affiliation.role!)
     const departmentInput = screen.getByRole('combobox', {
-      name: /department/i,
+      name: 'Department',
     })
     await userEvent.click(departmentInput)
     await userEvent.click(screen.getByText(customDepartment))
@@ -370,7 +369,7 @@ describe('<EmailsSection />', function () {
 
     await userEvent.click(
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       })
     )
 
@@ -402,7 +401,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
 
     await fetchMock.callHistory.flush(true)
@@ -422,14 +421,14 @@ describe('<EmailsSection />', function () {
     // open "add new email" section and click "let us know" to open the Country/University form
     await userEvent.click(button)
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       userEmailData.email
     )
-    await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
+    await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
 
     // select a country
     const countryInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /country/i,
+      name: 'Country',
     })
     await userEvent.click(countryInput)
     await userEvent.type(countryInput, 'Germ')
@@ -437,7 +436,7 @@ describe('<EmailsSection />', function () {
 
     // match several universities on initial typing
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /university/i,
+      name: 'University',
     })
     await userEvent.click(universityInput)
     await userEvent.type(universityInput, 'bo')
@@ -458,7 +457,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
 
     await fetchMock.callHistory.flush(true)
@@ -467,14 +466,14 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
 
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       userEmailData.email
     )
 
-    await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
+    await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
 
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /university/i,
+      name: 'University',
     })
 
     expect(universityInput.disabled).to.be.true
@@ -491,7 +490,7 @@ describe('<EmailsSection />', function () {
     // Select the country from dropdown
     await userEvent.type(
       screen.getByRole('combobox', {
-        name: /country/i,
+        name: 'Country',
       }),
       country
     )
@@ -505,10 +504,10 @@ describe('<EmailsSection />', function () {
     // Enter the university manually
     await userEvent.type(universityInput, newUniversity)
 
-    const roleInput = screen.getByRole('combobox', { name: /role/i })
+    const roleInput = screen.getByRole('combobox', { name: 'Role' })
     await userEvent.type(roleInput, userEmailData.affiliation.role!)
     const departmentInput = screen.getByRole('combobox', {
-      name: /department/i,
+      name: 'Department',
     })
     await userEvent.type(departmentInput, userEmailData.affiliation.department!)
 
@@ -530,7 +529,7 @@ describe('<EmailsSection />', function () {
 
     await userEvent.click(
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       })
     )
 
@@ -573,7 +572,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
 
     await fetchMock.callHistory.flush(true)
@@ -586,7 +585,7 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
 
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       `user@${hostnameFirstChar}`
     )
 
@@ -596,37 +595,37 @@ describe('<EmailsSection />', function () {
 
     expect(
       screen.queryByRole('combobox', {
-        name: /country/i,
+        name: 'Country',
       })
     ).to.be.null
     expect(
       screen.queryByRole('combobox', {
-        name: /university/i,
+        name: 'University',
       })
     ).to.be.null
     screen.getByRole('combobox', {
-      name: /role/i,
+      name: 'Role',
     })
     screen.getByRole('combobox', {
-      name: /department/i,
+      name: 'Department',
     })
 
-    await userEvent.click(screen.getByRole('button', { name: /change/i }))
+    await userEvent.click(screen.getByRole('button', { name: 'Change' }))
 
     screen.getByRole('combobox', {
-      name: /country/i,
+      name: 'Country',
     })
     screen.getByRole('combobox', {
-      name: /university/i,
+      name: 'University',
     })
     expect(
       screen.queryByRole('combobox', {
-        name: /role/i,
+        name: 'Role',
       })
     ).to.be.null
     expect(
       screen.queryByRole('combobox', {
-        name: /department/i,
+        name: 'Department',
       })
     ).to.be.null
   })
@@ -646,7 +645,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
 
     await fetchMock.callHistory.flush(true)
@@ -659,7 +658,7 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
 
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       `user@${hostnameFirstChar}`
     )
 
@@ -686,16 +685,16 @@ describe('<EmailsSection />', function () {
       .post('/user/emails/confirm-secondary', 200)
 
     await userEvent.type(
-      screen.getByRole('combobox', { name: /role/i }),
+      screen.getByRole('combobox', { name: 'Role' }),
       userEmailData.affiliation.role!
     )
     await userEvent.type(
-      screen.getByRole('combobox', { name: /department/i }),
+      screen.getByRole('combobox', { name: 'Department' }),
       userEmailData.affiliation.department!
     )
     await userEvent.click(
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       })
     )
 
@@ -743,12 +742,12 @@ describe('<EmailsSection />', function () {
         render(<EmailsSection />)
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
-          name: /add another email/i,
+          name: 'Add another email',
         })
 
         await userEvent.click(button)
 
-        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        const input = screen.getByRole('textbox', { name: 'Email' })
         fireEvent.change(input, {
           target: { value: 'user@autocomplete.edu' },
         })
@@ -786,12 +785,12 @@ describe('<EmailsSection />', function () {
         render(<EmailsSection />)
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
-          name: /add another email/i,
+          name: 'Add another email',
         })
 
         await userEvent.click(button)
 
-        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        const input = screen.getByRole('textbox', { name: 'Email' })
         fireEvent.change(input, {
           target: { value: 'user@autocomplete.edu' },
         })
@@ -833,12 +832,12 @@ describe('<EmailsSection />', function () {
         render(<EmailsSection />)
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
-          name: /add another email/i,
+          name: 'Add another email',
         })
 
         await userEvent.click(button)
 
-        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        const input = screen.getByRole('textbox', { name: 'Email' })
         fireEvent.change(input, {
           target: { value: 'user@autocomplete.edu' },
         })

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

@@ -61,17 +61,17 @@ describe('<LinkingSection />', function () {
 
     screen.getByText('Google')
     screen.getByText('Log in with Google.')
-    screen.getByRole('button', { name: /unlink/i })
+    screen.getByRole('button', { name: 'Unlink Google' })
 
     screen.getByText('ORCID')
     screen.getByText(
       /Securely establish your identity by linking your ORCID iD/
     )
     const helpLink = screen.getByRole('link', {
-      name: /learn more about orcid/i,
+      name: 'Learn more about ORCID',
     })
     expect(helpLink.getAttribute('href')).to.equal('/blog/434')
-    const linkButton = screen.getByRole('link', { name: /link orcid/i })
+    const linkButton = screen.getByRole('link', { name: 'Link ORCID' })
     expect(linkButton.getAttribute('href')).to.equal('/auth/orcid?intent=link')
   })
 

+ 13 - 5
services/web/test/frontend/features/settings/components/linking/integration-widget.test.tsx

@@ -33,7 +33,9 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
     })
 
     it('should render an upgrade link and track clicks', function () {
-      const upgradeLink = screen.getByRole('link', { name: /upgrade/i })
+      const upgradeLink = screen.getByRole('link', {
+        name: 'Integration Upgrade',
+      })
       expect(upgradeLink.getAttribute('href')).to.equal(
         '/user/subscription/plans'
       )
@@ -52,7 +54,9 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
 
     it('should render a link to initiate integration linking', function () {
       expect(
-        screen.getByRole('link', { name: 'Link' }).getAttribute('href')
+        screen
+          .getByRole('link', { name: 'Link Integration' })
+          .getAttribute('href')
       ).to.equal('/link')
     })
 
@@ -86,11 +90,13 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
     })
 
     it('should display an `unlink` button', function () {
-      screen.getByRole('button', { name: 'Unlink' })
+      screen.getByRole('button', { name: 'Unlink Integration' })
     })
 
     it('should open a modal with a link to confirm integration unlinking', function () {
-      fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
+      fireEvent.click(
+        screen.getByRole('button', { name: 'Unlink Integration' })
+      )
       const withinModal = within(screen.getByRole('dialog'))
       withinModal.getByText('confirm unlink')
       withinModal.getByText('you will be unlinked')
@@ -99,7 +105,9 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
     })
 
     it('should cancel unlinking when clicking "cancel" in the confirmation modal', async function () {
-      fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
+      fireEvent.click(
+        screen.getByRole('button', { name: 'Unlink Integration' })
+      )
       screen.getByText('confirm unlink')
       const cancelBtn = screen.getByRole('button', {
         name: 'Cancel',

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

@@ -13,8 +13,8 @@ import { SSOLinkingWidget } from '../../../../../../frontend/js/features/setting
 describe('<SSOLinkingWidget />', function () {
   const defaultProps = {
     providerId: 'integration_id',
-    title: 'integration',
-    description: 'integration description',
+    title: 'Integration',
+    description: 'Integration description',
     helpPath: '/help/integration',
     linkPath: '/integration/link',
     onUnlink: () => Promise.resolve(),
@@ -22,10 +22,12 @@ describe('<SSOLinkingWidget />', function () {
 
   it('should render', function () {
     render(<SSOLinkingWidget {...defaultProps} />)
-    screen.getByText('integration')
-    screen.getByText('integration description')
+    screen.getByText('Integration')
+    screen.getByText('Integration description')
     expect(
-      screen.getByRole('link', { name: /learn more/i }).getAttribute('href')
+      screen
+        .getByRole('link', { name: 'Learn more about Integration' })
+        .getAttribute('href')
     ).to.equal('/help/integration')
   })
 
@@ -33,7 +35,9 @@ describe('<SSOLinkingWidget />', function () {
     it('should render a link to `linkPath`', function () {
       render(<SSOLinkingWidget {...defaultProps} linked={false} />)
       expect(
-        screen.getByRole('link', { name: /link/i }).getAttribute('href')
+        screen
+          .getByRole('link', { name: 'Link Integration' })
+          .getAttribute('href')
       ).to.equal('/integration/link?intent=link')
     })
   })
@@ -49,19 +53,23 @@ describe('<SSOLinkingWidget />', function () {
     })
 
     it('should display an `unlink` button', function () {
-      screen.getByRole('button', { name: /unlink/i })
+      screen.getByRole('button', { name: 'Unlink Integration' })
     })
 
     it('should open a modal to confirm integration unlinking', function () {
-      fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
-      screen.getByText('Unlink integration Account')
+      fireEvent.click(
+        screen.getByRole('button', { name: 'Unlink Integration' })
+      )
+      screen.getByText('Unlink Integration Account')
       screen.getByText(
-        'Warning: When you unlink your account from integration you will not be able to sign in using integration anymore.'
+        'Warning: When you unlink your account from Integration you will not be able to sign in using Integration anymore.'
       )
     })
 
     it('should cancel unlinking when clicking cancel in the confirmation modal', async function () {
-      fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
+      fireEvent.click(
+        screen.getByRole('button', { name: 'Unlink Integration' })
+      )
       const cancelBtn = screen.getByRole('button', {
         name: 'Cancel',
         hidden: false,
@@ -80,9 +88,11 @@ describe('<SSOLinkingWidget />', function () {
       render(
         <SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
       )
-      fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
+      fireEvent.click(
+        screen.getByRole('button', { name: 'Unlink Integration' })
+      )
       confirmBtn = within(screen.getByRole('dialog')).getByRole('button', {
-        name: /unlink/i,
+        name: 'Unlink',
         hidden: false,
       })
     })
@@ -114,11 +124,13 @@ describe('<SSOLinkingWidget />', function () {
       render(
         <SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
       )
-      fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
+      fireEvent.click(
+        screen.getByRole('button', { name: 'Unlink Integration' })
+      )
       const confirmBtn = within(screen.getByRole('dialog')).getByRole(
         'button',
         {
-          name: /unlink/i,
+          name: 'Unlink',
           hidden: false,
         }
       )
@@ -130,7 +142,7 @@ describe('<SSOLinkingWidget />', function () {
     })
 
     it('should display the unlink button ', async function () {
-      await screen.findByRole('button', { name: /unlink/i })
+      await screen.findByRole('button', { name: 'Unlink Integration' })
     })
   })
 })