Browse Source

Merge pull request #23806 from overleaf/ii-bs5-manage-group-managers

[web] BS5 Group managers management

GitOrigin-RevId: 912fd29c3d5cf07eab0cd67e2771e60b1ba25fb3
ilkin-overleaf 1 year ago
parent
commit
d5adc6ee4b

+ 2 - 0
services/web/app/src/Features/UserMembership/UserMembershipController.mjs

@@ -120,6 +120,8 @@ async function _renderManagersPage(req, res, next, template) {
     entityConfig
   )
 
+  await SplitTestHandler.promises.getAssignment(req, res, 'bootstrap-5-groups')
+
   res.render(template, {
     name: entityName,
     users,

+ 4 - 0
services/web/app/views/user_membership/group-managers-react.pug

@@ -3,6 +3,10 @@ extends ../layout-marketing
 block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/group-managers'
 
+block vars
+	- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
+	- bootstrap5PageSplitTest = 'bootstrap-5-groups'
+
 block append meta
 	meta(name="ol-users", data-type="json", content=users)
 	meta(name="ol-groupId", data-type="string", content=groupId)

+ 4 - 0
services/web/app/views/user_membership/institution-managers-react.pug

@@ -3,6 +3,10 @@ extends ../layout-marketing
 block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/institution-managers'
 
+block vars
+	- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
+	- bootstrap5PageSplitTest = 'bootstrap-5-groups'
+
 block append meta
 	meta(name="ol-users", data-type="json", content=users)
 	meta(name="ol-groupId", data-type="string", content=groupId)

+ 4 - 0
services/web/app/views/user_membership/publisher-managers-react.pug

@@ -3,6 +3,10 @@ extends ../layout-marketing
 block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/publisher-managers'
 
+block vars
+	- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
+	- bootstrap5PageSplitTest = 'bootstrap-5-groups'
+
 block append meta
 	meta(name="ol-users", data-type="json", content=users)
 	meta(name="ol-groupId", data-type="string", content=groupId)

+ 122 - 95
services/web/frontend/js/features/group-management/components/managers-table.tsx

@@ -1,9 +1,6 @@
 import { useCallback, useState } from 'react'
-import { Button, Col, Form, FormControl, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { deleteJSON, FetchError, postJSON } from '@/infrastructure/fetch-json'
-import MaterialIcon from '../../../shared/components/material-icon'
-import Tooltip from '../../../shared/components/tooltip'
 import getMeta from '../../../utils/meta'
 import { parseEmails } from '../utils/emails'
 import ErrorAlert, { APIError } from './error-alert'
@@ -11,6 +8,17 @@ import UserRow from './user-row'
 import useUserSelection from '../hooks/use-user-selection'
 import { User } from '../../../../../types/group-management/user'
 import { debugConsole } from '@/utils/debugging'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import BackButton from '@/features/group-management/components/back-button'
+import OLCard from '@/features/ui/components/ol/ol-card'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLFormControl from '@/features/ui/components/ol/ol-form-control'
+import OLFormText from '@/features/ui/components/ol/ol-form-text'
+import OLTable from '@/features/ui/components/ol/ol-table'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 type ManagersPaths = {
   addMember: string
@@ -132,136 +140,155 @@ export function ManagersTable({
 
   return (
     <div className="container">
-      <Row>
-        <Col md={10} mdOffset={1}>
-          <h1>
-            <a href="/user/subscription" className="back-btn">
-              <MaterialIcon
-                type="arrow_back"
-                accessibilityLabel={t('back_to_subscription')}
-              />
-            </a>{' '}
-            {groupName || translations.title}
-          </h1>
-          <div className="card">
-            <div className="page-header">
+      <OLRow>
+        <OLCol lg={{ span: 10, offset: 1 }}>
+          <div className="group-heading" data-testid="group-heading">
+            <BackButton
+              href="/user/subscription"
+              accessibilityLabel={t('back_to_subscription')}
+            />
+            <h1 className="heading">{groupName || translations.title}</h1>
+          </div>
+          <OLCard>
+            <div
+              className="page-header mb-4"
+              data-testid="page-header-members-details"
+            >
               <div className="pull-right">
                 {removeMemberInflightCount > 0 ? (
-                  <Button bsStyle="danger" disabled>
+                  <OLButton variant="danger" disabled>
                     {t('removing')}&hellip;
-                  </Button>
+                  </OLButton>
                 ) : (
                   <>
                     {selectedUsers.length > 0 && (
-                      <Button bsStyle="danger" onClick={removeManagers}>
+                      <OLButton variant="danger" onClick={removeManagers}>
                         {translations.remove}
-                      </Button>
+                      </OLButton>
                     )}
                   </>
                 )}
               </div>
-              <h3>{translations.subtitle}</h3>
+              <h2 className="h3 mt-0">{translations.subtitle}</h2>
             </div>
             <div className="row-spaced-small">
               <ErrorAlert error={removeMemberError} />
-              <ul className="list-unstyled structured-list">
-                <li className="container-fluid">
-                  <Row>
-                    <Col xs={4}>
-                      <label htmlFor="select-all" className="sr-only">
-                        {t('select_all')}
-                      </label>
-                      <input
-                        className="select-all"
-                        id="select-all"
-                        type="checkbox"
-                        autoComplete="off"
-                        onChange={handleSelectAllClick}
-                        checked={selectedUsers.length === users.length}
+              <OLTable
+                className="managed-users-table managed-users-list structured-list"
+                data-testid="managed-users-table"
+              >
+                <thead>
+                  <tr>
+                    <th className="cell-checkbox">
+                      <BootstrapVersionSwitcher
+                        bs3={
+                          <input
+                            className="select-all"
+                            type="checkbox"
+                            autoComplete="off"
+                            onChange={handleSelectAllClick}
+                            checked={selectedUsers.length === users.length}
+                            aria-label={t('select_all')}
+                            data-testid="select-all-checkbox"
+                          />
+                        }
+                        bs5={
+                          <OLFormCheckbox
+                            autoComplete="off"
+                            onChange={handleSelectAllClick}
+                            checked={selectedUsers.length === users.length}
+                            aria-label={t('select_all')}
+                            data-testid="select-all-checkbox"
+                          />
+                        }
                       />
-                      <span className="header">{t('email')}</span>
-                    </Col>
-                    <Col xs={4}>
-                      <span className="header">{t('name')}</span>
-                    </Col>
-                    <Col xs={2}>
-                      <Tooltip
+                    </th>
+                    <th>{t('email')}</th>
+                    <th className="cell-name">{t('name')}</th>
+                    <th className="cell-last-active">
+                      <OLTooltip
                         id="last-active-tooltip"
                         description={t('last_active_description')}
                         overlayProps={{
                           placement: 'left',
                         }}
                       >
-                        <span className="header">
+                        <span>
                           {t('last_active')}
                           <sup>(?)</sup>
                         </span>
-                      </Tooltip>
-                    </Col>
-                    <Col xs={2}>
-                      <span className="header">{t('accepted_invite')}</span>
-                    </Col>
-                  </Row>
-                </li>
-                {users.length === 0 && (
-                  <li>
-                    <Row>
-                      <Col md={12} className="text-centered">
+                      </OLTooltip>
+                    </th>
+                    <th className="cell-accepted-invite">
+                      {t('accepted_invite')}
+                    </th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {users.length === 0 && (
+                    <tr>
+                      <td className="text-center" colSpan={5}>
                         <small>{t('no_members')}</small>
-                      </Col>
-                    </Row>
-                  </li>
-                )}
-                {users.map(user => (
-                  <UserRow
-                    key={user.email}
-                    user={user}
-                    selectUser={selectUser}
-                    unselectUser={unselectUser}
-                    selected={selectedUsers.includes(user)}
-                  />
-                ))}
-              </ul>
+                      </td>
+                    </tr>
+                  )}
+                  {users.map(user => (
+                    <UserRow
+                      key={user.email}
+                      user={user}
+                      selectUser={selectUser}
+                      unselectUser={unselectUser}
+                      selected={selectedUsers.includes(user)}
+                    />
+                  ))}
+                </tbody>
+              </OLTable>
             </div>
             <hr />
             <div>
               <p className="small">{t('add_more_managers')}</p>
               <ErrorAlert error={inviteError} />
-              <Form horizontal onSubmit={addManagers} className="form">
-                <Row>
-                  <Col xs={6}>
-                    <FormControl
+              <form onSubmit={addManagers} data-testid="add-members-form">
+                <OLRow>
+                  <OLCol xs={6}>
+                    <OLFormControl
                       type="input"
                       placeholder="jane@example.com, joe@example.com"
                       aria-describedby="add-members-description"
                       value={emailString}
                       onChange={handleEmailsChange}
                     />
-                  </Col>
-                  <Col xs={4}>
-                    {inviteUserInflightCount > 0 ? (
-                      <Button bsStyle="primary" disabled>
-                        {t('adding')}&hellip;
-                      </Button>
-                    ) : (
-                      <Button bsStyle="primary" onClick={addManagers}>
-                        {t('add')}
-                      </Button>
-                    )}
-                  </Col>
-                </Row>
-                <Row>
-                  <Col xs={8}>
-                    <span className="help-block">
+                  </OLCol>
+                  <OLCol xs={4}>
+                    <OLButton
+                      variant="primary"
+                      onClick={addManagers}
+                      isLoading={inviteUserInflightCount > 0}
+                      bs3Props={{
+                        loading:
+                          inviteUserInflightCount > 0 ? (
+                            <>{t('adding')}&hellip;</>
+                          ) : (
+                            t('add')
+                          ),
+                      }}
+                    >
+                      {t('add')}
+                    </OLButton>
+                  </OLCol>
+                </OLRow>
+                <OLRow>
+                  <OLCol xs={8}>
+                    <OLFormText bs3Props={{ className: 'help-block' }}>
                       {t('add_comma_separated_emails_help')}
-                    </span>
-                  </Col>
-                </Row>
-              </Form>
+                    </OLFormText>
+                  </OLCol>
+                </OLRow>
+              </form>
             </div>
-          </div>
-        </Col>
-      </Row>
+          </OLCard>
+        </OLCol>
+      </OLRow>
     </div>
   )
 }

+ 71 - 45
services/web/frontend/js/features/group-management/components/user-row.tsx

@@ -1,8 +1,11 @@
 import moment from 'moment'
 import { useCallback } from 'react'
-import { Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { User } from '../../../../../types/group-management/user'
+import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import Icon from '@/shared/components/icon'
+import MaterialIcon from '@/shared/components/material-icon'
 
 type GroupMemberRowProps = {
   user: User
@@ -31,52 +34,75 @@ export default function UserRow({
   )
 
   return (
-    <li key={`user-${user.email}`}>
-      <Row>
-        <Col xs={4}>
-          <label htmlFor={`select-user-${user.email}`} className="sr-only">
-            {t('select_user')}
-          </label>
-          <input
-            className="select-item"
-            id={`select-user-${user.email}`}
-            type="checkbox"
-            autoComplete="off"
-            checked={selected}
-            onChange={e => handleSelectUser(e, user)}
+    <tr key={`user-${user.email}`} className="managed-user-row">
+      <td className="cell-checkbox">
+        <BootstrapVersionSwitcher
+          bs3={
+            <input
+              className="select-item"
+              type="checkbox"
+              autoComplete="off"
+              checked={selected}
+              onChange={e => handleSelectUser(e, user)}
+              aria-label={t('select_user')}
+              data-testid="select-single-checkbox"
+            />
+          }
+          bs5={
+            <OLFormCheckbox
+              autoComplete="off"
+              checked={selected}
+              onChange={e => handleSelectUser(e, user)}
+              aria-label={t('select_user')}
+              data-testid="select-single-checkbox"
+            />
+          }
+        />
+      </td>
+      <td>{user.email}</td>
+      <td className="cell-name">
+        {user.first_name} {user.last_name}
+      </td>
+      <td className="cell-last-active">
+        {user.last_active_at
+          ? moment(user.last_active_at).format('Do MMM YYYY')
+          : 'N/A'}
+      </td>
+      <td className="cell-accepted-invite">
+        {user.invite ? (
+          <BootstrapVersionSwitcher
+            bs3={
+              <Icon
+                type="times"
+                accessibilityLabel={t('invite_not_accepted')}
+              />
+            }
+            bs5={
+              <MaterialIcon
+                type="clear"
+                accessibilityLabel={t('invite_not_accepted')}
+              />
+            }
           />
-          <span>{user.email}</span>
-        </Col>
-        <Col xs={4}>
-          {user.first_name} {user.last_name}
-        </Col>
-        <Col xs={2}>
-          {user.last_active_at
-            ? moment(user.last_active_at).format('Do MMM YYYY')
-            : 'N/A'}
-        </Col>
-        <Col xs={2}>
-          {user.invite ? (
-            <>
-              <i
-                className="fa fa-times"
-                aria-hidden="true"
-                aria-label={t('invite_not_accepted')}
+        ) : (
+          <BootstrapVersionSwitcher
+            bs3={
+              <Icon
+                type="check"
+                className="text-success"
+                accessibilityLabel={t('accepted_invite')}
               />
-              <span className="sr-only">{t('invite_not_accepted')}</span>
-            </>
-          ) : (
-            <>
-              <i
-                className="fa fa-check text-success"
-                aria-hidden="true"
-                aria-label={t('accepted_invite')}
+            }
+            bs5={
+              <MaterialIcon
+                type="check"
+                className="text-success"
+                accessibilityLabel={t('accepted_invite')}
               />
-              <span className="sr-only">{t('accepted_invite')}</span>
-            </>
-          )}
-        </Col>
-      </Row>
-    </li>
+            }
+          />
+        )}
+      </td>
+    </tr>
   )
 }

+ 8 - 0
services/web/frontend/stylesheets/bootstrap-5/components/group-members.scss

@@ -71,6 +71,10 @@
       text-align: right;
     }
 
+    .cell-accepted-invite {
+      width: 17%;
+    }
+
     &.managed-users-active {
       .cell-email {
         width: 30%;
@@ -131,6 +135,10 @@
       text-align: right;
     }
 
+    .cell-accepted-invite {
+      width: 17%;
+    }
+
     &.managed-users-active {
       .cell-email {
         width: 41%;

+ 8 - 0
services/web/frontend/stylesheets/components/group-members.less

@@ -126,6 +126,10 @@
       min-width: 25px;
     }
 
+    .cell-accepted-invite {
+      width: 17%;
+    }
+
     &.managed-users-active {
       .cell-email {
         width: 35%;
@@ -185,6 +189,10 @@
       min-width: 25px;
     }
 
+    .cell-accepted-invite {
+      width: 17%;
+    }
+
     &.managed-users-active {
       .cell-email {
         width: 42%;

+ 83 - 61
services/web/test/frontend/features/group-management/components/group-managers.spec.tsx

@@ -35,23 +35,25 @@ describe('group managers', function () {
   })
 
   it('renders the group management page', function () {
-    cy.get('h1').contains('My Awesome Team')
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('john.doe@test.com')
-        cy.contains('John Doe')
-        cy.contains('15th Jan 2023')
-        cy.get(`[aria-label="Invite not yet accepted"]`)
+    cy.findByRole('heading', { name: /my awesome team/i, level: 1 })
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByText('john.doe@test.com')
+          cy.findByText('John Doe')
+          cy.findByText('15th Jan 2023')
+          cy.findByText('Invite not yet accepted')
+        })
+
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByText('bobby.lapointe@test.com')
+          cy.findByText('Bobby Lapointe')
+          cy.findByText('2nd Jan 2023')
+          cy.findByText('Accepted invite')
+        })
       })
-
-      cy.get('li:nth-child(3)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
-      })
-    })
   })
 
   it('sends an invite', function () {
@@ -65,16 +67,20 @@ describe('group managers', function () {
       },
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
+    cy.findByTestId('add-members-form').within(() => {
+      cy.findByRole('textbox').type('someone.else@test.com')
+      cy.findByRole('button').click()
+    })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(4)').within(() => {
-        cy.contains('someone.else@test.com')
-        cy.contains('N/A')
-        cy.get(`[aria-label="Invite not yet accepted"]`)
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(3)').within(() => {
+          cy.findByText('someone.else@test.com')
+          cy.findByText('N/A')
+          cy.findByText('Invite not yet accepted')
+        })
       })
-    })
   })
 
   it('tries to send an invite and displays the error', function () {
@@ -87,31 +93,41 @@ describe('group managers', function () {
       },
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
-    cy.get('.alert').contains('Error: User already added')
+    cy.findByTestId('add-members-form').within(() => {
+      cy.findByRole('textbox').type('someone.else@test.com')
+      cy.findByRole('button').click()
+    })
+    cy.findByRole('alert').should('contain.text', 'Error: User already added')
   })
 
   it('checks the select all checkbox', function () {
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).should('not.be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByLabelText(/select user/i).should('not.be.checked')
+        })
       })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
