Просмотр исходного кода

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 месяцев назад
Родитель
Сommit
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')
     login('user@example.com')
     cy.visit('/project')
     cy.visit('/project')
     cy.findByRole('menuitem', { name: 'Account' }).click()
     cy.findByRole('menuitem', { name: 'Account' }).click()
-    cy.findByText('Log Out').click()
+    cy.findByRole('menuitem', { name: 'Log Out' }).click()
     cy.url().should('include', '/login')
     cy.url().should('include', '/login')
     cy.visit('/project')
     cy.visit('/project')
     cy.url().should('include', '/login')
     cy.url().should('include', '/login')

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

@@ -14,7 +14,7 @@ describe('admin panel', function () {
   function registrationTests() {
   function registrationTests() {
     it('via GUI and opening URL manually', () => {
     it('via GUI and opening URL manually', () => {
       const user = `${uuid()}@example.com`
       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')
       cy.get('td')
         .contains(/\/user\/activate/)
         .contains(/\/user\/activate/)
@@ -26,7 +26,7 @@ describe('admin panel', function () {
 
 
     it('via GUI and email', () => {
     it('via GUI and email', () => {
       const user = `${uuid()}@example.com`
       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
       let url: string
       cy.get('td')
       cy.get('td')
@@ -93,8 +93,8 @@ describe('admin panel', function () {
       beforeEach(() => {
       beforeEach(() => {
         login(admin)
         login(admin)
         cy.visit('/project')
         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()
       registrationTests()
     })
     })
@@ -139,17 +139,17 @@ describe('admin panel', function () {
       beforeEach(() => {
       beforeEach(() => {
         login(admin)
         login(admin)
         cy.visit('/project')
         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', () => {
       it('publish and clear admin messages', () => {
         const message = 'Admin Message ' + uuid()
         const message = 'Admin Message ' + uuid()
 
 
         cy.log('create system message')
         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)
         cy.findByText(message)
 
 
         login(user1)
         login(user1)
@@ -159,10 +159,10 @@ describe('admin panel', function () {
         cy.log('clear system messages')
         cy.log('clear system messages')
         login(admin)
         login(admin)
         cy.visit('/project')
         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')
         cy.log('verify system messages are no longer displayed')
         login(user1)
         login(user1)
@@ -175,16 +175,16 @@ describe('admin panel', function () {
       beforeEach(() => {
       beforeEach(() => {
         login(admin)
         login(admin)
         cy.visit('/project')
         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', () => {
       it('displays expected tabs', () => {
         const tabs = ['Users', 'License Usage']
         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', () => {
       it('license usage tab', () => {
@@ -202,10 +202,12 @@ describe('admin panel', function () {
       })
       })
 
 
       it('user list RegExp search', () => {
       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(() => {
       beforeEach(() => {
         login(admin)
         login(admin)
         cy.visit('/project')
         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}/)
         cy.url().should('match', /\/admin\/user\/[a-fA-F0-9]{24}/)
       })
       })
 
 
@@ -228,15 +232,15 @@ describe('admin panel', function () {
           'Audit Log',
           'Audit Log',
           'Sessions',
           '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', () => {
       describe('user info tab', () => {
         beforeEach(() => {
         beforeEach(() => {
-          cy.get('[role="tab"]').contains('User Info').click()
+          cy.findByRole('tab', { name: 'User Info' }).click()
         })
         })
 
 
         it('displays required sections', () => {
         it('displays required sections', () => {
@@ -246,33 +250,61 @@ describe('admin panel', function () {
         })
         })
 
 
         it('should not display SaaS-only sections', () => {
         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', () => {
       it('transfer project ownership', () => {
         cy.log("access project admin through owners' project list")
         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.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', () => {
       it('displays expected tabs', () => {
         const tabs = ['Project Info', 'Deleted Docs', 'Audit Log']
         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')
       cy.log('select project to delete')
       findProjectRow(deletedProjectName).within(() =>
       findProjectRow(deletedProjectName).within(() =>
-        cy.get('input[type="checkbox"]').first().check()
+        cy
+          .findByRole('checkbox', { name: `Select ${deletedProjectName}` })
+          .first()
+          .check()
       )
       )
-
       cy.log('delete project')
       cy.log('delete project')
       findProjectRow(deletedProjectName).within(() =>
       findProjectRow(deletedProjectName).within(() =>
         cy.findByRole('button', { name: 'Trash' }).click()
         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.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(() =>
       findProjectRow(deletedProjectName).within(() =>
         cy.findByRole('button', { name: 'Delete' }).click()
         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')
       cy.log('login as an admin and navigate to the deleted project')
       login(admin)
       login(admin)
       cy.visit('/admin/user')
       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.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.url().should('contain', `/admin/project/${projectToDeleteId}`)
 
 
       cy.log('login as the user and verify the project is restored')
       cy.log('login as the user and verify the project is restored')
       login(user1)
       login(user1)
       cy.visit('/project')
       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 { isExcludedBySharding, startWith } from './helpers/config'
 import { throttledRecompile } from './helpers/compile'
 import { throttledRecompile } from './helpers/compile'
 
 
+const USER = 'user@example.com'
+const COLLABORATOR = 'collaborator@example.com'
+
 describe('Project creation and compilation', function () {
 describe('Project creation and compilation', function () {
   if (isExcludedBySharding('CE_DEFAULT')) return
   if (isExcludedBySharding('CE_DEFAULT')) return
   startWith({})
   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 () {
   it('users can create project and compile it', function () {
-    login('user@example.com')
+    login(USER)
     createProject('test-project')
     createProject('test-project')
     const recompile = throttledRecompile()
     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()
     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 () {
   it('create and edit markdown file', function () {
     const fileName = `test-${Date.now()}.md`
     const fileName = `test-${Date.now()}.md`
     const markdownContent = '# Markdown title'
     const markdownContent = '# Markdown title'
-    login('user@example.com')
+    login(USER)
     createProject('test-project')
     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.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
     // 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',
       'contain.text',
       '\\maketitle'
       '\\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',
       'contain.text',
       markdownContent
       markdownContent
     )
     )
@@ -54,7 +67,7 @@ describe('Project creation and compilation', function () {
   it('can link and display linked image from other project', function () {
   it('can link and display linked image from other project', function () {
     const sourceProjectName = `test-project-${Date.now()}`
     const sourceProjectName = `test-project-${Date.now()}`
     const targetProjectName = `${sourceProjectName}-target`
     const targetProjectName = `${sourceProjectName}-target`
-    login('user@example.com')
+    login(USER)
 
 
     createProject(sourceProjectName, {
     createProject(sourceProjectName, {
       type: 'Example project',
       type: 'Example project',
@@ -63,15 +76,17 @@ describe('Project creation and compilation', function () {
     createProject(targetProjectName)
     createProject(targetProjectName)
 
 
     // link the image from `projectName` into this project
     // 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.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 Project').select(sourceProjectName)
       cy.findByLabelText('Select a File').select('frog.jpg')
       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')
       .should('have.attr', 'href')
       .then(href => {
       .then(href => {
         cy.get('@sourceProjectId').then(sourceProjectId => {
         cy.get('@sourceProjectId').then(sourceProjectId => {
@@ -83,7 +98,7 @@ describe('Project creation and compilation', function () {
   it('can refresh linked files as collaborator', function () {
   it('can refresh linked files as collaborator', function () {
     const sourceProjectName = `test-project-${Date.now()}`
     const sourceProjectName = `test-project-${Date.now()}`
     const targetProjectName = `${sourceProjectName}-target`
     const targetProjectName = `${sourceProjectName}-target`
-    login('user@example.com')
+    login(USER)
     createProject(sourceProjectName, {
     createProject(sourceProjectName, {
       type: 'Example project',
       type: 'Example project',
       open: false,
       open: false,
@@ -91,31 +106,36 @@ describe('Project creation and compilation', function () {
     createProject(targetProjectName).as('targetProjectId')
     createProject(targetProjectName).as('targetProjectId')
 
 
     // link the image from `projectName` into this project
     // 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.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 Project').select(sourceProjectName)
       cy.findByLabelText('Select a File').select('frog.jpg')
       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.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.')
       cy.findByText('Invite not yet accepted.')
     })
     })
 
 
-    login('collaborator@example.com')
+    login(COLLABORATOR)
     openProjectViaInviteNotification(targetProjectName)
     openProjectViaInviteNotification(targetProjectName)
     cy.get('@targetProjectId').then(targetProjectId => {
     cy.get('@targetProjectId').then(targetProjectId => {
       cy.url().should('include', 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')
       .should('have.attr', 'href')
       .then(href => {
       .then(href => {
         cy.get('@sourceProjectId').then(sourceProjectId => {
         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 { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
 import { prepareWaitForNextCompileSlot } from './helpers/compile'
 import { prepareWaitForNextCompileSlot } from './helpers/compile'
 
 
+const USER = 'user@example.com'
+const COLLABORATOR = 'collaborator@example.com'
+
 describe('editor', () => {
 describe('editor', () => {
   if (isExcludedBySharding('PRO_DEFAULT_1')) return
   if (isExcludedBySharding('PRO_DEFAULT_1')) return
   startWith({ pro: true })
   startWith({ pro: true })
-  ensureUserExists({ email: 'user@example.com' })
-  ensureUserExists({ email: 'collaborator@example.com' })
+  ensureUserExists({ email: USER })
+  ensureUserExists({ email: COLLABORATOR })
 
 
   let projectName: string
   let projectName: string
   let projectId: string
   let projectId: string
@@ -22,7 +25,7 @@ describe('editor', () => {
   let waitForCompileRateLimitCoolOff: (fn: () => void) => void
   let waitForCompileRateLimitCoolOff: (fn: () => void) => void
   beforeWithReRunOnTestRetry(function () {
   beforeWithReRunOnTestRetry(function () {
     projectName = `project-${uuid()}`
     projectName = `project-${uuid()}`
-    login('user@example.com')
+    login(USER)
     createProject(projectName, { type: 'Example project', open: false }).then(
     createProject(projectName, { type: 'Example project', open: false }).then(
       id => (projectId = id)
       id => (projectId = id)
     )
     )
@@ -31,7 +34,7 @@ describe('editor', () => {
   })
   })
 
 
   beforeEach(() => {
   beforeEach(() => {
-    login('user@example.com')
+    login(USER)
     waitForCompileRateLimitCoolOff(() => {
     waitForCompileRateLimitCoolOff(() => {
       openProjectById(projectId)
       openProjectById(projectId)
     })
     })
@@ -40,9 +43,16 @@ describe('editor', () => {
   describe('spelling', function () {
   describe('spelling', function () {
     function changeSpellCheckLanguageTo(lng: string) {
     function changeSpellCheckLanguageTo(lng: string) {
       cy.log(`change project language to '${lng}'`)
       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 () {
     afterEach(function () {
@@ -70,20 +80,28 @@ describe('editor', () => {
       cy.findByText(word).should('not.have.class', 'ol-cm-spelling-error')
       cy.findByText(word).should('not.have.class', 'ol-cm-spelling-error')
 
 
       cy.log('remove word from dictionary')
       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)
         cy.findByText(word)
           .parent()
           .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
         // the modal has 2 close buttons, this ensures the one with the visible label is
         // clicked, otherwise it would need `force: true`
         // 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.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.log('rewrite word to force spelling error')
       cy.get('.cm-line').type('{selectAll}{del}' + word + '{enter}')
       cy.get('.cm-line').type('{selectAll}{del}' + word + '{enter}')
@@ -94,7 +112,11 @@ describe('editor', () => {
 
 
   describe('editor', () => {
   describe('editor', () => {
     it('renders jpg', () => {
     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"]')
       cy.get('[alt="frog.jpg"]')
         .should('be.visible')
         .should('be.visible')
         .and('have.prop', 'naturalWidth')
         .and('have.prop', 'naturalWidth')
@@ -108,7 +130,7 @@ describe('editor', () => {
         force: true,
         force: true,
       })
       })
       cy.get('button').contains('𝜉').click()
       cy.get('button').contains('𝜉').click()
-      cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+      cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
         'contain.text',
         'contain.text',
         '\\xi'
         '\\xi'
       )
       )
@@ -120,23 +142,27 @@ describe('editor', () => {
 
 
   describe('add new file to project', () => {
   describe('add new file to project', () => {
     beforeEach(() => {
     beforeEach(() => {
-      cy.get('button').contains('New file').click({ force: true })
+      cy.findByRole('button', { name: 'New file' }).click()
     })
     })
 
 
     testNewFileUpload()
     testNewFileUpload()
 
 
     it('should not display import from URL', () => {
     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', () => {
   describe('left menu', () => {
     beforeEach(() => {
     beforeEach(() => {
-      cy.get('button').contains('Menu').click()
+      cy.findByRole('navigation', {
+        name: 'Project actions',
+      })
+        .findByRole('button', { name: 'Menu' })
+        .click()
     })
     })
 
 
     it('can download project sources', () => {
     it('can download project sources', () => {
-      cy.get('a').contains('Source').click()
+      cy.findByRole('link', { name: 'Source' }).click()
       const zipName = projectName.replaceAll('-', '_')
       const zipName = projectName.replaceAll('-', '_')
       cy.task('readFileInZip', {
       cy.task('readFileInZip', {
         pathToZip: `cypress/downloads/${zipName}.zip`,
         pathToZip: `cypress/downloads/${zipName}.zip`,
@@ -146,10 +172,12 @@ describe('editor', () => {
 
 
     it('can download project PDF', () => {
     it('can download project PDF', () => {
       cy.log('ensure project is compiled')
       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('-', '_')
         const pdfName = projectName.replaceAll('-', '_')
         cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should(
         cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should(
           'contain',
           'contain',
@@ -160,11 +188,13 @@ describe('editor', () => {
 
 
     it('word count', () => {
     it('word count', () => {
       cy.log('ensure project is compiled')
       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('Total Words:')
         cy.findByText('607')
         cy.findByText('607')
         cy.findByText('Headers:')
         cy.findByText('Headers:')
@@ -179,51 +209,73 @@ describe('editor', () => {
 
 
   describe('layout selector', () => {
   describe('layout selector', () => {
     it('show editor only and switch between editor and pdf', () => {
     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.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.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.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')
       cy.get('.cm-editor').should('be.visible')
     })
     })
 
 
     it('show PDF only and go back to Editor & PDF', () => {
     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.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.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')
       cy.get('.cm-editor').should('be.visible')
     })
     })
 
 
     it('PDF in a separate tab (tests editor only)', () => {
     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.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')
       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/,
         name: /Create First Project|New Project/,
       }).click()
       }).click()
       cy.findByRole('link', { name: 'Example 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.findByRole('button', { name: 'Create' }).click()
       cy.url()
       cy.url()
         .should('match', /\/project\/[a-fA-F0-9]{24}/)
         .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 LightningFS from '@isomorphic-git/lightning-fs'
 import { throttledRecompile } from './helpers/compile'
 import { throttledRecompile } from './helpers/compile'
 
 
+const USER = 'user@example.com'
+
 describe('git-bridge', function () {
 describe('git-bridge', function () {
   const ENABLED_VARS = {
   const ENABLED_VARS = {
     GIT_BRIDGE_ENABLED: 'true',
     GIT_BRIDGE_ENABLED: 'true',
@@ -35,18 +37,21 @@ describe('git-bridge', function () {
       pro: true,
       pro: true,
       vars: ENABLED_VARS,
       vars: ENABLED_VARS,
     })
     })
-    ensureUserExists({ email: 'user@example.com' })
+    ensureUserExists({ email: USER })
 
 
     function clearAllTokens() {
     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() {
     function maybeClearAllTokens() {
       cy.visit('/user/settings')
       cy.visit('/user/settings')
-      cy.findByText('Git integration')
+      cy.findByRole('heading', { name: 'Git integration' })
       cy.get('button')
       cy.get('button')
         .contains(/Generate token|Add another token/)
         .contains(/Generate token|Add another token/)
         .then(btn => {
         .then(btn => {
@@ -57,66 +62,83 @@ describe('git-bridge', function () {
     }
     }
 
 
     beforeEach(function () {
     beforeEach(function () {
-      login('user@example.com')
+      login(USER)
     })
     })
 
 
     it('should render the git-bridge UI in the settings', () => {
     it('should render the git-bridge UI in the settings', () => {
       maybeClearAllTokens()
       maybeClearAllTokens()
       cy.visit('/user/settings')
       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}/)
         .contains(/olp_[a-zA-Z0-9]{16}/)
+        .then(el => el.text())
         .as('newToken')
         .as('newToken')
       cy.findAllByText('Close').last().click()
       cy.findAllByText('Close').last().click()
       cy.get('@newToken').then(token => {
       cy.get('@newToken').then(token => {
         // There can be more than one token with the same prefix when retrying
         // There can be more than one token with the same prefix when retrying
         cy.findAllByText(
         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)
         ).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()
       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 () {
     it('should render the git-bridge UI in the editor', function () {
       maybeClearAllTokens()
       maybeClearAllTokens()
       createProject('git').as('projectId')
       createProject('git').as('projectId')
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .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.findByTestId('git-bridge-modal').within(() => {
         cy.get('@projectId').then(id => {
         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', {
         cy.findByRole('button', {
-          name: /generate token/i,
+          name: 'Generate token',
         }).click()
         }).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
       // Re-open
       cy.url().then(url => cy.visit(url))
       cy.url().then(url => cy.visit(url))
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
-      cy.findByText('Git').click()
+      cy.findByTestId('left-menu').within(() => {
+        cy.findByRole('button', { name: 'Git' }).click()
+      })
       cy.findByTestId('git-bridge-modal').within(() => {
       cy.findByTestId('git-bridge-modal').within(() => {
         cy.get('@projectId').then(id => {
         cy.get('@projectId').then(id => {
           cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
           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(/generate a new one in Account settings/)
-        cy.findByText('Go to settings')
+        cy.findByRole('link', { name: 'Go to settings' })
           .should('have.attr', 'target', '_blank')
           .should('have.attr', 'target', '_blank')
           .and('have.attr', 'href', '/user/settings')
           .and('have.attr', 'href', '/user/settings')
       })
       })
@@ -197,18 +219,23 @@ describe('git-bridge', function () {
       const recompile = throttledRecompile()
       const recompile = throttledRecompile()
 
 
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .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.get('@projectId').then(projectId => {
         cy.findByTestId('git-bridge-modal').within(() => {
         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', {
         cy.findByRole('button', {
-          name: /generate token/i,
+          name: 'Generate token',
         }).click()
         }).click()
         cy.get('code')
         cy.get('code')
           .contains(/olp_[a-zA-Z0-9]{16}/)
           .contains(/olp_[a-zA-Z0-9]{16}/)
@@ -255,8 +282,8 @@ describe('git-bridge', function () {
               },
               },
             }
             }
             const authorOptions = {
             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`
             const mainTex = `${dir}/main.tex`
 
 
@@ -372,24 +399,26 @@ Hello world
   })
   })
 
 
   function checkDisabled() {
   function checkDisabled() {
-    ensureUserExists({ email: 'user@example.com' })
+    ensureUserExists({ email: USER })
 
 
     it('should not render the git-bridge UI in the settings', () => {
     it('should not render the git-bridge UI in the settings', () => {
-      login('user@example.com')
+      login(USER)
       cy.visit('/user/settings')
       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 () {
     it('should not render the git-bridge UI in the editor', function () {
-      login('user@example.com')
+      login(USER)
       createProject('maybe git')
       createProject('maybe git')
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .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()
       queueReset()
       triggerCompile()
       triggerCompile()
       cy.log('Wait for compile to finish')
       cy.log('Wait for compile to finish')
-      cy.findByText('Recompile').should('be.visible')
+      cy.findByRole('button', { name: 'Recompile' }).should('be.visible')
     })
     })
   }
   }
   function recompile() {
   function recompile() {

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

@@ -2,11 +2,13 @@ import { login } from './login'
 import { openEmail } from './email'
 import { openEmail } from './email'
 import { v4 as uuid } from 'uuid'
 import { v4 as uuid } from 'uuid'
 
 
+export const NEW_PROJECT_BUTTON_MATCHER = /new project/i
+
 export function createProject(
 export function createProject(
   name: string,
   name: string,
   {
   {
     type = 'Blank project',
     type = 'Blank project',
-    newProjectButtonMatcher = /new project/i,
+    newProjectButtonMatcher = NEW_PROJECT_BUTTON_MATCHER,
     open = true,
     open = true,
   }: {
   }: {
     type?: 'Blank project' | 'Example project'
     type?: 'Blank project' | 'Example project'
@@ -41,7 +43,7 @@ export function createProject(
   cy.findAllByText(type, { exact: false }).first().click()
   cy.findAllByText(type, { exact: false }).first().click()
   cy.findByRole('dialog').within(() => {
   cy.findByRole('dialog').within(() => {
     cy.get('input').type(name)
     cy.get('input').type(name)
-    cy.findByText('Create').click()
+    cy.findByRole('button', { name: 'Create' }).click()
   })
   })
   if (open) {
   if (open) {
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
@@ -104,7 +106,7 @@ function shareProjectByEmail(
   level: 'Viewer' | 'Editor'
   level: 'Viewer' | 'Editor'
 ) {
 ) {
   openProjectByName(projectName)
   openProjectByName(projectName)
-  cy.findByText('Share').click()
+  cy.findByRole('button', { name: 'Share' }).click()
   cy.findByRole('dialog').within(() => {
   cy.findByRole('dialog').within(() => {
     cy.findByLabelText('Add email address', { selector: 'input' }).type(
     cy.findByLabelText('Add email address', { selector: 'input' }).type(
       `${email},`
       `${email},`
@@ -115,10 +117,10 @@ function shareProjectByEmail(
         cy.findByTestId('add-collaborator-select')
         cy.findByTestId('add-collaborator-select')
           .click()
           .click()
           .then(() => {
           .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.')
     cy.findByText('Invite not yet accepted.')
   })
   })
 }
 }
@@ -203,7 +205,9 @@ export function waitForMainDocToLoad() {
 
 
 export function openFile(fileName: string, waitFor: string) {
 export function openFile(fileName: string, waitFor: string) {
   // force: The file-tree pane is too narrow to display the full name.
   // 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
   // wait until we've switched to the selected file
   cy.findByText('Loading…').should('not.exist')
   cy.findByText('Loading…').should('not.exist')
@@ -221,7 +225,9 @@ export function createNewFile() {
     cy.findByText('Create').click()
     cy.findByText('Create').click()
   })
   })
   // force: The file-tree pane is too narrow to display the full name.
   // 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
   // wait until we've switched to the newly created empty file
   cy.findByText('Loading…').should('not.exist')
   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) {
   function addLabel(name: string) {
     cy.log(`add label ${JSON.stringify(name)}`)
     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.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) {
   function downloadVersion(name: string) {
     cy.log(`download version ${JSON.stringify(name)}`)
     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'
   const CLASS_ADDITION = 'ol-cm-addition-marker'
@@ -46,11 +66,13 @@ describe('History', function () {
     const recompile = throttledRecompile()
     const recompile = throttledRecompile()
 
 
     cy.log('add content, including a line that will get removed soon')
     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()
     recompile()
-    cy.findByText('History').click()
+    cy.findByRole('button', { name: 'History' }).click()
 
 
     cy.log('expect to see additions in history')
     cy.log('expect to see additions in history')
     cy.get('.document-diff-container').within(() => {
     cy.get('.document-diff-container').within(() => {
@@ -61,10 +83,10 @@ describe('History', function () {
     addLabel('Before removal')
     addLabel('Before removal')
 
 
     cy.log('remove content')
     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}')
     cy.findByText('% to be removed').parent().type('{end}{shift}{upArrow}{del}')
     recompile()
     recompile()
-    cy.findByText('History').click()
+    cy.findByRole('button', { name: 'History' }).click()
 
 
     cy.log('expect to see annotation for newly removed content in history')
     cy.log('expect to see annotation for newly removed content in history')
     cy.get('.document-diff-container').within(() => {
     cy.get('.document-diff-container').within(() => {
@@ -75,18 +97,32 @@ describe('History', function () {
     addLabel('After removal')
     addLabel('After removal')
 
 
     cy.log('add more content after labeling')
     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()
     recompile()
 
 
     cy.log('compare non current versions')
     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(
     cy.log(
       'expect to see annotation for removed content between the two versions'
       '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', () => {
     it('should add a documentation entry to the nav bar', () => {
       login(REGULAR_USER)
       login(REGULAR_USER)
       cy.visit('/project')
       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', () => {
     it('should display a tutorial link in the welcome page', () => {
       login(WITHOUT_PROJECTS_USER)
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
       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', () => {
     it('should render wiki page', () => {
       login(REGULAR_USER)
       login(REGULAR_USER)
       cy.visit(UPLOADING_A_PROJECT_URL)
       cy.visit(UPLOADING_A_PROJECT_URL)
       // Wiki content
       // 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')
         .should('be.visible')
         .and((el: any) => {
         .and((el: any) => {
           expect(el[0].naturalWidth, 'renders image').to.be.greaterThan(0)
           expect(el[0].naturalWidth, 'renders image').to.be.greaterThan(0)
         })
         })
       // Wiki navigation
       // 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 () {
     it('should navigate back and forth', function () {
       login(REGULAR_USER)
       login(REGULAR_USER)
       cy.visit(COPYING_A_PROJECT_URL)
       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.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.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", () => {
     it("'Import from GitHub' is not displayed in the welcome page", () => {
       login(WITHOUT_PROJECTS_USER)
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/project')
       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', () => {
     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
       const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors
       cy.log('select project')
       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.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.log('remove tag')
-      cy.get(`button[aria-label="Remove tag ${tagName}"]`)
+      cy.findByRole('button', { name: `Remove tag ${tagName}` })
         .first()
         .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', () => {
     it('can filter by tag', () => {
       cy.log('create a separate project to filter')
       cy.log('create a separate project to filter')
       const nonTaggedProjectName = `project-${uuid()}`
       const nonTaggedProjectName = `project-${uuid()}`
-      login(REGULAR_USER)
       createProject(nonTaggedProjectName, { open: false })
       createProject(nonTaggedProjectName, { open: false })
 
 
       cy.log('select project')
       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.log('add tag to project')
       const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors
       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(
       cy.log(
         'check the non-tagged project is filtered out after clicking the tag'
         '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)
     createProject(projectName)
 
 
     // Add chat message
     // Add chat message
-    cy.findByText('Chat').click()
+    cy.findByRole('button', { name: 'Chat' }).click()
     // wait for lazy loading of the chat pane
     // wait for lazy loading of the chat pane
     cy.findByText('Send your first message to your collaborators')
     cy.findByText('Send your first message to your collaborators')
     cy.get(
     cy.get(
@@ -61,11 +61,11 @@ describe('Project Sharing', function () {
 
 
   function expectContentReadOnlyAccess() {
   function expectContentReadOnlyAccess() {
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
     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',
       'contain.text',
       '\\maketitle'
       '\\maketitle'
     )
     )
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'have.attr',
       'have.attr',
       'contenteditable',
       'contenteditable',
       'false'
       'false'
@@ -77,12 +77,12 @@ describe('Project Sharing', function () {
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
     cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
     const recompile = throttledRecompile()
     const recompile = throttledRecompile()
     // wait for the editor to finish loading
     // 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',
       'contain.text',
       '\\maketitle'
       '\\maketitle'
     )
     )
     // the editor should be writable
     // the editor should be writable
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'have.attr',
       'have.attr',
       'contenteditable',
       'contenteditable',
       'true'
       'true'
@@ -90,14 +90,20 @@ describe('Project Sharing', function () {
     cy.findByText('\\maketitle').parent().click()
     cy.findByText('\\maketitle').parent().click()
     cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
     cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
     // should have written
     // should have written
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       'contain.text',
       `\\section{${section}}`
       `\\section{${section}}`
     )
     )
     // check PDF
     // check PDF
     recompile()
     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() {
   function expectNoAccess() {
@@ -117,7 +123,7 @@ describe('Project Sharing', function () {
   }
   }
 
 
   function expectChatAccess() {
   function expectChatAccess() {
-    cy.findByText('Chat').click()
+    cy.findByRole('button', { name: 'Chat' }).click()
     cy.findByText('New Chat Message')
     cy.findByText('New Chat Message')
   }
   }
 
 
@@ -132,17 +138,17 @@ describe('Project Sharing', function () {
   }
   }
 
 
   function expectNoChatAccess() {
   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() {
   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() {
   function expectCommentAccess() {
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       'contain.text',
       '\\maketitle'
       '\\maketitle'
     )
     )
@@ -151,11 +157,11 @@ describe('Project Sharing', function () {
 
 
     cy.findByRole('button', { name: 'Add comment' }).should('be.visible')
     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() {
   function expectNoCommentAccess() {
-    cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
+    cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
       'contain.text',
       'contain.text',
       '\\maketitle'
       '\\maketitle'
     )
     )
@@ -163,7 +169,7 @@ describe('Project Sharing', function () {
     cy.findByText('\\maketitle').parent().dblclick()
     cy.findByText('\\maketitle').parent().dblclick()
 
 
     cy.findByRole('button', { name: 'Add comment' }).should('not.exist')
     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() {
   function expectFullReadOnlyAccess() {
@@ -200,12 +206,15 @@ describe('Project Sharing', function () {
   }
   }
 
 
   function expectEditAuthoredAs(author: string) {
   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 () {
   describe('via email', function () {
     const email = 'collaborator-email@example.com'
     const email = 'collaborator-email@example.com'
     ensureUserExists({ email })
     ensureUserExists({ email })

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

@@ -30,30 +30,35 @@ describe('SandboxedCompiles', function () {
       createProject('sandboxed')
       createProject('sandboxed')
       const recompile = throttledRecompile()
       const recompile = throttledRecompile()
       cy.log('wait for compile')
       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.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.findByText(/This is pdfTeX, Version .+ \(TeX Live 2023\) /)
 
 
       cy.log('Switch TeXLive version from 2023 to 2022')
       cy.log('Switch TeXLive version from 2023 to 2022')
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .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')
       cy.log('Trigger compile with other TeX Live version')
       recompile()
       recompile()
 
 
       cy.log('Check which compiler version was used, expect 2022')
       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\) /)
       cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2022\) /)
     })
     })
 
 
@@ -101,39 +106,43 @@ describe('SandboxedCompiles', function () {
         projectName = `Project ${uuid()}`
         projectName = `Project ${uuid()}`
         createProject(projectName)
         createProject(projectName)
         const recompile = throttledRecompile()
         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()
         recompile()
         cy.log('wait for pdf-rendering')
         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 () {
       it('should sync to code', function () {
         cy.log('navigate to \\maketitle using double click in PDF')
         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.get('.cm-activeLine').should('have.text', '\\maketitle')
 
 
         cy.log('navigate to Section A using double click in PDF')
         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.get('.cm-activeLine').should('have.text', '\\section{Section A}')
 
 
         cy.log('navigate to Section B using arrow button')
         cy.log('navigate to Section B using arrow button')
-        cy.get('.pdfjs-viewer-inner')
+        cy.findByTestId('pdfjs-viewer-inner')
           .should('have.prop', 'scrollTop')
           .should('have.prop', 'scrollTop')
           .as('start')
           .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) => {
         cy.get('@start').then((start: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', start)
           waitUntilScrollingFinished('.pdfjs-viewer-inner', start)
         })
         })
@@ -141,21 +150,27 @@ describe('SandboxedCompiles', function () {
         // Cypress appears to click on a button that references a stale position.
         // Cypress appears to click on a button that references a stale position.
         // Adding a cy.wait() statement is the most reliable "fix" so far :/
         // Adding a cy.wait() statement is the most reliable "fix" so far :/
         cy.wait(1000)
         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}')
         cy.get('.cm-activeLine').should('have.text', '\\section{Section B}')
       })
       })
 
 
       it('should sync to pdf', function () {
       it('should sync to pdf', function () {
         cy.log('zoom in')
         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.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')
         waitUntilScrollingFinished('.pdfjs-viewer-inner', -1).as('start')
 
 
         cy.log('navigate to title')
         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) => {
         cy.get('@start').then((start: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', start)
           waitUntilScrollingFinished('.pdfjs-viewer-inner', start)
             .as('title')
             .as('title')
@@ -163,10 +178,10 @@ describe('SandboxedCompiles', function () {
         })
         })
 
 
         cy.log('navigate to Section A')
         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) => {
         cy.get('@title').then((title: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', title)
           waitUntilScrollingFinished('.pdfjs-viewer-inner', title)
             .as('sectionA')
             .as('sectionA')
@@ -174,10 +189,10 @@ describe('SandboxedCompiles', function () {
         })
         })
 
 
         cy.log('navigate to Section B')
         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) => {
         cy.get('@sectionA').then((title: any) => {
           waitUntilScrollingFinished('.pdfjs-viewer-inner', title)
           waitUntilScrollingFinished('.pdfjs-viewer-inner', title)
             .as('sectionB')
             .as('sectionB')
@@ -192,14 +207,18 @@ describe('SandboxedCompiles', function () {
       login('user@example.com')
       login('user@example.com')
       createProject('test-project')
       createProject('test-project')
       const recompile = throttledRecompile()
       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()
       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')
       createProject('XeLaTeX')
       const recompile = throttledRecompile()
       const recompile = throttledRecompile()
       cy.log('wait for compile')
       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.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.findByText(/This is pdfTeX/)
 
 
       cy.log('Switch compiler to from pdfLaTeX to XeLaTeX')
       cy.log('Switch compiler to from pdfLaTeX to XeLaTeX')
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .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')
       cy.log('Trigger compile with other compiler')
       recompile()
       recompile()
 
 
       cy.log('Check which compiler was used, expect XeLaTeX')
       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/)
       cy.findByText(/This is XeTeX/)
     })
     })
   }
   }
@@ -252,9 +273,9 @@ describe('SandboxedCompiles', function () {
 
 
       cy.log('Check that there is no TeX Live version toggle')
       cy.log('Check that there is no TeX Live version toggle')
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText(LABEL_TEX_LIVE_VERSION).should('not.exist')
       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 { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 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 WITHOUT_PROJECTS_USER = 'user-without-projects@example.com'
 const ADMIN_USER = 'admin@example.com'
 const ADMIN_USER = 'admin@example.com'
@@ -43,7 +43,9 @@ describe('Templates', () => {
     it('should show templates link on welcome page', () => {
     it('should show templates link on welcome page', () => {
       login(WITHOUT_PROJECTS_USER)
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/')
       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$/)
       cy.url().should('match', /\/templates$/)
     })
     })
 
 
@@ -56,9 +58,9 @@ describe('Templates', () => {
       createProject(name, { type: 'Example project' }).as('templateProjectId')
       createProject(name, { type: 'Example project' }).as('templateProjectId')
 
 
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Manage Template').click()
 
 
@@ -139,9 +141,9 @@ describe('Templates', () => {
         cy.visit(`/project/${projectId}`)
         cy.visit(`/project/${projectId}`)
       )
       )
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Manage Template').click()
       cy.findByText('Publish').click()
       cy.findByText('Publish').click()
@@ -168,9 +170,9 @@ describe('Templates', () => {
       cy.url().should('match', /\/project\/[a-f0-9]{24}$/)
       cy.url().should('match', /\/project\/[a-f0-9]{24}$/)
       cy.get('.project-name').should('contain.text', 'Your Paper') // might have (1) suffix
       cy.get('.project-name').should('contain.text', 'Your Paper') // might have (1) suffix
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Manage Template').should('not.exist')
       cy.findByText('Manage Template').should('not.exist')
@@ -191,9 +193,9 @@ describe('Templates', () => {
         cy.visit(`/project/${projectId}`)
         cy.visit(`/project/${projectId}`)
       )
       )
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Manage Template').click()
       cy.findByText('Unpublish')
       cy.findByText('Unpublish')
@@ -206,9 +208,9 @@ describe('Templates', () => {
         cy.visit(`/project/${projectId}`)
         cy.visit(`/project/${projectId}`)
       )
       )
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Manage Template').click()
       cy.findByText('Manage Template').click()
       cy.findByText('Unpublish').click()
       cy.findByText('Unpublish').click()
@@ -218,9 +220,7 @@ describe('Templates', () => {
 
 
       // check for template links, after creating the first project
       // check for template links, after creating the first project
       cy.visit('/')
       cy.visit('/')
-      cy.findAllByRole('button')
-        .contains(/new project/i)
-        .click()
+      cy.findAllByRole('button', { name: NEW_PROJECT_BUTTON_MATCHER }).click()
       cy.findAllByText('All Templates')
       cy.findAllByText('All Templates')
         .first()
         .first()
         .parent()
         .parent()
@@ -236,9 +236,9 @@ describe('Templates', () => {
       createProject('maybe templates')
       createProject('maybe templates')
 
 
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       })
       })
-        .findByRole('button', { name: /Menu/i })
+        .findByRole('button', { name: 'Menu' })
         .click()
         .click()
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Word Count') // wait for lazy loading
       cy.findByText('Manage Template').should('not.exist')
       cy.findByText('Manage Template').should('not.exist')
@@ -250,16 +250,14 @@ describe('Templates', () => {
 
 
       // check for template links, after creating the first project
       // check for template links, after creating the first project
       cy.visit('/')
       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')
       cy.findAllByText('All Templates').should('not.exist')
     })
     })
 
 
     it('should not show templates link on welcome page', () => {
     it('should not show templates link on welcome page', () => {
       login(WITHOUT_PROJECTS_USER)
       login(WITHOUT_PROJECTS_USER)
       cy.visit('/')
       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')
       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')
         cy.log('Trigger full flush')
         recompile()
         recompile()
         cy.findByRole('navigation', {
         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()
           .click()
         cy.findByText('Source').click()
         cy.findByText('Source').click()
         cy.get('.left-menu-modal-backdrop').click({ force: true })
         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.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
       cy.findByRole('navigation', {
       cy.findByRole('navigation', {
-        name: /Project actions/i,
+        name: 'Project actions',
       }).within(() => {
       }).within(() => {
         cy.findByText(PROJECT_NAME)
         cy.findByText(PROJECT_NAME)
       })
       })

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

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

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

@@ -121,7 +121,9 @@ function ActionButton({
   titleId,
   titleId,
 }: ActionButtonProps) {
 }: ActionButtonProps) {
   const { t } = useTranslation()
   const { t } = useTranslation()
+  const upgradeTextId = `${titleId}-upgrade`
   const linkTextId = `${titleId}-link`
   const linkTextId = `${titleId}-link`
+  const unlinkTextId = `${titleId}-unlink`
 
 
   if (!hasFeature) {
   if (!hasFeature) {
     return (
     return (
@@ -129,17 +131,19 @@ function ActionButton({
         variant="primary"
         variant="primary"
         href="/user/subscription/plans"
         href="/user/subscription/plans"
         onClick={() => trackUpgradeClick(integration)}
         onClick={() => trackUpgradeClick(integration)}
-        aria-labelledby={`${titleId} ${linkTextId}`}
+        aria-labelledby={`${titleId} ${upgradeTextId}`}
       >
       >
-        <span id={linkTextId}>{t('upgrade')}</span>
+        <span id={upgradeTextId}>{t('upgrade')}</span>
       </OLButton>
       </OLButton>
     )
     )
   } else if (linked) {
   } else if (linked) {
     return (
     return (
       <OLButton
       <OLButton
         variant="danger-ghost"
         variant="danger-ghost"
+        aria-labelledby={`${unlinkTextId} ${titleId}`}
         onClick={handleUnlinkClick}
         onClick={handleUnlinkClick}
         disabled={disabled}
         disabled={disabled}
+        id={unlinkTextId}
       >
       >
         {t('unlink')}
         {t('unlink')}
       </OLButton>
       </OLButton>
@@ -148,7 +152,12 @@ function ActionButton({
     return (
     return (
       <>
       <>
         {disabled ? (
         {disabled ? (
-          <OLButton disabled variant="secondary">
+          <OLButton
+            disabled
+            variant="secondary"
+            aria-labelledby={`${linkTextId} ${titleId}`}
+            id={linkTextId}
+          >
             {t('link')}
             {t('link')}
           </OLButton>
           </OLButton>
         ) : (
         ) : (
@@ -156,6 +165,8 @@ function ActionButton({
             variant="secondary"
             variant="secondary"
             href={linkPath}
             href={linkPath}
             onClick={() => trackLinkingClick(integration)}
             onClick={() => trackLinkingClick(integration)}
+            aria-labelledby={`${linkTextId} ${titleId}`}
+            id={linkTextId}
           >
           >
             {t('link')}
             {t('link')}
           </OLButton>
           </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_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_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_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_clone_this_project": "Git clone this project.",
   "git_gitHub_dropbox_mendeley_papers_and_zotero_integrations": "Git, GitHub, Dropbox, Papers, Zotero, and Mendeley integrations",
   "git_gitHub_dropbox_mendeley_papers_and_zotero_integrations": "Git, GitHub, Dropbox, Papers, Zotero, and Mendeley integrations",
   "git_integration": "Git integration",
   "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,
         name: /Project history and labels/i,
       }).within(() => {
       }).within(() => {
         cy.findByRole('group', {
         cy.findByRole('group', {
-          name: /Show all of the project history/i,
+          name: 'Show all of the project history or only labelled versions.',
         }).within(() => {
         }).within(() => {
           cy.findByText(/Labels/i).click()
           cy.findByText(/Labels/i).click()
         })
         })
@@ -353,7 +353,7 @@ describe('change list', function () {
         name: /Project history and labels/i,
         name: /Project history and labels/i,
       }).within(() => {
       }).within(() => {
         cy.findByRole('group', {
         cy.findByRole('group', {
-          name: /Show all of the project history/i,
+          name: 'Show all of the project history or only labelled versions.',
         }).within(() => {
         }).within(() => {
           cy.findByText(/Labels/i).click()
           cy.findByText(/Labels/i).click()
         })
         })
@@ -716,7 +716,7 @@ describe('change list', function () {
         name: /Project history and labels/i,
         name: /Project history and labels/i,
       }).within(() => {
       }).within(() => {
         cy.findByRole('group', {
         cy.findByRole('group', {
-          name: /Show all of the project history/i,
+          name: 'Show all of the project history or only labelled versions.',
         }).within(() => {
         }).within(() => {
           cy.findByText(/Labels/i).click()
           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(
     fireEvent.click(
       screen.getByRole('button', {
       screen.getByRole('button', {
-        name: /update/i,
+        name: 'Update account info',
       })
       })
     )
     )
     expect(updateMock.callHistory.called()).to.be.true
     expect(updateMock.callHistory.called()).to.be.true
@@ -68,7 +68,7 @@ describe('<AccountInfoSection />', function () {
       target: { value: 'john' },
       target: { value: 'john' },
     })
     })
     const button = screen.getByRole('button', {
     const button = screen.getByRole('button', {
-      name: /update/i,
+      name: 'Update account info',
     }) as HTMLButtonElement
     }) as HTMLButtonElement
 
 
     expect(button.disabled).to.be.true
     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) {
 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' } })
   fireEvent.change(inputCode, { target: { value: '123456' } })
   const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
   const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
     name: 'Confirm',
     name: 'Confirm',
   })
   })
   fireEvent.click(submitCodeBtn)
   fireEvent.click(submitCodeBtn)
   await waitForElementToBeRemoved(() =>
   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', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
-    await screen.findByRole('button', { name: /add another email/i })
+    await screen.findByRole('button', { name: 'Add another email' })
   })
   })
 
 
   it('renders input', async function () {
   it('renders input', async function () {
@@ -105,11 +104,11 @@ describe('<EmailsSection />', function () {
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
     fireEvent.click(button)
     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 () {
   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)
     await fetchMock.callHistory.flush(true)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
     fireEvent.click(button)
     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
     // initially the text is displayed and the "add email" button disabled
     screen.getByText('Start by adding your email address.')
     screen.getByText('Start by adding your email address.')
     expect(
     expect(
       screen.getByRole<HTMLButtonElement>('button', {
       screen.getByRole<HTMLButtonElement>('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       }).disabled
       }).disabled
     ).to.be.true
     ).to.be.true
 
 
@@ -141,7 +140,7 @@ describe('<EmailsSection />', function () {
     screen.getByText('Start by adding your email address.')
     screen.getByText('Start by adding your email address.')
     expect(
     expect(
       screen.getByRole<HTMLButtonElement>('button', {
       screen.getByRole<HTMLButtonElement>('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       }).disabled
       }).disabled
     ).to.be.true
     ).to.be.true
 
 
@@ -152,7 +151,7 @@ describe('<EmailsSection />', function () {
     expect(screen.queryByText('Start by adding your email address.')).to.be.null
     expect(screen.queryByText('Start by adding your email address.')).to.be.null
     expect(
     expect(
       screen.getByRole<HTMLButtonElement>('button', {
       screen.getByRole<HTMLButtonElement>('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       }).disabled
       }).disabled
     ).to.be.false
     ).to.be.false
   })
   })
@@ -162,11 +161,11 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
     fireEvent.click(button)
     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 () {
   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.'
       '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
       .exist
   })
   })
 
 
@@ -192,7 +191,7 @@ describe('<EmailsSection />', function () {
 
 
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
       'button',
       'button',
-      { name: /add another email/i }
+      { name: 'Add another email' }
     )
     )
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -203,14 +202,14 @@ describe('<EmailsSection />', function () {
       .post('/user/emails/confirm-secondary', 200)
       .post('/user/emails/confirm-secondary', 200)
 
 
     fireEvent.click(addAnotherEmailBtn)
     fireEvent.click(addAnotherEmailBtn)
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
 
 
     fireEvent.change(input, {
     fireEvent.change(input, {
       target: { value: userEmailData.email },
       target: { value: userEmailData.email },
     })
     })
 
 
     const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
     const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
-      name: /add new email/i,
+      name: 'Add new email',
     })
     })
 
 
     expect(submitBtn.disabled).to.be.false
     expect(submitBtn.disabled).to.be.false
@@ -221,7 +220,7 @@ describe('<EmailsSection />', function () {
 
 
     await waitForElementToBeRemoved(() =>
     await waitForElementToBeRemoved(() =>
       screen.getByRole('button', {
       screen.getByRole('button', {
-        name: /Loading/i,
+        name: 'Loading',
       })
       })
     )
     )
 
 
@@ -235,7 +234,7 @@ describe('<EmailsSection />', function () {
 
 
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
       'button',
       'button',
-      { name: /add another email/i }
+      { name: 'Add another email' }
     )
     )
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -245,14 +244,14 @@ describe('<EmailsSection />', function () {
       .post('/user/emails/secondary', 400)
       .post('/user/emails/secondary', 400)
 
 
     fireEvent.click(addAnotherEmailBtn)
     fireEvent.click(addAnotherEmailBtn)
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
 
 
     fireEvent.change(input, {
     fireEvent.change(input, {
       target: { value: userEmailData.email },
       target: { value: userEmailData.email },
     })
     })
 
 
     const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
     const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
-      name: /add new email/i,
+      name: 'Add new email',
     })
     })
 
 
     expect(submitBtn.disabled).to.be.false
     expect(submitBtn.disabled).to.be.false
@@ -262,7 +261,7 @@ describe('<EmailsSection />', function () {
     expect(submitBtn.disabled).to.be.true
     expect(submitBtn.disabled).to.be.true
 
 
     await screen.findByText(
     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).to.not.be.null
     expect(submitBtn.disabled).to.be.false
     expect(submitBtn.disabled).to.be.false
@@ -273,7 +272,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -282,7 +281,7 @@ describe('<EmailsSection />', function () {
 
 
     await userEvent.click(button)
     await userEvent.click(button)
 
 
-    const input = screen.getByLabelText(/email/i, { selector: 'input' })
+    const input = screen.getByRole('textbox', { name: 'Email' })
     fireEvent.change(input, {
     fireEvent.change(input, {
       target: { value: 'user@autocomplete.edu' },
       target: { value: 'user@autocomplete.edu' },
     })
     })
@@ -297,7 +296,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -306,14 +305,14 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
     await userEvent.click(button)
 
 
     await userEvent.type(
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       userEmailData.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', {
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /university/i,
+      name: 'University',
     })
     })
 
 
     expect(universityInput.disabled).to.be.true
     expect(universityInput.disabled).to.be.true
@@ -330,7 +329,7 @@ describe('<EmailsSection />', function () {
     // Select the country from dropdown
     // Select the country from dropdown
     await userEvent.type(
     await userEvent.type(
       screen.getByRole('combobox', {
       screen.getByRole('combobox', {
-        name: /country/i,
+        name: 'Country',
       }),
       }),
       country
       country
     )
     )
@@ -347,10 +346,10 @@ describe('<EmailsSection />', function () {
       await screen.findByText(userEmailData.affiliation.institution.name)
       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!)
     await userEvent.type(roleInput, userEmailData.affiliation.role!)
     const departmentInput = screen.getByRole('combobox', {
     const departmentInput = screen.getByRole('combobox', {
-      name: /department/i,
+      name: 'Department',
     })
     })
     await userEvent.click(departmentInput)
     await userEvent.click(departmentInput)
     await userEvent.click(screen.getByText(customDepartment))
     await userEvent.click(screen.getByText(customDepartment))
@@ -370,7 +369,7 @@ describe('<EmailsSection />', function () {
 
 
     await userEvent.click(
     await userEvent.click(
       screen.getByRole('button', {
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       })
       })
     )
     )
 
 
@@ -402,7 +401,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
 
 
     await fetchMock.callHistory.flush(true)
     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
     // open "add new email" section and click "let us know" to open the Country/University form
     await userEvent.click(button)
     await userEvent.click(button)
     await userEvent.type(
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       userEmailData.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
     // select a country
     const countryInput = screen.getByRole<HTMLInputElement>('combobox', {
     const countryInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /country/i,
+      name: 'Country',
     })
     })
     await userEvent.click(countryInput)
     await userEvent.click(countryInput)
     await userEvent.type(countryInput, 'Germ')
     await userEvent.type(countryInput, 'Germ')
@@ -437,7 +436,7 @@ describe('<EmailsSection />', function () {
 
 
     // match several universities on initial typing
     // match several universities on initial typing
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /university/i,
+      name: 'University',
     })
     })
     await userEvent.click(universityInput)
     await userEvent.click(universityInput)
     await userEvent.type(universityInput, 'bo')
     await userEvent.type(universityInput, 'bo')
@@ -458,7 +457,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -467,14 +466,14 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
     await userEvent.click(button)
 
 
     await userEvent.type(
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       userEmailData.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', {
     const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
-      name: /university/i,
+      name: 'University',
     })
     })
 
 
     expect(universityInput.disabled).to.be.true
     expect(universityInput.disabled).to.be.true
@@ -491,7 +490,7 @@ describe('<EmailsSection />', function () {
     // Select the country from dropdown
     // Select the country from dropdown
     await userEvent.type(
     await userEvent.type(
       screen.getByRole('combobox', {
       screen.getByRole('combobox', {
-        name: /country/i,
+        name: 'Country',
       }),
       }),
       country
       country
     )
     )
@@ -505,10 +504,10 @@ describe('<EmailsSection />', function () {
     // Enter the university manually
     // Enter the university manually
     await userEvent.type(universityInput, newUniversity)
     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!)
     await userEvent.type(roleInput, userEmailData.affiliation.role!)
     const departmentInput = screen.getByRole('combobox', {
     const departmentInput = screen.getByRole('combobox', {
-      name: /department/i,
+      name: 'Department',
     })
     })
     await userEvent.type(departmentInput, userEmailData.affiliation.department!)
     await userEvent.type(departmentInput, userEmailData.affiliation.department!)
 
 
@@ -530,7 +529,7 @@ describe('<EmailsSection />', function () {
 
 
     await userEvent.click(
     await userEvent.click(
       screen.getByRole('button', {
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       })
       })
     )
     )
 
 
@@ -573,7 +572,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -586,7 +585,7 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
     await userEvent.click(button)
 
 
     await userEvent.type(
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       `user@${hostnameFirstChar}`
       `user@${hostnameFirstChar}`
     )
     )
 
 
@@ -596,37 +595,37 @@ describe('<EmailsSection />', function () {
 
 
     expect(
     expect(
       screen.queryByRole('combobox', {
       screen.queryByRole('combobox', {
-        name: /country/i,
+        name: 'Country',
       })
       })
     ).to.be.null
     ).to.be.null
     expect(
     expect(
       screen.queryByRole('combobox', {
       screen.queryByRole('combobox', {
-        name: /university/i,
+        name: 'University',
       })
       })
     ).to.be.null
     ).to.be.null
     screen.getByRole('combobox', {
     screen.getByRole('combobox', {
-      name: /role/i,
+      name: 'Role',
     })
     })
     screen.getByRole('combobox', {
     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', {
     screen.getByRole('combobox', {
-      name: /country/i,
+      name: 'Country',
     })
     })
     screen.getByRole('combobox', {
     screen.getByRole('combobox', {
-      name: /university/i,
+      name: 'University',
     })
     })
     expect(
     expect(
       screen.queryByRole('combobox', {
       screen.queryByRole('combobox', {
-        name: /role/i,
+        name: 'Role',
       })
       })
     ).to.be.null
     ).to.be.null
     expect(
     expect(
       screen.queryByRole('combobox', {
       screen.queryByRole('combobox', {
-        name: /department/i,
+        name: 'Department',
       })
       })
     ).to.be.null
     ).to.be.null
   })
   })
@@ -646,7 +645,7 @@ describe('<EmailsSection />', function () {
     render(<EmailsSection />)
     render(<EmailsSection />)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
-      name: /add another email/i,
+      name: 'Add another email',
     })
     })
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
@@ -659,7 +658,7 @@ describe('<EmailsSection />', function () {
     await userEvent.click(button)
     await userEvent.click(button)
 
 
     await userEvent.type(
     await userEvent.type(
-      screen.getByLabelText(/email/i, { selector: 'input' }),
+      screen.getByRole('textbox', { name: 'Email' }),
       `user@${hostnameFirstChar}`
       `user@${hostnameFirstChar}`
     )
     )
 
 
@@ -686,16 +685,16 @@ describe('<EmailsSection />', function () {
       .post('/user/emails/confirm-secondary', 200)
       .post('/user/emails/confirm-secondary', 200)
 
 
     await userEvent.type(
     await userEvent.type(
-      screen.getByRole('combobox', { name: /role/i }),
+      screen.getByRole('combobox', { name: 'Role' }),
       userEmailData.affiliation.role!
       userEmailData.affiliation.role!
     )
     )
     await userEvent.type(
     await userEvent.type(
-      screen.getByRole('combobox', { name: /department/i }),
+      screen.getByRole('combobox', { name: 'Department' }),
       userEmailData.affiliation.department!
       userEmailData.affiliation.department!
     )
     )
     await userEvent.click(
     await userEvent.click(
       screen.getByRole('button', {
       screen.getByRole('button', {
-        name: /add new email/i,
+        name: 'Add new email',
       })
       })
     )
     )
 
 
@@ -743,12 +742,12 @@ describe('<EmailsSection />', function () {
         render(<EmailsSection />)
         render(<EmailsSection />)
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
-          name: /add another email/i,
+          name: 'Add another email',
         })
         })
 
 
         await userEvent.click(button)
         await userEvent.click(button)
 
 
-        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        const input = screen.getByRole('textbox', { name: 'Email' })
         fireEvent.change(input, {
         fireEvent.change(input, {
           target: { value: 'user@autocomplete.edu' },
           target: { value: 'user@autocomplete.edu' },
         })
         })
@@ -786,12 +785,12 @@ describe('<EmailsSection />', function () {
         render(<EmailsSection />)
         render(<EmailsSection />)
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
-          name: /add another email/i,
+          name: 'Add another email',
         })
         })
 
 
         await userEvent.click(button)
         await userEvent.click(button)
 
 
-        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        const input = screen.getByRole('textbox', { name: 'Email' })
         fireEvent.change(input, {
         fireEvent.change(input, {
           target: { value: 'user@autocomplete.edu' },
           target: { value: 'user@autocomplete.edu' },
         })
         })
@@ -833,12 +832,12 @@ describe('<EmailsSection />', function () {
         render(<EmailsSection />)
         render(<EmailsSection />)
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
-          name: /add another email/i,
+          name: 'Add another email',
         })
         })
 
 
         await userEvent.click(button)
         await userEvent.click(button)
 
 
-        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        const input = screen.getByRole('textbox', { name: 'Email' })
         fireEvent.change(input, {
         fireEvent.change(input, {
           target: { value: 'user@autocomplete.edu' },
           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('Google')
     screen.getByText('Log in with Google.')
     screen.getByText('Log in with Google.')
-    screen.getByRole('button', { name: /unlink/i })
+    screen.getByRole('button', { name: 'Unlink Google' })
 
 
     screen.getByText('ORCID')
     screen.getByText('ORCID')
     screen.getByText(
     screen.getByText(
       /Securely establish your identity by linking your ORCID iD/
       /Securely establish your identity by linking your ORCID iD/
     )
     )
     const helpLink = screen.getByRole('link', {
     const helpLink = screen.getByRole('link', {
-      name: /learn more about orcid/i,
+      name: 'Learn more about ORCID',
     })
     })
     expect(helpLink.getAttribute('href')).to.equal('/blog/434')
     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')
     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 () {
     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(
       expect(upgradeLink.getAttribute('href')).to.equal(
         '/user/subscription/plans'
         '/user/subscription/plans'
       )
       )
@@ -52,7 +54,9 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
 
 
     it('should render a link to initiate integration linking', function () {
     it('should render a link to initiate integration linking', function () {
       expect(
       expect(
-        screen.getByRole('link', { name: 'Link' }).getAttribute('href')
+        screen
+          .getByRole('link', { name: 'Link Integration' })
+          .getAttribute('href')
       ).to.equal('/link')
       ).to.equal('/link')
     })
     })
 
 
@@ -86,11 +90,13 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
     })
     })
 
 
     it('should display an `unlink` button', 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 () {
     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'))
       const withinModal = within(screen.getByRole('dialog'))
       withinModal.getByText('confirm unlink')
       withinModal.getByText('confirm unlink')
       withinModal.getByText('you will be unlinked')
       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 () {
     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')
       screen.getByText('confirm unlink')
       const cancelBtn = screen.getByRole('button', {
       const cancelBtn = screen.getByRole('button', {
         name: 'Cancel',
         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 () {
 describe('<SSOLinkingWidget />', function () {
   const defaultProps = {
   const defaultProps = {
     providerId: 'integration_id',
     providerId: 'integration_id',
-    title: 'integration',
-    description: 'integration description',
+    title: 'Integration',
+    description: 'Integration description',
     helpPath: '/help/integration',
     helpPath: '/help/integration',
     linkPath: '/integration/link',
     linkPath: '/integration/link',
     onUnlink: () => Promise.resolve(),
     onUnlink: () => Promise.resolve(),
@@ -22,10 +22,12 @@ describe('<SSOLinkingWidget />', function () {
 
 
   it('should render', function () {
   it('should render', function () {
     render(<SSOLinkingWidget {...defaultProps} />)
     render(<SSOLinkingWidget {...defaultProps} />)
-    screen.getByText('integration')
-    screen.getByText('integration description')
+    screen.getByText('Integration')
+    screen.getByText('Integration description')
     expect(
     expect(
-      screen.getByRole('link', { name: /learn more/i }).getAttribute('href')
+      screen
+        .getByRole('link', { name: 'Learn more about Integration' })
+        .getAttribute('href')
     ).to.equal('/help/integration')
     ).to.equal('/help/integration')
   })
   })
 
 
@@ -33,7 +35,9 @@ describe('<SSOLinkingWidget />', function () {
     it('should render a link to `linkPath`', function () {
     it('should render a link to `linkPath`', function () {
       render(<SSOLinkingWidget {...defaultProps} linked={false} />)
       render(<SSOLinkingWidget {...defaultProps} linked={false} />)
       expect(
       expect(
-        screen.getByRole('link', { name: /link/i }).getAttribute('href')
+        screen
+          .getByRole('link', { name: 'Link Integration' })
+          .getAttribute('href')
       ).to.equal('/integration/link?intent=link')
       ).to.equal('/integration/link?intent=link')
     })
     })
   })
   })
@@ -49,19 +53,23 @@ describe('<SSOLinkingWidget />', function () {
     })
     })
 
 
     it('should display an `unlink` button', 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 () {
     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(
       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 () {
     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', {
       const cancelBtn = screen.getByRole('button', {
         name: 'Cancel',
         name: 'Cancel',
         hidden: false,
         hidden: false,
@@ -80,9 +88,11 @@ describe('<SSOLinkingWidget />', function () {
       render(
       render(
         <SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
         <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', {
       confirmBtn = within(screen.getByRole('dialog')).getByRole('button', {
-        name: /unlink/i,
+        name: 'Unlink',
         hidden: false,
         hidden: false,
       })
       })
     })
     })
@@ -114,11 +124,13 @@ describe('<SSOLinkingWidget />', function () {
       render(
       render(
         <SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
         <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(
       const confirmBtn = within(screen.getByRole('dialog')).getByRole(
         'button',
         'button',
         {
         {
-          name: /unlink/i,
+          name: 'Unlink',
           hidden: false,
           hidden: false,
         }
         }
       )
       )
@@ -130,7 +142,7 @@ describe('<SSOLinkingWidget />', function () {
     })
     })
 
 
     it('should display the unlink button ', async function () {
     it('should display the unlink button ', async function () {
-      await screen.findByRole('button', { name: /unlink/i })
+      await screen.findByRole('button', { name: 'Unlink Integration' })
     })
     })
   })
   })
 })
 })