From 8a2a099ccc797148f0201a1b043334946cc8e221 Mon Sep 17 00:00:00 2001 From: Chi Bong Ho Date: Fri, 10 Jul 2026 15:23:26 -0400 Subject: [PATCH] UHM-9332 and UHM-9334: Add tests for creating / deleting bed, and test for cancelling transfer request --- e2e/pages/bed-management-page.ts | 87 +++++++++++++++++++ e2e/pages/index.ts | 1 + e2e/pages/ward-page.ts | 32 +++++-- .../bed-management/bed-management.spec.ts | 36 ++++++++ e2e/specs/ward/ward-admission.spec.ts | 18 +++- 5 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 e2e/pages/bed-management-page.ts create mode 100644 e2e/specs/bed-management/bed-management.spec.ts diff --git a/e2e/pages/bed-management-page.ts b/e2e/pages/bed-management-page.ts new file mode 100644 index 0000000..2758dad --- /dev/null +++ b/e2e/pages/bed-management-page.ts @@ -0,0 +1,87 @@ +import { expect, type Page } from '@playwright/test'; +import { step, test } from '../core'; + +export class BedManagementPage { + private constructor(readonly page: Page) {} + + readonly firstAddBedButton = () => this.page.locator('.bed-layout .block.add-block').first(); + readonly bedNumberField = () => this.page.locator('#bed-number-field'); + readonly saveButton = () => this.page.getByRole('button', { name: 'Save', exact: true }); + + locationBlock(locationName: string) { + return this.page.locator('.location.block').filter({ hasText: locationName }); + } + + bedBlock(bedNumber: string) { + return this.page.locator('.existing-bed').filter({ hasText: bedNumber }); + } + + deleteBedButton(bedNumber: string) { + return this.bedBlock(bedNumber).locator('a[title="delete"]'); + } + + @step + static async open(page: Page) { + return test.step('When I navigate to the bed management admission locations page', async () => { + const bedManagementPage = new BedManagementPage(page); + await page.goto('/openmrs/owa/bedmanagement/admissionLocations.html'); + return bedManagementPage; + }); + } + + @step + async selectLocation(locationName: string) { + return test.step(`When I select the "${locationName}" location`, async () => { + await this.locationBlock(locationName).click(); + }); + } + + /** + * Opens the first "Add Bed" slot and saves it with the given bed number. + * Returns the bed number actually saved (the field truncates input beyond its maxlength). + */ + @step + async addBed(bedNumber: string) { + return test.step(`When I add a new bed with number "${bedNumber}"`, async () => { + await this.firstAddBedButton().click(); + await this.bedNumberField().fill(bedNumber); + const savedBedNumber = await this.bedNumberField().inputValue(); + await this.saveButton().click(); + return savedBedNumber; + }); + } + + @step + async expectBedCreated(bedNumber: string) { + return test.step(`Then I should see bed "${bedNumber}" displayed`, async () => { + await expect(this.bedBlock(bedNumber)).toBeVisible(); + }); + } + + /** + * Hovers over the bed to reveal its delete button (hidden until hover), clicks it, + * and accepts the native JS confirm dialog that follows. + */ + @step + async deleteBed(bedNumber: string) { + return test.step(`When I delete bed "${bedNumber}"`, async () => { + this.page.once('dialog', (dialog) => dialog.accept()); + await this.bedBlock(bedNumber).hover(); + await this.deleteBedButton(bedNumber).click(); + }); + } + + @step + async expectDeletedSuccessfullyNotification() { + return test.step('Then I should see a "Deleted successfully" notification', async () => { + await expect(this.page.getByText('Deleted successfully')).toBeVisible(); + }); + } + + @step + async expectBedDeleted(bedNumber: string) { + return test.step(`Then bed "${bedNumber}" should no longer be displayed`, async () => { + await expect(this.bedBlock(bedNumber)).not.toBeVisible(); + }); + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts index 1027330..4f21dc5 100644 --- a/e2e/pages/index.ts +++ b/e2e/pages/index.ts @@ -1,3 +1,4 @@ export * from './dispensing-page'; export * from './ward-page'; export * from './service-queues-page'; +export * from './bed-management-page'; diff --git a/e2e/pages/ward-page.ts b/e2e/pages/ward-page.ts index e3b7f14..1d64f57 100644 --- a/e2e/pages/ward-page.ts +++ b/e2e/pages/ward-page.ts @@ -79,7 +79,7 @@ export class WardPage { static async open(page: Page) { return test.step('When I navigate to the ward page', async () => { const wardPage = new WardPage(page); - wardPage.page.goto('/openmrs/spa/home/ward'); + await wardPage.page.goto('/openmrs/spa/home/ward'); return wardPage; }); } @@ -107,12 +107,16 @@ export class WardPage { @step async selectBed(bedNumber: string) { - return test.step(`When I select bed ${bedNumber} for the patient admission`, async () => { - if (await this.page.getByRole('combobox', { name: 'Choose an option' }).isVisible()) { - await this.page.getByRole('combobox', { name: 'Choose an option' }).click(); - } - await this.page.getByText(new RegExp(`^${bedNumber} · `)).click(); - }); + // if the ward contains many beds, the bed selection will be a dropdown instead of radio buttons. Handle both cases. + const bedDropdown = this.page.getByRole('combobox', { name: 'Choose an option' }); + const isDropdownVisible = await bedDropdown + .waitFor({ state: 'visible', timeout: 2000 }) + .then(() => true) + .catch(() => false); + if (isDropdownVisible) { + await bedDropdown.click(); + } + await this.page.getByText(new RegExp(`^${bedNumber} · `)).click(); } @step @@ -203,4 +207,18 @@ export class WardPage { await expect(this.page.getByText('admission request cancelled')).toBeVisible(); }); } + + @step + async expectBedNumber(bedNumber: string) { + return test.step(`Then I should see bed number ${bedNumber} displayed`, async () => { + await expect(this.page.getByText(bedNumber, { exact: true })).toBeVisible(); + }); + } + + @step + async expectBedNumberNotVisible(bedNumber: string) { + return test.step(`Then I should not see bed number ${bedNumber} displayed`, async () => { + await expect(this.page.getByText(bedNumber, { exact: true })).not.toBeVisible(); + }); + } } diff --git a/e2e/specs/bed-management/bed-management.spec.ts b/e2e/specs/bed-management/bed-management.spec.ts new file mode 100644 index 0000000..0b43648 --- /dev/null +++ b/e2e/specs/bed-management/bed-management.spec.ts @@ -0,0 +1,36 @@ +import { changeLocation } from '../../commands'; +import { KGHLocationsUuids, test } from '../../core'; +import { BedManagementPage, WardPage } from '../../pages'; + +test.describe('Bed Management', () => { + test.beforeEach(async ({ api }) => { + await changeLocation(api, KGHLocationsUuids['MCOE Triage']); + }); + + test('Add a bed via bed mangement, then remove it', async ({ page }) => { + const randomFourDigitNumber = Math.floor(1000 + Math.random() * 9000); + const bedNumber = `TestBed${randomFourDigitNumber}`; + + let bedManagementPage = await BedManagementPage.open(page); + await bedManagementPage.selectLocation('MCOE Triage'); + + const savedBedNumber = await bedManagementPage.addBed(bedNumber); + + await bedManagementPage.expectBedCreated(savedBedNumber); + + // go to the ward page and confirm the bed is displayed there as well + let wardPage = await WardPage.open(page); + await wardPage.expectBedNumber(savedBedNumber); + + bedManagementPage = await BedManagementPage.open(page); + await bedManagementPage.selectLocation('MCOE Triage'); + + await bedManagementPage.deleteBed(savedBedNumber); + await bedManagementPage.expectDeletedSuccessfullyNotification(); + await bedManagementPage.expectBedDeleted(savedBedNumber); + + // go to the ward page and confirm the bed is no longer displayed there as well + wardPage = await WardPage.open(page); + await wardPage.expectBedNumberNotVisible(savedBedNumber); + }); +}); diff --git a/e2e/specs/ward/ward-admission.spec.ts b/e2e/specs/ward/ward-admission.spec.ts index b3be25b..ca22ed3 100644 --- a/e2e/specs/ward/ward-admission.spec.ts +++ b/e2e/specs/ward/ward-admission.spec.ts @@ -8,7 +8,7 @@ test.describe('Ward App', () => { await changeLocation(api, KGHLocationsUuids['MCOE Triage']); }); - test('Admit a patient to a ward from the admission requests list, then transfer to another location', async ({ + test('Admit a patient to a ward from the admission requests list, then transfer to another location, then create transfer request to yet another location and cancel it', async ({ page, api, adultWoman, @@ -51,6 +51,22 @@ test.describe('Ward App', () => { await antenatalWardPage.expectAdmissionSuccessNotification(patientName, bedNumber); await wardPage.expectPatientAdmittedToWard(adultWoman); + + await wardPage.patientCard(patientName).click(); + await wardPage.transfersButton().click(); + await wardPage.selectLocation('Triage | MCOE'); + await wardPage.clickSaveButton(); + await wardPage.expectTransferRequestSubmitted(patientName); + + await changeLocation(api, KGHLocationsUuids['MCOE Triage']); + const mcoeTriageWardPage = await WardPage.open(page); + await mcoeTriageWardPage.manageAdmissionRequests().click(); + await wardPage.cancelAdmissionButton(patientName).click(); + + await wardPage.clinicalNotesField().fill('Patient does not require transfer at this time'); + await wardPage.clickSaveButton(); + + await wardPage.expectAdmissionRequestCancelled(); }); test('Cancel a patient admission request from the ward admission requests list', async ({