Skip to content

Commit eaf6b4c

Browse files
authored
Merge pull request #5654 from masslight/vmolastau/otr-1192-add-smoke-testing-automation
Vmolastau/otr 1192 add smoke testing automation
2 parents 15e3d29 + e3d9d7b commit eaf6b4c

27 files changed

+598
-268
lines changed

.github/workflows/e2e-ehr.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ on:
2121
- staging
2222
- demo
2323
default: 'local'
24+
mode:
25+
description: 'Tests running mode'
26+
required: false
27+
type: choice
28+
options:
29+
- normal
30+
- smoke
31+
default: 'normal'
2432
workflow_call:
2533
inputs:
2634
environment:
@@ -158,7 +166,12 @@ jobs:
158166
if [ "$ENV" == "local" ] || [ "$ENV" == "e2e" ]; then
159167
npm run ehr:e2e:$ENV:integration
160168
else
161-
npm run ehr:e2e:$ENV
169+
MODE="${{ inputs.mode }}"
170+
if [ "$MODE" == "smoke" ]; then
171+
npm run ehr:e2e:$ENV:smoke
172+
else
173+
npm run ehr:e2e:$ENV
174+
fi
162175
fi
163176
env:
164177
CI: true

.github/workflows/e2e-intake.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ on:
2323
- staging
2424
- demo
2525
default: 'local'
26+
mode:
27+
description: 'Tests running mode'
28+
required: false
29+
type: choice
30+
options:
31+
- normal
32+
- smoke
33+
default: 'normal'
2634
workflow_call:
2735
inputs:
2836
environment:
@@ -396,7 +404,12 @@ jobs:
396404
if [ "$ENV" == "local" ]; then
397405
npm run intake:e2e:local:specs
398406
else
399-
npm run intake:e2e:$ENV:specs
407+
MODE="${{ inputs.mode }}"
408+
if [ "$MODE" == "smoke" ]; then
409+
npm run intake:e2e:$ENV:smoke
410+
else
411+
npm run intake:e2e:$ENV:specs
412+
fi
400413
fi
401414
env:
402415
CI: true