-      })
-    })
 
-    cy.get('.select-all').click()
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('be.checked')
+    cy.findByTestId('managed-users-table')
+      .find('thead')
+      .within(() => {
+        cy.findByLabelText(/select all/i).check()
       })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('be.checked')
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).should('be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByLabelText(/select user/i).should('be.checked')
+        })
       })
-    })
   })
 
   it('remove a member', function () {
@@ -119,22 +135,26 @@ describe('group managers', function () {
       statusCode: 200,
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).check()
+        })
       })
-    })
 
-    cy.get('button').contains('Remove manager').click()
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
+    cy.findByRole('button', { name: /remove manager/i }).click()
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByText('bobby.lapointe@test.com')
+          cy.findByText('Bobby Lapointe')
+          cy.findByText('2nd Jan 2023')
+          cy.findByText('Accepted invite')
+        })
       })
-    })
   })
 
   it('tries to remove a manager and displays the error', function () {
@@ -142,13 +162,15 @@ describe('group managers', function () {
       statusCode: 500,
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).check()
+        })
       })
-    })
-    cy.get('button').contains('Remove manager').click()
+    cy.findByRole('button', { name: /remove manager/i }).click()
 
-    cy.get('.alert').contains('Sorry, something went wrong')
+    cy.findByRole('alert').should('contain.text', 'Sorry, something went wrong')
   })
 })

