Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ on:
jobs:
main:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repo
uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions e2e/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './patient-operations';
export * from './visit-operations';
61 changes: 61 additions & 0 deletions e2e/commands/patient-operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { type APIRequestContext, expect } from '@playwright/test';
import type { Patient } from '../types';

export const generateRandomPatient = async (api: APIRequestContext): Promise<Patient> => {
const identifierRes = await api.post('idgen/identifiersource/8549f706-7e85-4c1d-9424-217d50a2988b/identifier', {
data: {},
});
await expect(identifierRes.ok()).toBeTruthy();

const { identifier } = await identifierRes.json();

const patientRes = await api.post('patient', {
// TODO: This is not configurable right now. It probably should be.
data: {
identifiers: [
{
identifier,
identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334',
location: '44c3efb0-2583-4c80-a79e-1f756a03c0a1',
preferred: true,
},
],
person: {
addresses: [
{
address1: 'Bom Jesus Street',
address2: '',
cityVillage: 'Recife',
country: 'Brazil',
postalCode: '50030-310',
stateProvince: 'Pernambuco',
},
],
attributes: [],
birthdate: '2020-02-01',
birthdateEstimated: false,
dead: false,
gender: 'M',
names: [
{
familyName: `Smith${Math.floor(Math.random() * 10000)}`,
givenName: `John${Math.floor(Math.random() * 10000)}`,
middleName: '',
preferred: true,
},
],
},
},
});
await expect(patientRes.ok()).toBeTruthy();
return await patientRes.json();
};

export const getPatient = async (api: APIRequestContext, uuid: string): Promise<Patient> => {
const patientRes = await api.get(`patient/${uuid}?v=full`);
return await patientRes.json();
};

export const deletePatient = async (api: APIRequestContext, uuid: string) => {
await api.delete(`patient/${uuid}`, { data: {} });
};
31 changes: 31 additions & 0 deletions e2e/commands/visit-operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type APIRequestContext, expect } from '@playwright/test';
import { type Visit } from '@openmrs/esm-framework';
import dayjs from 'dayjs';

export const startVisit = async (api: APIRequestContext, patientId: string): Promise<Visit> => {
const visitRes = await api.post('visit', {
data: {
startDatetime: dayjs().subtract(1, 'D').format('YYYY-MM-DDTHH:mm:ss.SSSZZ'),
patient: patientId,
location: process.env.E2E_LOGIN_DEFAULT_LOCATION_UUID,
visitType: '7b0f5697-27e3-40c4-8bae-f4049abfb4ed',
attributes: [],
},
});

expect(visitRes.ok()).toBeTruthy();
return await visitRes.json();
};

export const endVisit = async (api: APIRequestContext, visit: Visit) => {
const visitRes = await api.post(`visit/${visit.uuid}`, {
data: {
location: visit.location.uuid,
startDatetime: visit.startDatetime,
visitType: visit.visitType.uuid,
stopDatetime: dayjs().format('YYYY-MM-DDTHH:mm:ss.SSSZZ'),
},
});

return await visitRes.json();
};
2 changes: 1 addition & 1 deletion e2e/fixtures/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { APIRequestContext, PlaywrightWorkerArgs, WorkerFixture } from '@playwright/test';
import { type APIRequestContext, type PlaywrightWorkerArgs, type WorkerFixture } from '@playwright/test';

