diff --git a/e2e-tests/case.ts b/e2e-tests/case.ts index 5248111975..8518966888 100644 --- a/e2e-tests/case.ts +++ b/e2e-tests/case.ts @@ -80,3 +80,54 @@ export const caseHome = (page: Page) => { saveCaseAndEnd, }; }; + +// Add utility functions moved from caseList.ts +export async function viewClosePrintView(page: Page) { + const openPrintButton = page.locator('[data-testid="CasePrint-Button"]'); + await openPrintButton.waitFor({ state: 'visible' }); + await openPrintButton.click(); + console.log('Opened Case Print'); + const closePrintButton = page.locator('[data-testid="NavigableContainer-CloseCross"]'); + await closePrintButton.waitFor({ state: 'visible' }); + await closePrintButton.click(); + console.log('Close Case Print'); +} + +export async function clickEditCase(page: Page) { + const editButton = page.locator('[data-testid="Case-EditButton"]'); + await editButton.waitFor({ state: 'visible' }); + await editButton.click(); +} + +export async function updateCaseSummary(page: Page) { + const summaryInput = page.locator('[data-testid="CaseSummary-Input"]'); + await summaryInput.waitFor({ state: 'visible' }); + await summaryInput.fill('Updated summary'); + const saveButton = page.locator('[data-testid="CaseSummary-SaveButton"]'); + await saveButton.waitFor({ state: 'visible' }); + await saveButton.click(); +} + +export async function verifyCaseSummaryUpdated(page: Page) { + const summaryContent = page.locator('[data-testid="CaseSummary-Content"]'); + await summaryContent.waitFor({ state: 'visible' }); + expect(await summaryContent.textContent()).toContain('Updated summary'); +} + +export async function verifyCasePrintButtonIsVisible(page: Page) { + const printButton = page.locator('[data-testid="CasePrint-Button"]'); + await printButton.waitFor({ state: 'visible' }); + expect(await printButton.isVisible()).toBe(true); +} + +export async function verifyCategoryTooltipIsVisible(page: Page) { + const tooltip = page.locator('[data-testid="Category-Tooltip"]'); + await tooltip.waitFor({ state: 'visible' }); + expect(await tooltip.isVisible()).toBe(true); +} + +export async function closeModal(page: Page) { + const closeCaseButton = page.locator('[data-testid="NavigableContainer-CloseCross"]'); + await closeCaseButton.waitFor({ state: 'visible' }); + await closeCaseButton.click(); +} diff --git a/e2e-tests/caseList.ts b/e2e-tests/caseList.ts index b250fcebaf..d635529487 100644 --- a/e2e-tests/caseList.ts +++ b/e2e-tests/caseList.ts @@ -32,6 +32,7 @@ export type CaseSectionForm> = { export const caseList = (page: Page) => { const caseListPage = page.locator('div.Twilio-ViewCollection'); + console.log('Case List table is visible.'); const selectors = { caseListRowIdButton: caseListPage.locator( @@ -94,6 +95,21 @@ export const caseList = (page: Page) => { console.log(`Filtered cases by: ${filter} filter with selection of: ${option}`); } + async function verifyCaseIdsAreInListInOrder(expectedIds: string[]) { + const rows = await page.locator('tr[data-testid^="CaseList-TableRow"]').all(); + + const ids = await Promise.all( + rows.map(async (row) => { + const button = row.locator('[data-testid="CaseList-CaseID-Button"]'); + const buttonText = (await button.textContent())?.trim() || ''; + const caseId = buttonText.replace(/OpenCase/, '').trim(); //extract case id + return caseId; + }), + ); + + expect(ids).toEqual(expectedIds); + } + //Open Case async function openFirstCaseButton() { const openCaseButton = selectors.openFirstCaseButton; @@ -105,94 +121,11 @@ export const caseList = (page: Page) => { return caseHome(page); } - //Check print view - // TODO: Move to case.ts - async function viewClosePrintView() { - const openPrintButton = selectors.casePrintButton; - await openPrintButton.waitFor({ state: 'visible' }); - await openPrintButton.click(); - console.log('Opened Case Print'); - - const closePrintButton = selectors.modalCloseButton; - await closePrintButton.waitFor({ state: 'visible' }); - await closePrintButton.click(); - console.log('Close Case Print'); - } - - //Edit Case - // TODO: Move to case.ts - async function editCase() { - const editCaseButton = selectors.caseEditButton; - await editCaseButton.waitFor({ state: 'visible' }); - await expect(editCaseButton).toContainText('Edit'); - await editCaseButton.click(); - console.log('Edit Case'); - } - - const currentTime = new Date(); - - // Add/Update Summary - // TODO: Move to case.ts - async function updateCaseSummary() { - const summaryTextArea = selectors.caseSummaryTextArea; - await summaryTextArea.waitFor({ state: 'visible' }); - await summaryTextArea.fill(`E2E Case Summary Test Edited on ${currentTime}`); - - const updateCaseButton = selectors.updateCaseButton; - await updateCaseButton.waitFor({ state: 'visible' }); - await expect(updateCaseButton).toContainText('Save'); - const responsePromise = page.waitForResponse('**/cases/**'); - await updateCaseButton.click(); - await responsePromise; - - console.log('Updated Case Summary'); - } - - // Verify case summary update - async function verifyCaseSummaryUpdated() { - const summaryText = selectors.caseSummaryText; - await summaryText.waitFor({ state: 'visible' }); - await expect(summaryText).toContainText(`E2E Case Summary Test Edited on ${currentTime}`); - } - - async function verifyCasePrintButtonIsVisible() { - const printButton = selectors.casePrintButton; - await printButton.waitFor({ state: 'visible' }); - await expect(printButton).toBeVisible(); - } - - async function verifyCategoryTooltipIsVisible() { - const categoryTooltip = selectors.categoryTooltip; - await categoryTooltip.waitFor({ state: 'visible' }); - await expect(categoryTooltip).toBeVisible(); - } - - async function verifyCaseIdsAreInListInOrder(ids: string[]) { - await selectors.caseListRowIdButton.first().waitFor({ state: 'visible' }); - const caseListIdButtons = await selectors.caseListRowIdButton.all(); - expect(caseListIdButtons.length).toBe(ids.length); - await Promise.all(caseListIdButtons.map((l, idx) => expect(l).toContainText(ids[idx]))); - } - - //Close Modal (probably can move this to more generic navigation file now we have more standardised navigation) - async function closeModal() { - const closeCaseButton = selectors.modalCloseButton; - await closeCaseButton.waitFor({ state: 'visible' }); - await closeCaseButton.click(); - } - return { openFilter, closeFilter, filterCases, - openFirstCaseButton, - viewClosePrintView, - editCase, - updateCaseSummary, - verifyCaseSummaryUpdated, - verifyCasePrintButtonIsVisible, - verifyCategoryTooltipIsVisible, - closeModal, verifyCaseIdsAreInListInOrder, + openFirstCaseButton, }; }; diff --git a/e2e-tests/tests/caselist.spec.ts b/e2e-tests/tests/caselist.spec.ts index 72f265308c..9f932493b9 100644 --- a/e2e-tests/tests/caselist.spec.ts +++ b/e2e-tests/tests/caselist.spec.ts @@ -16,6 +16,15 @@ import { Page, request, test } from '@playwright/test'; import { caseList } from '../caseList'; +import { + viewClosePrintView, + clickEditCase, + verifyCaseSummaryUpdated, + verifyCasePrintButtonIsVisible, + verifyCategoryTooltipIsVisible, + closeModal, + updateCaseSummary, +} from '../case'; import { skipTestIfNotTargeted, skipTestIfDataUpdateDisabled } from '../skipTest'; import { notificationBar } from '../notificationBar'; import { setupContextAndPage, closePage } from '../browser'; @@ -51,13 +60,14 @@ test.describe.serial('Open and Edit a Case in Case List page', () => { let page = caseList(pluginPage); await page.filterCases('status', 'Open'); - // await page.filterCases('Counselor', 'Aselo Alerts'); + await page.filterCases('counselor', 'Aselo Alerts'); + const caseHomePage = await page.openFirstCaseButton(); // Open notifications cover up the print icon :facepalm await notificationBar(pluginPage).dismissAllNotifications(); - await page.viewClosePrintView(); + await viewClosePrintView(pluginPage); await caseHomePage.addCaseSection({ sectionTypeId: 'note', @@ -79,16 +89,16 @@ test.describe.serial('Open and Edit a Case in Case List page', () => { }, }); - await page.editCase(); + await clickEditCase(pluginPage); - await page.updateCaseSummary(); + await updateCaseSummary(pluginPage); - await page.verifyCaseSummaryUpdated(); + await verifyCaseSummaryUpdated(pluginPage); - await page.verifyCasePrintButtonIsVisible(); - await page.verifyCategoryTooltipIsVisible(); + await verifyCasePrintButtonIsVisible(pluginPage); + await verifyCategoryTooltipIsVisible(pluginPage); - await page.closeModal(); + await closeModal(pluginPage); console.log('Closed Case'); }); }); diff --git a/e2e-tests/ui-tests/tests/case-list.spec.ts b/e2e-tests/ui-tests/tests/case-list.spec.ts index 7acb21654b..16b24e986f 100644 --- a/e2e-tests/ui-tests/tests/case-list.spec.ts +++ b/e2e-tests/ui-tests/tests/case-list.spec.ts @@ -31,6 +31,28 @@ test.describe.serial('Case List', () => { let page: Page; const cases = hrmCases(); const permissions = hrmPermissions(); + // Define the filters array for easy access and modification + const filters: Filter[] = ['status', 'counselor', 'createdAtFilter', 'updatedAtFilter']; + let caseListPage: ReturnType; + + const warnViolations = (results: AxeResults, componentDescription: string) => { + if (results.violations.length) { + console.warn( + `${results.violations.length} accessibility violations found in ${componentDescription}.`, + ); + } + }; + + const scanFilterDialogue = async (filter: Filter) => { + console.debug(`Scanning the '${filter}' filter dialog...`); + await caseListPage.openFilter(filter); + const filterAccessibilityScanResults = await new AxeBuilder({ page }) + .include(`div[data-testid='CaseList-Filters-Panel']`) + .analyze(); + // expect(filterAccessibilityScanResults.violations).toEqual([]); + warnViolations(filterAccessibilityScanResults, `the '${filter}' filter dialog`); + await caseListPage.openFilter(filter); + }; test.beforeAll(async ({ browser }) => { await mockServer.start(); @@ -43,8 +65,14 @@ test.describe.serial('Case List', () => { await mockServer.stop(); }); - test('Case list loads items', async () => { + test.beforeEach(async () => { await page.goto('/case-list', { waitUntil: 'networkidle' }); + await page.waitForSelector('div.Twilio-View-case-list', { state: 'visible', timeout: 10000 }); + caseListPage = caseList(page); + }); + + test('Case list loads items', async () => { + await page.waitForTimeout(10000); await caseList(page).verifyCaseIdsAreInListInOrder( cases .getMockCases() @@ -58,27 +86,20 @@ test.describe.serial('Case List', () => { .include('div.Twilio-View-case-list') .analyze(); expect(accessibilityScanResults.violations).toEqual([]); - const caseListPage = caseList(page); - - const warnViolations = (results: AxeResults, componentDescription: string) => { - if (results.violations.length) { - console.warn( - `${results.violations.length} accessibility violations found in ${componentDescription}.`, - ); - } - }; - const scanFilterDialogue = async (filter: Filter) => { - console.debug(`Scanning the '${filter}' filter dialog...`); - await caseListPage.openFilter(filter); - const filterAccessibilityScanResults = await new AxeBuilder({ page }) - .include(`div[data-testid='CaseList-Filters-Panel']`) - .analyze(); - // expect(filterAccessibilityScanResults.violations).toEqual([]); - warnViolations(filterAccessibilityScanResults, `the '${filter}' filter dialog`); - await caseListPage.openFilter(filter); - }; + for (const filter of filters) { + await scanFilterDialogue(filter); + } + }); + test('Case list accessibility: screen reader labels', async () => { + // Check that filter buttons have aria-label or accessible name + const filterButtons = await page.$$('[data-testid^="CaseList-Filter-"]'); + for (const btn of filterButtons) { + const ariaLabel = await btn.getAttribute('aria-label'); + const label = await btn.evaluate((el) => el.textContent?.trim() || ''); + expect(ariaLabel || label.length > 0).toBeTruthy(); + } await scanFilterDialogue('status'); await scanFilterDialogue('counselor'); await scanFilterDialogue('createdAtFilter'); @@ -89,12 +110,5 @@ test.describe.serial('Case List', () => { .analyze(); expect(caseHomeAccessibilityScanResults.violations).toEqual([]); warnViolations(caseHomeAccessibilityScanResults, `the case home page`); - await caseListPage.editCase(); - const caseEditAccessibilityScanResults = await new AxeBuilder({ page }) - .include('div.Twilio-View-case-list') - .analyze(); - //expect(caseHomeAccessibilityScanResults.violations).toEqual([]); - warnViolations(caseEditAccessibilityScanResults, `the case summary edit page`); - await caseListPage.closeModal(); }); }); diff --git a/e2e-tests/ui-tests/tests/case-view.spec.ts b/e2e-tests/ui-tests/tests/case-view.spec.ts new file mode 100644 index 0000000000..91eea376af --- /dev/null +++ b/e2e-tests/ui-tests/tests/case-view.spec.ts @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { expect, Page, test } from '@playwright/test'; +import * as mockServer from '../flex-in-a-box/proxied-endpoints'; +import '../flex-in-a-box/local-resources'; +import hrmCases from '../aselo-service-mocks/hrm/cases'; +import hrmPermissions from '../aselo-service-mocks/hrm/permissions'; +import { caseList } from '../../caseList'; +import { clickEditCase, closeModal } from '../../case'; +import AxeBuilder from '@axe-core/playwright'; +import { aseloPage } from '../aselo-service-mocks/aselo-page'; + +test.describe.serial('Case View', () => { + let page: Page; + const cases = hrmCases(); + const permissions = hrmPermissions(); + + test.beforeAll(async ({ browser }) => { + await mockServer.start(); + page = await aseloPage(browser); + await cases.mockCaseEndpoints(page); + await permissions.mockPermissionEndpoint(page); + await page.goto('/case-list', { waitUntil: 'networkidle' }); + }); + + test.afterAll(async () => { + await mockServer.stop(); + }); + + test.beforeEach(async () => { + await page.goto('/case-list', { waitUntil: 'networkidle' }); + await page.waitForSelector('div.Twilio-View-case-list', { state: 'visible', timeout: 10000 }); + await caseList(page).openFirstCaseButton(); + await page.waitForSelector('div[data-testid="CaseHome-CaseDetailsComponent"]', { + state: 'visible', + timeout: 10000, + }); + }); + + test('Case view page is loaded and passes AXE scan', async () => { + const caseViewScanResults = await new AxeBuilder({ page }) + .include('div[data-testid="CaseHome-CaseDetailsComponent"]') + .analyze(); + expect(caseViewScanResults.violations).toEqual([]); + }); + + test('Case view page is accessible and has case information and overview elements', async () => { + const checkElementAccessibility = async (testId: string, attributeName: string) => { + await expect(page.getByTestId(testId)).toBeVisible(); + const element = page.getByTestId(testId); + expect(await element.getAttribute(attributeName)).toBeTruthy(); + }; + + const checkElementHasVisibleLabel = async (testId: string) => { + const input = page.getByTestId(testId); + const labelId = await input.getAttribute('aria-labelledby'); + + if (labelId) { + const label = page.locator(`#${labelId}`); + await expect(label).toBeVisible(); + } + }; + + await checkElementAccessibility('Case-DetailsHeaderCaseId', 'id'); + await expect(page.getByTestId('Case-DetailsHeaderCounselor')).toBeVisible(); + const caseOverviewIds = ['CaseDetailsStatusLabel', 'childIsAtRisk', 'createdAt', 'updatedAt']; + + for (const fieldId of caseOverviewIds) { + await checkElementAccessibility(`Case-CaseOverview-${fieldId}`, 'aria-labelledby'); + await checkElementHasVisibleLabel(`Case-CaseOverview-${fieldId}`); + } + + const caseViewScanResults = await new AxeBuilder({ page }) + .include('div[data-testid="CaseHome-CaseDetailsComponent"]') + .analyze(); + expect(caseViewScanResults.violations).toEqual([]); + + await closeModal(page); + }); + + test('Case overview edit form opens and supports keyboard navigation', async () => { + await clickEditCase(page); + await page.waitForSelector('[data-testid="Case-EditCaseOverview"]', { + state: 'visible', + timeout: 10000, + }); + await expect(page.getByTestId('Case-EditCaseOverview')).toBeVisible(); + + const getActiveElement = () => { + return ( + document.activeElement?.getAttribute('id') || + document.activeElement?.getAttribute('data-testid') + ); + }; + const activeElement = await page.evaluate(getActiveElement); + expect(activeElement).toBeTruthy(); + + const formControls = [ + { selector: '#status', type: 'select' }, + { selector: '#childIsAtRisk', type: 'checkbox' }, + { selector: '#followUpDate', type: 'date' }, + { selector: '#reportDate', type: 'date' }, + { selector: '#operatingArea', type: 'select' }, + { selector: '#priority', type: 'select' }, + { selector: '#summary', type: 'textarea' }, + { selector: '[data-testid="Case-EditCaseScreen-SaveItem"]', type: 'button' }, + ]; + + await page.focus(formControls[0].selector); + + for (let i = 1; i < formControls.length; i++) { + if (i > 0 && formControls[i - 1].type === 'date') { + await page.keyboard.press('Tab'); // day + await page.keyboard.press('Tab'); // year + await page.keyboard.press('Tab'); // calendar icon/next field + } else { + await page.keyboard.press('Tab'); + } + + const focusedElement = await page.evaluate(getActiveElement); + + const expectedId = formControls[i].selector.startsWith('#') + ? formControls[i].selector.substring(1) + : formControls[i].selector.match(/data-testid="([^"]+)"/)?.[1]; + + expect(focusedElement).toBe(expectedId); + } + + await closeModal(page); + }); + + test('Case overview edit form meets accessibility requirements for screen reader', async () => { + await clickEditCase(page); + await page.waitForSelector('[data-testid="Case-EditCaseOverview"]', { + state: 'visible', + timeout: 10000, + }); + await expect(page.getByTestId('Case-EditCaseOverview')).toBeVisible(); + + const caseEditScanResults = await new AxeBuilder({ page }) + .include('div[data-testid="Case-EditCaseOverview"]') + .analyze(); + expect(caseEditScanResults.violations).toEqual([]); + await closeModal(page); + await expect(page.getByTestId('Case-EditCaseOverview')).not.toBeVisible(); + await expect(page.getByTestId('CaseHome-CaseDetailsComponent')).toBeVisible(); + }); +}); diff --git a/plugin-hrm-form/src/components/case/caseOverview/EditCaseOverview.tsx b/plugin-hrm-form/src/components/case/caseOverview/EditCaseOverview.tsx index faa197b83c..facc6c5b89 100644 --- a/plugin-hrm-form/src/components/case/caseOverview/EditCaseOverview.tsx +++ b/plugin-hrm-form/src/components/case/caseOverview/EditCaseOverview.tsx @@ -231,42 +231,44 @@ const EditCaseOverview: React.FC = ({ }; return ( - - - - - - - {l} - {r} - - - -
- - -