+ 83 - 61
services/web/test/frontend/features/group-management/components/institution-managers.spec.tsx

@@ -35,23 +35,25 @@ describe('institution managers', function () {
   })
 
   it('renders the institution management page', function () {
-    cy.get('h1').contains('My Awesome Institution')
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('john.doe@test.com')
-        cy.contains('John Doe')
-        cy.contains('15th Jan 2023')
-        cy.get(`[aria-label="Invite not yet accepted"]`)
+    cy.findByRole('heading', { name: /my awesome institution/i, level: 1 })
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByText('john.doe@test.com')
+          cy.findByText('John Doe')
+          cy.findByText('15th Jan 2023')
+          cy.findByText('Invite not yet accepted')
+        })
+
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByText('bobby.lapointe@test.com')
+          cy.findByText('Bobby Lapointe')
+          cy.findByText('2nd Jan 2023')
+          cy.findByText('Accepted invite')
+        })
       })
-
-      cy.get('li:nth-child(3)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
-      })
-    })
   })
 
   it('sends an invite', function () {
@@ -65,16 +67,20 @@ describe('institution managers', function () {
       },
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
+    cy.findByTestId('add-members-form').within(() => {
+      cy.findByRole('textbox').type('someone.else@test.com')
+      cy.findByRole('button').click()
+    })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(4)').within(() => {
-        cy.contains('someone.else@test.com')
-        cy.contains('N/A')
-        cy.get(`[aria-label="Invite not yet accepted"]`)
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(3)').within(() => {
+          cy.findByText('someone.else@test.com')
+          cy.findByText('N/A')
+          cy.findByText('Invite not yet accepted')
+        })
       })