/**
* A fixture which initializes an [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext)
Expand Down
5 changes: 4 additions & 1 deletion e2e/pages/home-page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';

export class HomePage {
constructor(readonly page: Page) {}
readonly helpButton = () => this.page.locator('button[aria-controls="help-menu-popup"]');
readonly nextButton = () => this.page.getByRole('button', { name: 'Next', exact: true });
readonly finishButton = () => this.page.getByRole('button', { name: 'Last' });

async goto() {
await this.page.goto('/openmrs/spa/home/service-queues');
Expand Down
96 changes: 96 additions & 0 deletions e2e/specs/basic-overview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { test } from '@playwright/test';
import { HomePage } from '../pages';
import { expect } from '@playwright/test';

test('Basic overview tutorial', async ({ page }) => {
const homePage = new HomePage(page);

await test.step('When I visit the home page', async () => {
await homePage.goto();
});

await test.step('And I click the `Help` button', async () => {
await homePage.helpButton().click();
});

await test.step('And I click the `Tutorials` button', async () => {
await page.getByText(/tutorials/i).click();
});

await test.step('Then I should see the Tutorials modal', async () => {
await expect(page.getByRole('heading', { name: /tutorials/i })).toBeVisible();
await expect(
page.getByText(/find walkthroughs and video tutorials on some of the core features of openmrs./i),
).toBeVisible();
});

await test.step('And I click the `Basic Overview` tutorial', async () => {
await page
.locator('li')
.filter({ hasText: /basic overview/i })
.locator('a', { hasText: /walkthrough/i })
.click();
});

await test.step('Then I should see the first tooltip', async () => {
await expect(
page.getByText(
/welcome to openmrs! this is the main dashboard where you can navigate to various features of the system./i,
),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the second tooltip', async () => {
await expect(
page.getByText(/this is the search icon. use it to find patients in the system quickly./i),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the third tooltip', async () => {
await expect(
page.getByText(/this is the add patient icon. click here to register a new patient into the system./i),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the fourth tooltip', async () => {
await expect(
page.getByText(/the user icon. click here to change your user preferences and settings./i),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the fifth tooltip', async () => {
await expect(
page.getByText(/this table displays active visits. here you can see all the ongoing patient visits./i),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the last tooltip', async () => {
await expect(
page.getByText(/this table shows appointments. view and manage patient appointments from this section./i),
).toBeVisible();
});

await test.step('And I click the `Finish` button', async () => {
await homePage.finishButton().click();
});
});
111 changes: 111 additions & 0 deletions e2e/specs/demo-tutorial.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { test } from '@playwright/test';
import { HomePage } from '../pages';
import { expect } from '@playwright/test';

test('Demo tutorial', async ({ page }) => {
const homePage = new HomePage(page);

await test.step('When I visit the home page', async () => {
await homePage.goto();
});

await test.step('And I click the `Help` button', async () => {
await homePage.helpButton().click();
});

await test.step('And I click the `Tutorials` button', async () => {
await page.getByText(/tutorials/i).click();
});

await test.step('Then I should see the Tutorial modal', async () => {
await expect(page.getByRole('heading', { name: /tutorials/i })).toBeVisible();
await expect(
page.getByText(/find walkthroughs and video tutorials on some of the core features of openmrs./i),
).toBeVisible();
});

await test.step('And I click the `Tutorial for demo purposes` tutorial', async () => {
await page
.locator('li')
.filter({ hasText: /tutorial for demo purposes/i })
.locator('a', { hasText: /walkthrough/i })
.click();
});

await test.step('Then I should see the first tooltip', async () => {
await expect(page.getByText(/let us walk through the tutorial features together./i)).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the second tooltip', async () => {
await expect(
page.getByText(
/click on this link. this step is configured to be automatic and will take you to the next step. once the given query selector resolves an element on the page, it will proceed automatically./i,
),
).toBeVisible();
});

await test.step('And I click the `Appointments` button', async () => {
await page.evaluate(() => {
document.querySelector('.react-joyride__overlay')?.setAttribute('style', 'z-index: 1 !important');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to use this at multiple instances (mostly for a button click) because Playwright was unable to interact with the button as it was covered by a tooltip overlay.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @harshthakkr, Could you please send a screenshot of this scenario?

Copy link
Author

@harshthakkr harshthakkr Mar 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen.Recording.2025-03-13.at.10.37.51.AM.mov

Playwright Error

Error: locator.click: Test timeout of 180000ms exceeded.
Call log:

  • waiting for getByRole('button', { name: 'Search patient' })
  • locator resolved to <button type="button" name="SearchPatientIcon" aria-labe…>…</button>
  • attempting click action
  • waiting for element to be visible, enabled and stable
  • element is visible, enabled and stable
  • scrolling into view if needed
  • done scrolling
  • <div role="presentation" data-test-id="overlay" class…>…</div> from <div id="react-joyride-portal">…</div>

});
await page.getByRole('link', { name: /appointments/i }).click();
});

await test.step('Then I should see the third tooltip', async () => {
await expect(page.getByText(/congrats! you have reached the clinical appointments dashboard./i)).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the fourth tooltip', async () => {
await expect(
page.getByText(
/Now, let’s see how this behaves when elements take a bit longer to load. Set your network throttling to "Slow 3G" and hit "Next"./i,
),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the fifth tooltip', async () => {
await expect(
page.getByText(
/Let's navigate to the laboratory page. Our next target is the "Tests Ordered" table. I’ll disappear once you reach the laboratory page and reappear when the table is loaded. See you there!/i,
),
).toBeVisible();
});

await test.step('And I click the `Laboratory` button', async () => {
await page.getByRole('link', { name: /laboratory/i }).click();
});

await test.step('Then I should see the sixth tooltip', async () => {
await expect(
page.getByText(
/it's me again. by default, i'll wait for the element to appear, so you don't have to worry about slow components when writing a new tutorial./i,
),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});

await test.step('Then I should see the seventh tooltip', async () => {
await expect(
page.getByText(/Now let's do a fun exercise. Can you find out how to view a patient's allergies on your own?/i),
).toBeVisible();
});

await test.step('And I click the `Next` button', async () => {
await homePage.nextButton().click();
});
});
Loading
Loading