E2E_README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ export class ResourceHandler {
207207

208208
- **ENV**: Determines configuration set (`local`, `demo`, `staging`, `testing`) and cascades through entire system
209209
- **INTEGRATION_TEST**: Controls resource creation method (batch vs application endpoints)
210+
- **SMOKE TEST**: Controls picking the right patient and avoid cleanup (for production environments)
210211
- **CI**: Auto-detected, affects retry logic, worker count, and artifact capture settings
211212
- **UI Flag**: Enables headed mode for debugging instead of headless execution
212213
- **Auth Credentials**: System automatically uses enhanced test credentials when available

apps/ehr/tests/e2e-utils/resource-handler.ts

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import Oystehr, { BatchInputPostRequest } from '@oystehr/sdk';
2+
import { Page } from '@playwright/test';
23
import {
34
Address,
45
Appointment,
56
ClinicalImpression,
67
Consent,
8+
ContactPoint,
79
DocumentReference,
810
Encounter,
911
FhirResource,
@@ -20,8 +22,10 @@ import {
2022
import { readFileSync } from 'fs';
2123
import { DateTime } from 'luxon';
2224
import { dirname, join } from 'path';
25+
import { VisitDetailsPage } from 'tests/e2e/page/VisitDetailsPage';
2326
import { fileURLToPath } from 'url';
2427
import {
28+
CancellationReasonOptionsInPerson,
2529
cleanAppointmentGraph,
2630
CreateAppointmentResponse,
2731
createFetchClientWithOystehrAuth,
@@ -30,6 +34,7 @@ import {
3034
FHIR_APPOINTMENT_INTAKE_HARVESTING_COMPLETED_TAG,
3135
FHIR_APPOINTMENT_PREPROCESSED_TAG,
3236
formatPhoneNumber,
37+
genderMap,
3338
GetPaperworkAnswers,
3439
RelationshipOption,
3540
ServiceMode,
@@ -65,18 +70,22 @@ export function getAccessTokenFromUserJson(): string {
6570
return token;
6671
}
6772

73+
const IS_SMOKE_TEST = process.env.SMOKE_TEST === 'true' || false;
74+
6875
const EightDigitsString = '20250519';
6976

70-
export const PATIENT_FIRST_NAME = 'Jon';
71-
export const PATIENT_LAST_NAME = 'Snow';
72-
export const PATIENT_GENDER = 'Male';
77+
export const PATIENT_FIRST_NAME = IS_SMOKE_TEST ? 'Katie' : 'Jon';
78+
export const PATIENT_LAST_NAME = IS_SMOKE_TEST ? 'Patient' : 'Snow';
79+
export const PATIENT_GENDER = IS_SMOKE_TEST ? 'Female' : 'Male';
7380

7481
export const PATIENT_BIRTHDAY = '2002-07-07';
7582
export const PATIENT_BIRTH_DATE_SHORT = '07/07/2002';
7683
export const PATIENT_BIRTH_DATE_LONG = 'July 07, 2002';
7784

7885
export const PATIENT_PHONE_NUMBER = '21' + EightDigitsString;
79-
export const PATIENT_EMAIL = `john.doe.${EightDigitsString}3@example.com`;
86+
export const PATIENT_EMAIL = IS_SMOKE_TEST
87+
? `ykulik+${PATIENT_FIRST_NAME}${PATIENT_LAST_NAME}@masslight.com`
88+
: `john.doe.${EightDigitsString}3@example.com`;
8089
export const PATIENT_CITY = 'New York';
8190
export const PATIENT_LINE = `${EightDigitsString} Test Line`;
8291
export const PATIENT_LINE_2 = 'Apt 4B';
@@ -171,6 +180,25 @@ export class ResourceHandler {
171180
});
172181
}
173182

183+
private async findTestPatientResource(): Promise<Patient | null> {
184+
const oystehr = await this.apiClient;
185+
const patientSearchResults = await oystehr.fhir.search<Patient>({
186+
resourceType: 'Patient',
187+
params: [
188+
{
189+
name: 'given',
190+
value: 'Katie',
191+
},
192+
{
193+
name: 'family',
194+
value: 'Patient',
195+
},
196+
],
197+
});
198+
const patient = patientSearchResults.unbundle()[0] as Patient;
199+
return patient;
200+
}
201+
174202
public async createAppointment(inputParams?: CreateTestAppointmentInput): Promise<CreateAppointmentResponse> {
175203
try {
176204
const address: Address = {
@@ -180,17 +208,53 @@ export class ResourceHandler {
180208
postalCode: inputParams?.postalCode ?? PATIENT_POSTAL_CODE,
181209
};
182210

183-
const patientData = {
184-
firstNames: [inputParams?.firstName ?? PATIENT_FIRST_NAME],
185-
lastNames: [inputParams?.lastName ?? PATIENT_LAST_NAME],
186-
numberOfAppointments: 1,
187-
reasonsForVisit: [inputParams?.reasonsForVisit ?? PATIENT_REASON_FOR_VISIT],
188-
phoneNumbers: [inputParams?.phoneNumber ?? PATIENT_PHONE_NUMBER],
189-
emails: [inputParams?.email ?? PATIENT_EMAIL],
190-
gender: inputParams?.gender ?? PATIENT_GENDER.toLowerCase(),
191-
birthDate: inputParams?.birthDate ?? PATIENT_BIRTHDAY,
192-
address: [address],
193-
};
211+
let patientData = {};
212+
let phoneNumber = formatPhoneNumber(PATIENT_PHONE_NUMBER)!;
213+
214+
console.log('Initial phone number:', phoneNumber);
215+
216+
if (IS_SMOKE_TEST) {
217+
console.log('In SMOKE_TEST mode using Katie Patient');
218+
// if it's SMOKE_TEST mode - find Katie Patient patient resource and use it to create appointment
219+
const testPatient = await this.findTestPatientResource();
220+
221+
if (!testPatient) {
222+
console.log('Katie Patient not found, will create new patient for Katie Patient');
223+
} else {
224+
patientData = {
225+
firstNames: [testPatient?.name?.[0]?.given?.[0] ?? ''],
226+
lastNames: [testPatient?.name?.[0]?.family ?? ''],
227+
numberOfAppointments: 1,
228+
reasonsForVisit: [inputParams?.reasonsForVisit ?? PATIENT_REASON_FOR_VISIT],
229+
phoneNumbers: [
230+
testPatient?.telecom
231+
?.find((telecom: ContactPoint) => telecom.system === 'phone')
232+
?.value?.replace('+1', '') || PATIENT_PHONE_NUMBER,
233+
],
234+
emails: [testPatient?.telecom?.find((telecom: ContactPoint) => telecom.system === 'email')?.value ?? ''],
235+
gender: testPatient?.gender ?? genderMap.female,
236+
birthDate: testPatient?.birthDate ?? '',
237+
};
238+
phoneNumber = formatPhoneNumber(
239+
testPatient?.telecom?.find((telecom: ContactPoint) => telecom.system === 'phone')?.value ||
240+
PATIENT_PHONE_NUMBER
241+
)!;
242+
}
243+
}
244+
245+
if (!IS_SMOKE_TEST || Object.keys(patientData).length === 0) {
246+
patientData = {
247+
firstNames: [inputParams?.firstName ?? PATIENT_FIRST_NAME],
248+
lastNames: [inputParams?.lastName ?? PATIENT_LAST_NAME],
249+
numberOfAppointments: 1,
250+
reasonsForVisit: [inputParams?.reasonsForVisit ?? PATIENT_REASON_FOR_VISIT],
251+
phoneNumbers: [inputParams?.phoneNumber ?? PATIENT_PHONE_NUMBER],
252+
emails: [inputParams?.email ?? PATIENT_EMAIL],
253+
gender: inputParams?.gender ?? PATIENT_GENDER.toLowerCase(),
254+
birthDate: inputParams?.birthDate ?? PATIENT_BIRTHDAY,
255+
address: [address],
256+
};
257+
}
194258

195259
if (!process.env.PROJECT_API_ZAMBDA_URL) {
196260
throw new Error('PROJECT_API_ZAMBDA_URL is not set');
@@ -208,11 +272,13 @@ export class ResourceHandler {
208272
throw new Error('PROJECT_ID is not set');
209273
}
210274

275+
console.log(formatPhoneNumber(PATIENT_PHONE_NUMBER)!, phoneNumber);
276+
211277
// Create appointment and related resources using zambda
212278
const appointmentData = await createSampleAppointments({
213279
oystehr: await this.apiClient,
214280
authToken: getAccessTokenFromUserJson(),
215-
phoneNumber: formatPhoneNumber(PATIENT_PHONE_NUMBER)!,
281+
phoneNumber,
216282
createAppointmentZambdaId: this.#createAppointmentZambdaId,
217283
zambdaUrl: process.env.PROJECT_API_ZAMBDA_URL,
218284
serviceMode: this.#flow === 'telemed' ? ServiceMode.virtual : ServiceMode['in-person'],
@@ -347,7 +413,19 @@ export class ResourceHandler {
347413
};
348414
}
349415

350-
public async cleanupResources(): Promise<void> {
416+
public async cleanupResources(page?: Page): Promise<void> {
417+
if (process.env.SMOKE_TEST === 'true') {
418+
console.log('Smoke test mode detected, canceling visits through UI');
419+
if (!page) {
420+
throw new Error('Page instance parameter is required to cancel visit in smoke test mode');
421+
}
422+
await page?.goto(`/visit/${this.appointment.id!}`);
423+
const visitDetails = new VisitDetailsPage(page!);
424+
await visitDetails.clickCancelVisitButton();
425+
await visitDetails.selectCancelationReason(CancellationReasonOptionsInPerson['Patient improved']);
426+
await visitDetails.clickCancelButtonFromDialogue();
427+
return;
428+
}
351429
console.log('------------------------------------------------------------');
352430
console.log('Starting resource cleanup');
353431
// TODO: here we should change appointment id to encounter id when we'll fix this bug in frontend,

apps/ehr/tests/e2e/specs/in-person/inPersonVisit.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ test.describe('In-person visit', async () => {
6464
// await patientInfoPage.inPersonHeader().verifyStatus('ready for provider');
6565
// });
6666

67-
test.describe('happy path', () => {
67+
test.describe('happy path', { tag: '@smoke' }, () => {
6868
const PROCESS_ID = `inPersonVisit.spec.ts-${DateTime.now().toMillis()}`;
6969
let insuranceCarrier1: QuestionnaireItemAnswerOption | undefined;
7070
let insuranceCarrier2: QuestionnaireItemAnswerOption | undefined;
@@ -147,7 +147,7 @@ test.describe('In-person visit', async () => {
147147
});
148148

149149
test.afterAll(async () => {
150-
await resourceHandler.cleanupResources();
150+
await resourceHandler.cleanupResources(page);
151151
await page.close();
152152
await context.close();
153153
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { BrowserContext, Page, test } from '@playwright/test';
2+
import { DateTime } from 'luxon';
3+
import { ResourceHandler } from 'tests/e2e-utils/resource-handler';
4+
5+
const PROCESS_ID = `visitDetailsPage.spec.ts-${DateTime.now().toMillis()}`;
6+
const resourceHandler = new ResourceHandler(PROCESS_ID, 'in-person');
7+
8+
test.describe('Visit details page', { tag: '@smoke' }, async () => {
9+
let page: Page;
10+
let context: BrowserContext;
11+
// let visitDetailsPage: VisitDetailsPage;
12+
13+
test.beforeAll(async ({ browser }) => {
14+
await resourceHandler.setResources();
15+
await resourceHandler.waitTillHarvestingDone(resourceHandler.appointment.id!);
16+
17+
context = await browser.newContext();
18+
page = await context.newPage();
19+
await page.goto(`/visit/${resourceHandler.appointment.id!}`);
20+
// visitDetailsPage = await expectVisitDetailsPage(page, resourceHandler.appointment.id!);
21+
});
22+
23+
test.afterAll(async () => {
24+
await resourceHandler.cleanupResources(page);
25+
await page.close();
26+
await context.close();
27+
});
28+
29+
test.describe.configure({ mode: 'serial' });
30+
31+
test('should display visit details page correctly', async () => {});
32+
});

apps/ehr/tests/e2e/specs/patientRecordPage.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ const populateAllRequiredFields = async (patientInformationPage: PatientInformat
262262
}
263263
};
264264

265-
test.describe('Patient Record Page tests', () => {
265+
test.describe('Patient Record Page tests', { tag: '@smoke' }, () => {
266266
const PROCESS_ID = `patientRecordPage-mutating-patient-info-fields-${DateTime.now().toMillis()}`;
267267
const resourceHandler = new ResourceHandler(PROCESS_ID);
268268

@@ -276,9 +276,9 @@ test.describe('Patient Record Page tests', () => {
276276
page = await context.newPage();
277277
});
278278
test.afterAll(async () => {
279+
await resourceHandler.cleanupResources(page);
279280
await page.close();
280281
await context.close();
281-
await resourceHandler.cleanupResources();
282282
});
283283
let patientInformationPage: PatientInformationPage;
284284

@@ -1729,7 +1729,7 @@ test.describe('Patient Record Page tests', () => {
17291729
});
17301730
});
17311731

1732-
test.describe('Patient Record Page tests with zero patient data filled in', async () => {
1732+
test.describe('Patient Record Page tests with zero patient data filled in', { tag: '@smoke' }, async () => {
17331733
const PROCESS_ID = `patientRecordPage-zero-data-${DateTime.now().toMillis()}`;
17341734
const resourceHandler = new ResourceHandler(PROCESS_ID);
17351735

@@ -1743,7 +1743,9 @@ test.describe('Patient Record Page tests with zero patient data filled in', asyn
17431743
});
17441744

17451745
test.afterAll(async () => {
1746-
await resourceHandler.cleanupResources();
1746+
await resourceHandler.cleanupResources(page);
1747+
await page.close();
1748+
await context.close();
17471749
});
17481750

17491751
test('Check state, ethnicity, race, relationship to patient are required', async () => {

apps/intake/tests/specs/0_paperworkSetup/setup.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ function writeTestData(filename: string, data: unknown): void {
145145
// fill in any paperwork.
146146
// ---------------------------------------------------------------------------------------------------------------------
147147

148-
test.describe.parallel('In-Person: Create test patients and appointments', () => {
148+
test.describe.parallel('In-Person: Create test patients and appointments', { tag: '@smoke' }, () => {
149149
test('Create patient without responsible party, with card payment, filling only required fields', async ({
150150
page,
151151
}) => {
@@ -309,7 +309,7 @@ test.describe.parallel('In-Person: Create test patients and appointments', () =>
309309
});
310310
});
311311

312-
test.describe.parallel('Telemed: Create test patients and appointments', () => {
312+
test.describe.parallel('Telemed: Create test patients and appointments', { tag: '@smoke' }, () => {
313313
test('Create patient with responsible party, with insurance payment, filling all fields', async ({ page }) => {
314314
const { flowClass, paperwork } = await test.step('Set up playwright', async () => {
315315
addAppointmentToIdsAndAddMetaTag(page, processId);

0 commit comments

Comments
 (0)