-    })
   })
 
   it('tries to send an invite and displays the error', function () {
@@ -87,31 +93,41 @@ describe('institution managers', function () {
       },
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
-    cy.get('.alert').contains('Error: User already added')
+    cy.findByTestId('add-members-form').within(() => {
+      cy.findByRole('textbox').type('someone.else@test.com')
+      cy.findByRole('button').click()
+    })
+    cy.findByRole('alert').should('contain.text', 'Error: User already added')
   })
 
   it('checks the select all checkbox', function () {
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).should('not.be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByLabelText(/select user/i).should('not.be.checked')
+        })
       })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
-      })
-    })
 
-    cy.get('.select-all').click()
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('be.checked')
+    cy.findByTestId('managed-users-table')
+      .find('thead')
+      .within(() => {
+        cy.findByLabelText(/select all/i).check()
       })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('be.checked')
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).should('be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByLabelText(/select user/i).should('be.checked')
+        })
       })
-    })
   })
 
   it('remove a member', function () {
@@ -119,22 +135,26 @@ describe('institution managers', function () {
       statusCode: 200,
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).check()
+        })
       })
-    })
 
-    cy.get('button').contains('Remove manager').click()
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
+    cy.findByRole('button', { name: /remove manager/i }).click()
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByText('bobby.lapointe@test.com')
+          cy.findByText('Bobby Lapointe')
+          cy.findByText('2nd Jan 2023')
+          cy.findByText('Accepted invite')
+        })
       })
-    })
   })
 
   it('tries to remove a manager and displays the error', function () {
@@ -142,13 +162,15 @@ describe('institution managers', function () {
       statusCode: 500,
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).check()
+        })
       })
-    })
-    cy.get('button').contains('Remove manager').click()
+    cy.findByRole('button', { name: /remove manager/i }).click()
 
-    cy.get('.alert').contains('Sorry, something went wrong')
+    cy.findByRole('alert').should('contain.text', 'Sorry, something went wrong')
   })
 })

+ 78 - 60
services/web/test/frontend/features/group-management/components/publisher-managers.spec.tsx

@@ -35,23 +35,25 @@ describe('publisher managers', function () {
   })
 
   it('renders the publisher management page', function () {
-    cy.get('h1').contains('My Awesome Publisher')
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('john.doe@test.com')
-        cy.contains('John Doe')
-        cy.contains('15th Jan 2023')
-        cy.get(`[aria-label="Invite not yet accepted"]`)
+    cy.findByRole('heading', { name: /my awesome publisher/i, level: 1 })
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByText('john.doe@test.com')
+          cy.findByText('John Doe')
+          cy.findByText('15th Jan 2023')
+          cy.findByText('Invite not yet accepted')
+        })
+
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByText('bobby.lapointe@test.com')
+          cy.findByText('Bobby Lapointe')
+          cy.findByText('2nd Jan 2023')
+          cy.findByText('Accepted invite')
+        })
       })
-
-      cy.get('li:nth-child(3)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
-      })
-    })
   })
 
   it('sends an invite', function () {
@@ -65,16 +67,20 @@ describe('publisher managers', function () {
       },
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
+    cy.findByTestId('add-members-form').within(() => {
+      cy.findByRole('textbox').type('someone.else@test.com')
+      cy.findByRole('button').click()
+    })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(4)').within(() => {
-        cy.contains('someone.else@test.com')
-        cy.contains('N/A')
-        cy.get(`[aria-label="Invite not yet accepted"]`)
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(3)').within(() => {
+          cy.findByText('someone.else@test.com')
+          cy.findByText('N/A')
+          cy.findByText('Invite not yet accepted')
+        })
       })
-    })
   })
 
   it('tries to send an invite and displays the error', function () {
@@ -87,31 +93,37 @@ describe('publisher managers', function () {
       },
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
-    cy.get('.alert').contains('Error: User already added')
+    cy.findByTestId('add-members-form').within(() => {
+      cy.findByRole('textbox').type('someone.else@test.com')
+      cy.findByRole('button').click()
+    })
+    cy.findByRole('alert').should('contain.text', 'Error: User already added')
   })
 
   it('checks the select all checkbox', function () {
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
-      })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).should('not.be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByLabelText(/select user/i).should('not.be.checked')
+        })
       })
-    })
 
     cy.get('.select-all').click()
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('be.checked')
-      })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('be.checked')
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).should('be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.findByLabelText(/select user/i).should('be.checked')
+        })
       })
-    })
   })
 
   it('remove a member', function () {
@@ -119,22 +131,26 @@ describe('publisher managers', function () {
       statusCode: 200,
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).check()
+        })
       })
-    })
 
-    cy.get('button').contains('Remove manager').click()
-
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
+    cy.findByRole('button', { name: 'Remove manager' }).click()
+
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByText('bobby.lapointe@test.com')
+          cy.findByText('Bobby Lapointe')
+          cy.findByText('2nd Jan 2023')
+          cy.findByText('Accepted invite')
+        })
       })
-    })
   })
 
   it('tries to remove a manager and displays the error', function () {
@@ -142,13 +158,15 @@ describe('publisher managers', function () {
       statusCode: 500,
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    cy.findByTestId('managed-users-table')
+      .find('tbody')
+      .within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.findByLabelText(/select user/i).check()
+        })
       })
-    })
-    cy.get('button').contains('Remove manager').click()
+    cy.findByRole('button', { name: /remove manager/i }).click()
 
-    cy.get('.alert').contains('Sorry, something went wrong')
+    cy.findByRole('alert').should('contain.text', 'Sorry, something went wrong')
   })
 })