diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f062d05 Binary files /dev/null and b/.DS_Store differ diff --git a/.env b/.env index ed48a07..a8f8c85 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -VSAC_API_KEY=#ReplaceMe COMPOSE_PROJECT_NAME=rems_demo +VSAC_API_KEY=#ReplaceMe diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 2d51043..f4113e8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,140 +1,155 @@ -name: Playwright Tests -on: - push: - branches: [ main, dev ] - pull_request: - branches: [ main, dev ] - workflow_dispatch: - inputs: - rems-setup-branch: - description: 'rems set up branch' - required: true - default: 'dev' - rems-intermediary-branch: - description: 'rems intermediary branch' - required: true - default: 'dev' - request-generator-branch: - description: 'request generator branch' - required: true - default: 'dev' - pims-branch: - description: 'pims branch' - required: true - default: 'dev' - rems-admin-branch: - description: 'rems admin branch' - required: true - default: 'dev' - rems-smart-on-fhir-branch: - description: 'rems smart on fhir branch' - required: true - default: 'dev' - rems-test-ehr-branch: - description: 'rems test-ehr branch' - required: true - default: 'dev' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Output Workflow Inputs - run: | - echo 'Input Variables (defaults to main if not specified):' - echo 'rems set up branch: ${{ github.event.inputs.rems-setup-branch }}' - echo 'rems intermediary branch: ${{ github.event.inputs.rems-intermediary-branch }}' - echo 'test ehr branch: ${{ github.event.inputs.rems-test-ehr-branch }}' - echo 'request generator branch: ${{ github.event.inputs.request-generator-branch }}' - echo 'rems admin branch: ${{ github.event.inputs.rems-admin-branch }}' - echo 'pims branch: ${{ github.event.inputs.pims-branch }}' - echo 'rems smart on fhir branch: ${{ github.event.inputs.rems-smart-on-fhir-branch }}' - - - - name: Checkout rems-setup - uses: actions/checkout@v4 - with: - repository: mcode/rems-setup - path: rems-setup - ref: ${{ github.event.inputs.rems-setup-branch }} - - - name: Checkout rems-intermediary repo - uses: actions/checkout@v4 - with: - repository: mcode/rems-intermediary - path: rems-intermediary - submodules: true - ref: ${{ github.event.inputs.rems-intermediary-branch }} - - - - name: Checkout test-ehr repo - uses: actions/checkout@v4 - with: - repository: mcode/test-ehr - path: test-ehr - ref: ${{ github.event.inputs.rems-test-ehr-branch }} - - - name: Checkout request-generator repo - uses: actions/checkout@v4 - with: - repository: mcode/request-generator - path: request-generator - ref: ${{ github.event.inputs.request-generator-branch }} - - - name: Checkout rems-admin repo - uses: actions/checkout@v4 - with: - repository: mcode/rems-admin - path: rems-admin - submodules: true - ref: ${{ github.event.inputs.rems-admin-branch }} - - - name: Checkout pims repo - uses: actions/checkout@v4 - with: - repository: mcode/pims - path: pims - ref: ${{ github.event.inputs.pims-branch }} - - - name: Checkout rems-smart-on-fhir repo - uses: actions/checkout@v4 - with: - repository: mcode/rems-smart-on-fhir - path: rems-smart-on-fhir - submodules: true - ref: ${{ github.event.inputs.rems-smart-on-fhir-branch }} - - - name: Build containers - run: docker compose -f docker-compose-local-build.yml build --no-cache --pull - working-directory: ./rems-setup - env: - VSAC_API_KEY: ${{secrets.VSAC_API_KEY}} - - - name: Start containers - run: docker compose -f docker-compose-local-build.yml up -d --wait --force-recreate - working-directory: ./rems-setup - env: - VSAC_API_KEY: ${{secrets.VSAC_API_KEY}} +# name: Playwright Tests +# on: +# push: +# branches: [ main, dev ] +# pull_request: +# branches: [ main, dev ] +# workflow_dispatch: +# inputs: +# rems-setup-branch: +# description: 'rems set up branch' +# required: true +# default: 'dev' +# rems-intermediary-branch: +# description: 'rems intermediary branch' +# required: true +# default: 'dev' +# request-generator-branch: +# description: 'request generator branch' +# required: true +# default: 'dev' +# pims-branch: +# description: 'pims branch' +# required: true +# default: 'dev' +# rems-admin-branch: +# description: 'rems admin branch' +# required: true +# default: 'dev' +# rems-smart-on-fhir-branch: +# description: 'rems smart on fhir branch' +# required: true +# default: 'dev' +# rems-test-ehr-branch: +# description: 'rems test-ehr branch' +# required: true +# default: 'dev' +# rems-directory-branch: +# description: 'rems directory branch' +# required: true +# default: 'dev' + +# jobs: +# test: +# runs-on: ubuntu-latest +# steps: +# - name: Output Workflow Inputs +# run: | +# echo 'Input Variables (defaults to main if not specified):' +# echo 'rems set up branch: ${{ github.event.inputs.rems-setup-branch }}' +# echo 'rems intermediary branch: ${{ github.event.inputs.rems-intermediary-branch }}' +# echo 'test ehr branch: ${{ github.event.inputs.rems-test-ehr-branch }}' +# echo 'request generator branch: ${{ github.event.inputs.request-generator-branch }}' +# echo 'rems admin branch: ${{ github.event.inputs.rems-admin-branch }}' +# echo 'pims branch: ${{ github.event.inputs.pims-branch }}' +# echo 'rems smart on fhir branch: ${{ github.event.inputs.rems-smart-on-fhir-branch }}' +# echo 'rems smart on directory branch: ${{ github.event.inputs.rems-directory-branch }}' + + + +# - name: Checkout rems-setup +# uses: actions/checkout@v4 +# with: +# repository: mcode/rems-setup +# path: rems-setup +# ref: ${{ github.event.inputs.rems-setup-branch }} + +# - name: Checkout rems-intermediary repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/rems-intermediary +# path: rems-intermediary +# submodules: true +# ref: ${{ github.event.inputs.rems-intermediary-branch }} + + +# - name: Checkout rems-directory repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/rems-directory +# path: rems-directory +# submodules: true +# ref: ${{ github.event.inputs.rems-directory-branch }} + + +# - name: Checkout test-ehr repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/test-ehr +# path: test-ehr +# ref: ${{ github.event.inputs.rems-test-ehr-branch }} + +# - name: Checkout request-generator repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/request-generator +# path: request-generator +# ref: ${{ github.event.inputs.request-generator-branch }} + +# - name: Checkout rems-admin repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/rems-admin +# path: rems-admin +# submodules: true +# ref: ${{ github.event.inputs.rems-admin-branch }} + +# - name: Checkout pims repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/pims +# path: pims +# ref: ${{ github.event.inputs.pims-branch }} + +# - name: Checkout rems-smart-on-fhir repo +# uses: actions/checkout@v4 +# with: +# repository: mcode/rems-smart-on-fhir +# path: rems-smart-on-fhir +# submodules: true +# ref: ${{ github.event.inputs.rems-smart-on-fhir-branch }} + +# - name: Build containers +# run: docker compose -f docker-compose-local-build.yml build --no-cache --pull +# working-directory: ./rems-setup +# env: +# VSAC_API_KEY: ${{secrets.VSAC_API_KEY}} + +# - name: Start containers +# run: docker compose -f docker-compose-local-build.yml up -d --wait --force-recreate +# working-directory: ./rems-setup +# env: +# VSAC_API_KEY: ${{secrets.VSAC_API_KEY}} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Install dependencies - run: npm ci - working-directory: ./rems-setup - - - name: Install Playwright Browsers - run: npx playwright install --with-deps - working-directory: ./rems-setup - - - name: Run Playwright tests - run: npx playwright test - working-directory: ./rems-setup - - - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: ./rems-setup/playwright-report/ - retention-days: 30 +# - uses: actions/setup-node@v3 +# with: +# node-version: 18 + +# - name: Install dependencies +# run: npm ci +# working-directory: ./rems-setup + +# - name: Install Playwright Browsers +# run: npx playwright install --with-deps +# working-directory: ./rems-setup + +# - name: Run Playwright tests +# run: npx playwright test +# working-directory: ./rems-setup + +# - uses: actions/upload-artifact@v4 +# if: always() +# with: +# name: playwright-report +# path: ./rems-setup/playwright-report/ +# retention-days: 30 diff --git a/README.md b/README.md index 07170a8..6fed1db 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Complete end-to-end set up guides for the REMS Proof of Concept prototype are li We use Playwright for end-to-end testing, which automates running the full prototype environment. 1. Install dependencies: `npm install` -2. Run all tests: `npx playwright test` or with the `-ui` flag to view them in the Chromium browser. +2. Run all tests: `npx playwright test` or with the `--ui` flag to view them in the Chromium browser. ## Sequence Diagram diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d2fd7f9..67f17d5 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -94,6 +94,8 @@ services: ports: - "3323:3323" - "3324:3324" + env_file: + - ../rems-directory/.env environment: REMS_ADMIN_1_URL: http://rems-administrator:8090/ REMS_ADMIN_2_URL: http://rems-administrator2:8095/ diff --git a/docker-compose-local-build.yml b/docker-compose-local-build.yml index d706f57..7e9287a 100644 --- a/docker-compose-local-build.yml +++ b/docker-compose-local-build.yml @@ -72,6 +72,8 @@ services: build: context: ../rems-directory container_name: rems_dev_rems-directory + env_file: + - ../rems-directory/.env ports: - "3323:3323" - "3324:4424" diff --git a/docker-compose.yml b/docker-compose.yml index 49548d3..b018b29 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,8 @@ services: build: image: codexrems/rems-directory:1.3 container_name: rems_prod_rems-directory + env_file: + - ../rems-directory/.env ports: - "3323:3323" environment: diff --git a/package-lock.json b/package-lock.json index a7407e3..e3d8628 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,32 +8,32 @@ "playwright": "^1.40.1" }, "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.50.1", "@types/node": "^20.10.1" } }, "node_modules/@playwright/test": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", - "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", "dev": true, "dependencies": { - "playwright": "1.40.1" + "playwright": "1.50.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@types/node": { - "version": "20.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", - "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "version": "20.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", + "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/fsevents": { @@ -50,37 +50,37 @@ } }, "node_modules/playwright": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", - "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", "dependencies": { - "playwright-core": "1.40.1" + "playwright-core": "1.50.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, - "node_modules/playwright/node_modules/playwright-core": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", - "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "node_modules/playwright-core": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true } } diff --git a/package.json b/package.json index bfdcbec..a4adb82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.50.1", "@types/node": "^20.10.1" }, "scripts": {}, diff --git a/playwright.config.ts b/playwright.config.ts index 20446c4..b43211e 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,7 +11,10 @@ import { defineConfig, devices } from '@playwright/test'; */ export default defineConfig({ testDir: './tests', - timeout: 60000, + timeout: 360_000, + expect: { + timeout: 15_000, + }, /* Run tests in files in parallel */ fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 2fd6016..0000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 0000000..c39d162 Binary files /dev/null and b/tests/.DS_Store differ diff --git a/tests/example.spec.ts b/tests/example.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/tests/useCase1/uc1.spec.ts b/tests/legacy-tests/uc1.ts similarity index 94% rename from tests/useCase1/uc1.spec.ts rename to tests/legacy-tests/uc1.ts index abaca5e..33259bd 100644 --- a/tests/useCase1/uc1.spec.ts +++ b/tests/legacy-tests/uc1.ts @@ -17,14 +17,14 @@ const medication = "Turalio"; // test.slow(); -test("UC1: content appears in SMART on FHIR, fill out patient enroll form", async ({ context, page }) => { +test("UC1: Basic Workflow Happy Path", async ({ context, page }) => { // 1. Go to the EHR UI at await page.goto("localhost:3000"); await page.waitForLoadState("networkidle"); // 1a. Sign in await page.getByRole('button', { name: /Launch/ }).click(); - await testUtilKeycloakLogin({ page: page }); + await testUtilKeycloakLogin({ page: page, username: "janedoe", password: "jane" }); // 1c1. Expect blank state. await expect(page).toHaveTitle(/EHR/); @@ -34,9 +34,10 @@ test("UC1: content appears in SMART on FHIR, fill out patient enroll form", asyn // 1b. Clear any lingering state in the database. await page.getByRole('button', { name: 'Settings' }).click(); await page.getByRole('button', { name: 'Reset PIMS Database' }).click(); - await page.getByRole('button', { name: 'Clear In-Progress Forms' }).click(); + await page.getByRole('button', { name: 'Clear EHR In-Progress Forms' }).click(); await page.getByRole('button', { name: 'Reset REMS-Admin Database' }).click(); - await page.getByRole('button', { name: 'Clear EHR MedicationDispenses' }).click(); + await page.getByRole('button', { name: 'Clear EHR Dispense Statuses' }).click(); + await page.getByRole('button', { name: 'Clear EHR Tasks' }).click(); await page.getByRole('button', { name: 'Reconnect EHR' }).click(); @@ -68,7 +69,7 @@ test("UC1: content appears in SMART on FHIR, fill out patient enroll form", asyn // 6. Click **Send Rx to PIMS** at the bottom of the page to send a prescription to the Pharmacist. await page.getByRole('button', { name: 'Send Rx to Pharmacy' }).click(); - // TODO: Expect feedback! but GUI doesn't show any yet. + await page.getByText('Success! NewRx Received By').click(); // 7. Click **Submit to REMS-Admin** at the bottom of the page, which demonstrates the case where an EHR has CDS Hooks // implemented natively. @@ -110,14 +111,8 @@ test("UC1: content appears in SMART on FHIR, fill out patient enroll form", asyn await smartPage.waitForLoadState("networkidle"); await expect(smartPage).toHaveTitle("REMS SMART on FHIR app"); - /* - // This is somehow passing right now? - // 12c1: Error if form not completely filled out. - await submitButton.click(); - await expect(smartPage.getByText(/Error: Partially completed form/)).toBeVisible(); - // 12c1.2: dismiss error dialog to continue - await smartPage.getByRole("button", { name: "OK" }).click(); - */ + await expect(smartPage.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await expect(smartPage.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); @@ -126,7 +121,7 @@ test("UC1: content appears in SMART on FHIR, fill out patient enroll form", asyn await firstField.fill('Jane Doe'); const field = smartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker', exact: true }).getByLabel('Signature *'); - await field.fill('John Doe'); + await field.fill('John Snow'); await smartPage.getByText('Form Loaded:').click(); await testUtilFillOutForm({ page: smartPage, submitButton: peSubmitButton }); @@ -384,7 +379,7 @@ test("UC1: content appears in SMART on FHIR, fill out patient enroll form", asyn await firstField4.fill('Jane Doe'); await psfPage.getByText('Form Loaded:').click(); - await testUtilFillOutForm({ page: pefPage, submitButton: pefSubmitButton }); + await testUtilFillOutForm({ page: pefPage, submitButton: psfSubmitButton }); await page3.getByRole('tab', { name: /HOME/i }).click(); @@ -394,4 +389,12 @@ test("UC1: content appears in SMART on FHIR, fill out patient enroll form", asyn await page3.waitForLoadState("networkidle"); await expect(page3.getByRole('list')).toContainText('Patient Status Update'); + await pharmacyPage2.getByRole('button', { name: 'VIEW ETASU' }).click(); + await expect(pharmacyPage2.getByText('✅').first()).toBeVisible(); + const numChecks2 = await pharmacyPage2.getByText('✅').count(); + expect( + (numChecks2), + `REMS Status panel showed wrong number of green check icons (${numChecks} checks)` + ).toEqual(5); + }); diff --git a/tests/legacy-tests/uc2.ts b/tests/legacy-tests/uc2.ts new file mode 100644 index 0000000..4e5262e --- /dev/null +++ b/tests/legacy-tests/uc2.ts @@ -0,0 +1,589 @@ +/** + +# Test Plan: Demo Workflow + +User: The test is acting as the Prescriber role. + + + */ + +import { expect, test } from "@playwright/test"; +import { testUtilFillOutForm } from "../util/fillOutForm"; +import { testUtilKeycloakLogin } from "../util/keycloakLogin"; + +/* Ideally these would be sourced from the testing environment, but constants are fine too. */ +const patientName = "Jon Snow"; +const medication = "TIRF"; + +// test.slow(); + +test("UC2: Task Workflow Happy Path", async ({ context, page }) => { + // 1. Go to the EHR UI at + await page.goto("localhost:3000"); + await page.waitForLoadState("networkidle"); + + // 1a. Sign in + await page.getByRole('button', { name: /Launch/ }).click(); + await testUtilKeycloakLogin({ page: page, username: "janedoe", password: "jane" }); + + // 1c1. Expect blank state. + await expect(page).toHaveTitle(/EHR/); + await expect(page.getByText("Select A Patient")).toBeVisible(); + await expect(page.getByRole("button", { name: "Send RX to PIMS" })).not.toBeVisible(); + + // 1b. Clear any lingering state in the database. + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('button', { name: 'Reset PIMS Database' }).click(); + await page.getByRole('button', { name: 'Clear EHR In-Progress Forms' }).click(); + await page.getByRole('button', { name: 'Reset REMS-Admin Database' }).click(); + await page.getByRole('button', { name: 'Clear EHR Dispense Statuses' }).click(); + await page.getByRole('button', { name: 'Clear EHR Tasks' }).click(); + await page.getByRole('button', { name: 'Reconnect EHR' }).click(); + + + // 2. Click **Patient Select** button in upper left. + await page.getByRole('button', { name: 'Select a Patient' }).click(); + const searchField = await page.getByLabel('Search'); + await searchField.fill(patientName); + await page.getByRole('option', { name: patientName }).click(); + + // 3. Find **Jon Snow** in the list of patients and click the first dropdown menu next to his name. + await expect(page.getByText("ID").first()).toBeVisible(); + await page.getByRole('button', { name: 'Request New Medication' }).click(); + + + // 4. Select **TIRF 200 UG Oral Transmucosal Lozenge 1237051** in the dropdown menu. + await page.getByRole('rowheader', { name: 'TIRF 200 UG Oral Transmucosal' }).click(); + + + // 5c. Expect the search dialog to have closed. + await expect(page.getByText("Bobby Tables")).not.toBeVisible(); + + // 5c2. Check that patient got selected + await expect(page.getByText(`Name: ${patientName}`)).toBeVisible(); + + // 5c3. Check that medication got selected + const medicationRE = new RegExp(`MedicationRequest.*${medication}`, "i"); + await expect(page.getByText(medicationRE)).toBeVisible(); + + // 6. Click **Send Rx to PIMS** at the bottom of the page to send a prescription to the Pharmacist. + await page.getByRole('button', { name: 'Send Rx to Pharmacy' }).click(); + + await page.getByText('Success! NewRx Received By').click(); + + // 7. Click **Submit to REMS-Admin** at the bottom of the page, which demonstrates the case where an EHR has CDS Hooks + // implemented natively. + await page.getByRole("button", { name: "Sign Order" }).click(); + + // 8. After several seconds you should receive a response in the form of two **CDS cards**: + // ??? what is the right timeout here? + // TODO: Can we make this slightly more specific in a way that's meaningful / visible to the user? + // e.g. `page.getByRole("warning", { name: "No Cards" })...` + await expect(page.getByText("No Cards")).not.toBeVisible({ timeout: 5000 }); + + // TODO: These are fragile selectors--fix the GUI to be more testable / user friendly (e.g. by adding title to a card) + const patientRequirementsCard = page.locator(".MuiCardContent-root", { + hasText: `${medication} REMS Patient Requirements`, + }); + + const prescriberRequirementsCard = page.locator(".MuiCardContent-root", { + hasText: `${medication} REMS Prescriber Requirements`, + }); + + await expect(patientRequirementsCard).toBeInViewport(); + await expect(prescriberRequirementsCard).toBeVisible(); + + + + // 9. Add each form as a task to complete later + + await page.getByRole('button', { name: 'Add "Completion of Patient' }).click(); + await page.getByRole('button', { name: 'Add "Completion of Prescriber Enrollment Questionnaire" to task list' }).click(); + await page.getByRole('button', { name: 'Add "Completion of Prescriber Knowledge Assessment Questionnaire" to task list' }).click(); + + // go to tasks + await page.locator('div:nth-child(2) > .MuiButtonBase-root').first().click(); + await page.getByRole('button', { name: 'Refresh' }).click(); + + await page.getByRole('tab', { name: 'UNASSIGNED TASKS (3)' }).click(); + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")').first()).toBeVisible() + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Patient Enrollment Questionnaire")').first()).toBeVisible() + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Enrollment Questionnaire")').first()).toBeVisible() + + + // ToDO: Enable Assigning from prescriber to Nurse - corresponds to line 226 + + // assign patient enrollment form to nurse + // await page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Patient Enrollment Questionnaire")') + // .locator('..')// Move up a level to the main container of the card + // .locator('button:has-text("Assign")').first().click(); + // await page.getByRole('menuitem', { name: 'Assign to nurse (Alice Nurse)' }).click(); + + // assign prescriber enrollment form to nurse + // await page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Enrollment Questionnaire")') + // .locator('..')// Move up a level to the main container of the card + // .locator('button:has-text("Assign")').first().click(); + + // await page.getByRole('menuitem', { name: 'Assign to nurse (Alice Nurse)' }).click(); + + + // assign prescriber knowledge assessment + await page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Assign")').first().click(); + await page.getByRole('menuitem', { name: 'Assign to me (Jane Doe)' }).click(); + + // go to my tasks + await page.getByRole('tab', { name: 'MY TASKS' }).click(); + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")').first()).toBeVisible() + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('div.RequestDashboard-taskTabHeader-18').last()).toContainText('STATUS: READY'); + + // BEFORE the click, set up promise to listen for new tab being opened. see + const knowledgeAssessmentSmartOnFHIRPagePromise = page.waitForEvent("popup"); + + await page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Launch")').first().click(); + + // Actually wait for the new page, and use it for the next part of the test. + const knowledgeAssessmentSmartPage = await knowledgeAssessmentSmartOnFHIRPagePromise; + + +// // 10. If you are asked for login credentials, use **alice** for username and **alice** for password +// // NOTE: You cannot have a conditional in a test, so this is written to always require login. +// // await testUtilKeycloakLogin({ page: smartPage }); + +// // 11. A webpage should open in a new tab, and after a few seconds, a questionnaire should appear. + await knowledgeAssessmentSmartPage.waitForLoadState("networkidle"); + await expect(knowledgeAssessmentSmartPage).toHaveTitle("REMS SMART on FHIR app"); + + // 12c1: Error if form not completely filled out. + await expect(knowledgeAssessmentSmartPage.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await knowledgeAssessmentSmartPage.getByRole('button', { name: '*You must include a value for' }).click(); + await expect(knowledgeAssessmentSmartPage.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); + + // 12c1.2: dismiss error dialog to continue + // await nurseSmartPage.getByRole("button", { name: "OK" }).click(); + + //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// + // expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); + const knowledgeAssessmentSubmitButton = knowledgeAssessmentSmartPage.getByRole("button", { name: "Submit REMS Bundle" }); + + const firstField = knowledgeAssessmentSmartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker NPI *' }).getByLabel('Signature *'); + await firstField.fill('Jane Doe'); + + await knowledgeAssessmentSmartPage.getByText('Form Loaded:').click(); + await testUtilFillOutForm({ page: knowledgeAssessmentSmartPage, submitButton: knowledgeAssessmentSubmitButton }); + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")').first()).toBeVisible() + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('div.RequestDashboard-taskTabHeader-18').last()).toContainText('STATUS: IN-PROGRESS'); + + await page.getByRole('button', { name: 'Status' }).click(); + await page.getByRole('menuitem', { name: 'completed' }).click(); + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('div.RequestDashboard-taskTabHeader-18').last()).toContainText('STATUS: COMPLETED'); + + await page.getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await page.getByRole('button', { name: 'Refresh' }).click(); + + await expect(page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Knowledge Assessment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('#simple-tabpanel-0').getByText('Complete Prescriber Knowledge')).toBeHidden(); + + + // nurse context + const nurseContext = await context.browser()!.newContext(); + const nursePage = await nurseContext.newPage(); // Create new page in Playwright's browser + await nursePage.goto('http://localhost:3000/'); + await nursePage.waitForLoadState("networkidle"); + + await nursePage.getByRole('button', { name: /Launch/ }).click(); + await testUtilKeycloakLogin({ page: nursePage, username: "alice", password: "alice" }); + + // 1c1. Expect blank state. + await expect(page).toHaveTitle(/EHR/); + + await nursePage.getByRole('button', { name: 'Select a patient All Patients' }).click(); + await nursePage.getByLabel('Search').click(); + await nursePage.getByRole('combobox', { name: 'Search' }).fill('jon snow'); + await nursePage.getByRole('button', { name: 'Select Patient' }).click(); + await nursePage.getByRole('button', { name: 'Refresh' }).click(); + await nursePage.getByRole('tab', { name: 'UNASSIGNED TASKS' }).click(); + + // ToDO: Remove once prescriber can assign to nurse - corresponds to line 111 + + // assign patient enrollment form to nurse + await nursePage.locator('div.RequestDashboard-taskTabDescription-22:has-text("Complete Patient Enrollment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Assign")').first().click(); + await nursePage.getByRole('menuitem', { name: 'Assign to me (Alice Nurse)' }).click(); + + // assign prescriber enrollment form to nurse + await nursePage.locator('div.RequestDashboard-taskTabDescription-22:has-text("Complete Prescriber Enrollment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Assign")').first().click(); + + await nursePage.getByRole('menuitem', { name: 'Assign to me (Alice Nurse)' }).click(); + + await nursePage.getByRole('tab', { name: 'MY TASKS' }).click(); + + // BEFORE the click, set up promise to listen for new tab being opened. see + const nurseSmartOnFHIRPagePrescriberEnrollmentPromise = nursePage.waitForEvent("popup"); + + await nursePage.locator('div.RequestDashboard-taskTabDescription-22:has-text("Complete Prescriber Enrollment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Launch")').first().click(); + + // Actually wait for the new page, and use it for the next part of the test. + const nurseSmartPagePrescriberEnrollment = await nurseSmartOnFHIRPagePrescriberEnrollmentPromise; + + +// // 10. If you are asked for login credentials, use **alice** for username and **alice** for password +// // NOTE: You cannot have a conditional in a test, so this is written to always require login. +// // await testUtilKeycloakLogin({ page: smartPage }); + +// // 11. A webpage should open in a new tab, and after a few seconds, a questionnaire should appear. + await nurseSmartPagePrescriberEnrollment.waitForLoadState("networkidle"); + await expect(nurseSmartPagePrescriberEnrollment).toHaveTitle("REMS SMART on FHIR app"); + + // 12c1: Error if form not completely filled out. + await expect(nurseSmartPagePrescriberEnrollment.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await nurseSmartPagePrescriberEnrollment.getByRole('button', { name: '*You must include a value for' }).click(); + await expect(nurseSmartPagePrescriberEnrollment.getByText('Signature', { exact: true })).toBeVisible(); + await expect(nurseSmartPagePrescriberEnrollment.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); + + // 12c1.2: dismiss error dialog to continue + // await nurseSmartPage.getByRole("button", { name: "OK" }).click(); + + //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// + // expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); + const nurseSaveButton = nurseSmartPagePrescriberEnrollment.getByRole("button", { name: "Save to EHR" }); + + // const firstField = smartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker NPI *' }).getByLabel('Signature *'); + // await firstField.fill('Jane Doe'); + + // const field = smartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker', exact: true }).getByLabel('Signature *'); + // await field.fill('John Doe'); + + // await smartPage.getByText('Form Loaded:').click(); + await testUtilFillOutForm({ page: nurseSmartPagePrescriberEnrollment, submitButton: nurseSaveButton }); + await nurseSmartPagePrescriberEnrollment.getByRole('button', { name: 'OK' }).click(); + + // return to back office page + + // await expect(page.locator('#simple-tabpanel-0').getByText('Complete Prescriber Enrollment')).toBeVisible() + + // await page.getByRole('button', { name: 'Status' }).click(); + // await page.getByRole('menuitem', { name: 'ready' }).click(); + + await nursePage.locator('div.RequestDashboard-taskTabDescription-22:has-text("Complete Prescriber Enrollment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Assign")').first().click(); + await nursePage.getByRole('menuitem', { name: 'Assign to requester (Jane' }).click(); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Prescriber Enrollment')).toBeHidden() + + // practitioner enrollment form + await page.getByRole('button', { name: 'Refresh' }).click(); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Prescriber Enrollment')).toBeVisible() + + + // BEFORE the click, set up promise to listen for new tab being opened. see + const prescriberEnrollmentSmartOnFHIRPagePromise = page.waitForEvent("popup"); + + await page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Prescriber Enrollment")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Launch")').first().click(); + + // Actually wait for the new page, and use it for the next part of the test. + const prescriberEnrollmentSmartPage = await prescriberEnrollmentSmartOnFHIRPagePromise; + + +// // 10. If you are asked for login credentials, use **alice** for username and **alice** for password +// // NOTE: You cannot have a conditional in a test, so this is written to always require login. +// // await testUtilKeycloakLogin({ page: smartPage }); + +// // 11. A webpage should open in a new tab, and after a few seconds, a questionnaire should appear. + await prescriberEnrollmentSmartPage.waitForLoadState("networkidle"); + await expect(prescriberEnrollmentSmartPage).toHaveTitle("REMS SMART on FHIR app"); + + // load the most recent in progress form + await prescriberEnrollmentSmartPage.waitForSelector('div[role="dialog"]'); + await prescriberEnrollmentSmartPage.locator('div[role="dialog"] .MuiButtonBase-root').nth(-2).click() + + // 12c1: Error if form not completely filled out. + await expect(prescriberEnrollmentSmartPage.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await prescriberEnrollmentSmartPage.getByRole('button', { name: '*You must include a value for' }).click(); + await expect(prescriberEnrollmentSmartPage.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); + + // 12c1.2: dismiss error dialog to continue + // await nurseSmartPage.getByRole("button", { name: "OK" }).click(); + + //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// + // expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); + const prescriberEnrollmentSubmitButton = prescriberEnrollmentSmartPage.getByRole("button", { name: "Submit REMS Bundle" }); + + const peSignatureField = prescriberEnrollmentSmartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker NPI *' }).getByLabel('Signature *'); + await peSignatureField.fill('Jane Doe'); + + await prescriberEnrollmentSmartPage.getByText('Form Loaded:').click(); + await testUtilFillOutForm({ page: prescriberEnrollmentSmartPage, submitButton: prescriberEnrollmentSubmitButton }); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Prescriber Enrollment')).toBeVisible() + await page.getByRole('button', { name: 'Status' }).click(); + await page.getByRole('menuitem', { name: 'completed' }).click(); + + await page.getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await page.getByRole('button', { name: 'Refresh' }).click(); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Prescriber Enrollment')).toBeHidden() + + const nurseSmartOnFHIRPagePatientEnrollmentPromise = nursePage.waitForEvent("popup"); + + // back to nurse view for patient enrollment form + await nursePage.locator('div.RequestDashboard-taskTabDescription-22:has-text("Complete Patient Enrollment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Launch")').first().click(); + + // Actually wait for the new page, and use it for the next part of the test. + const nurseSmartPagePatientEnrollment = await nurseSmartOnFHIRPagePatientEnrollmentPromise; + + + // // 10. If you are asked for login credentials, use **alice** for username and **alice** for password + // // NOTE: You cannot have a conditional in a test, so this is written to always require login. + // // await testUtilKeycloakLogin({ page: smartPage }); + + // // 11. A webpage should open in a new tab, and after a few seconds, a questionnaire should appear. + await nurseSmartPagePatientEnrollment.waitForLoadState("networkidle"); + await expect(nurseSmartPagePatientEnrollment).toHaveTitle("REMS SMART on FHIR app"); + + // 12c1: Error if form not completely filled out. + await expect(nurseSmartPagePatientEnrollment.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await nurseSmartPagePatientEnrollment.getByRole('button', { name: '*You must include a value for' }).click(); + await expect(nurseSmartPagePatientEnrollment.getByText('Signature', { exact: true }).first()).toBeVisible(); + await expect(nurseSmartPagePatientEnrollment.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); + + // 12c1.2: dismiss error dialog to continue + // await nurseSmartPage.getByRole("button", { name: "OK" }).click(); + + //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// + // expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); + const nurseSavePatientButton = nurseSmartPagePatientEnrollment.getByRole("button", { name: "Save to EHR" }); + + // const firstField = smartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker NPI *' }).getByLabel('Signature *'); + // await firstField.fill('Jane Doe'); + + // const field = smartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker', exact: true }).getByLabel('Signature *'); + // await field.fill('John Doe'); + + // await smartPage.getByText('Form Loaded:').click(); + await testUtilFillOutForm({ page: nurseSmartPagePatientEnrollment, submitButton: nurseSavePatientButton }); + await nurseSmartPagePatientEnrollment.getByRole('button', { name: 'OK' }).click(); + + // return to back office page + + // await expect(page.locator('#simple-tabpanel-0').getByText('Complete Prescriber Enrollment')).toBeVisible() + + // await page.getByRole('button', { name: 'Status' }).click(); + // await page.getByRole('menuitem', { name: 'ready' }).click(); + + await nursePage.locator('div.RequestDashboard-taskTabDescription-22:has-text("Complete Patient Enrollment Questionnaire")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Assign")').first().click(); + await nursePage.getByRole('menuitem', { name: 'Assign to patient (Jon Stark Snow)' }).click(); + + // patient context + const patientContext = await context.browser()!.newContext(); + const patientPage = await patientContext.newPage(); // Create new page in Playwright's browser + await patientPage.goto('http://localhost:3000/patient-portal'); + await patientPage.waitForLoadState("networkidle"); + + await patientPage.getByLabel('Username').click(); + await patientPage.getByLabel('Username').fill('jonsnow'); + await patientPage.getByLabel('Password').click(); + await patientPage.getByLabel('Password').fill('jon'); + + await patientPage.getByRole('button', { name: 'Log In' }).click(); + + await patientPage.getByRole('button', { name: 'Tasks' }).click(); + + + + // // BEFORE the click, set up promise to listen for new tab being opened. see + const patientEnrollmentSmartOnFHIRPagePromise = patientPage.waitForEvent("popup"); + + + // await patientPage.locator('div.RequestDashboard-taskTabDescription-125:has-text("Complete Patient Enrollment")') + // .locator('..')// Move up a level to the main container of the card + // .locator('button:has-text("Launch")').first().click(); + await patientPage.getByRole('button', { name: 'Launch' }).click(); + + + // Actually wait for the new page, and use it for the next part of the test. + const smartPagePatientEnrollment = await patientEnrollmentSmartOnFHIRPagePromise; + + + // // 10. If you are asked for login credentials, use **alice** for username and **alice** for password + // // NOTE: You cannot have a conditional in a test, so this is written to always require login. + + // // 11. A webpage should open in a new tab, and after a few seconds, a questionnaire should appear. + await smartPagePatientEnrollment.waitForLoadState("networkidle"); + await testUtilKeycloakLogin({ page: smartPagePatientEnrollment, username: "jonsnow", password: "jon" }); + + await expect(smartPagePatientEnrollment).toHaveTitle("REMS SMART on FHIR app"); + + + await smartPagePatientEnrollment.locator('div[role="dialog"] .MuiButtonBase-root').nth(-2).click() + + // 12c1: Error if form not completely filled out. + await expect(smartPagePatientEnrollment.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await smartPagePatientEnrollment.getByRole('button', { name: '*You must include a value for' }).click(); + await expect(smartPagePatientEnrollment.getByText('Signature', { exact: true }).first()).toBeVisible(); + await expect(smartPagePatientEnrollment.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); + + // 12c1.2: dismiss error dialog to continue + // await nurseSmartPage.getByRole("button", { name: "OK" }).click(); + + //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// + // expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); + const savePatientButton = smartPagePatientEnrollment.getByRole("button", { name: "Save to EHR" }); + + // const firstField = smartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker NPI *' }).getByLabel('Signature *'); + // await firstField.fill('Jane Doe'); + + const field = smartPagePatientEnrollment.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker', exact: true }).getByLabel('Signature *'); + await field.fill('John Doe'); + + // await smartPage.getByText('Form Loaded:').click(); + await testUtilFillOutForm({ page: smartPagePatientEnrollment, submitButton: savePatientButton }); + await smartPagePatientEnrollment.getByRole('button', { name: 'OK' }).click(); + + // await patientPage.locator('div.RequestDashboard-taskTabDescription-125:has-text("Complete Patient Enrollment Questionnaire")') + // .locator('..')// Move up a level to the main container of the card + // .locator('button:has-text("Assign")').first().click(); + await patientPage.getByRole('button', { name: 'Assign' }).click(); + await patientPage.getByRole('menuitem', { name: 'Assign to requester (Jane' }).click(); + + await expect(patientPage.locator('#simple-tabpanel-0').getByText('Complete Patient Enrollment')).toBeHidden() + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Patient Enrollment')).toBeHidden() + + // practitioner enrollment form + await page.getByRole('button', { name: 'Refresh' }).click(); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Patient Enrollment')).toBeVisible() + + + // BEFORE the click, set up promise to listen for new tab being opened. see + const prescriberPatientEnrollmentSmartOnFHIRPagePromise = page.waitForEvent("popup"); + + await page.locator('div.RequestDashboard-taskTabDescription-19:has-text("Complete Patient Enrollment")') + .locator('..')// Move up a level to the main container of the card + .locator('button:has-text("Launch")').first().click(); + + // Actually wait for the new page, and use it for the next part of the test. + const prescriberPatientEnrollmentSmartPage = await prescriberPatientEnrollmentSmartOnFHIRPagePromise; + + +// // 10. If you are asked for login credentials, use **alice** for username and **alice** for password +// // NOTE: You cannot have a conditional in a test, so this is written to always require login. +// // await testUtilKeycloakLogin({ page: smartPage }); + +// // 11. A webpage should open in a new tab, and after a few seconds, a questionnaire should appear. + await prescriberPatientEnrollmentSmartPage.waitForLoadState("networkidle"); + await expect(prescriberPatientEnrollmentSmartPage).toHaveTitle("REMS SMART on FHIR app"); + + // load the most recent in progress form + await prescriberPatientEnrollmentSmartPage.waitForSelector('div[role="dialog"]'); + await prescriberPatientEnrollmentSmartPage.locator('div[role="dialog"] .MuiButtonBase-root').nth(-2).click() + + // 12c1: Error if form not completely filled out. + await expect(prescriberPatientEnrollmentSmartPage.getByRole('button', { name: '*You must include a value for' })).toBeVisible(); + await prescriberPatientEnrollmentSmartPage.getByRole('button', { name: '*You must include a value for' }).click(); + await expect(prescriberPatientEnrollmentSmartPage.getByRole('button', { name: 'Submit REMS Bundle' })).toBeDisabled(); + + // 12c1.2: dismiss error dialog to continue + // await nurseSmartPage.getByRole("button", { name: "OK" }).click(); + + //////////// 12. Fill out the questionnaire and hit **Submit REMS Bundle**. //////////////// + // expect(smartPage.getByText("Patient Enrollment")).toBeVisible(); + const prescriberPatientEnrollmentSubmitButton = prescriberPatientEnrollmentSmartPage.getByRole("button", { name: "Submit REMS Bundle" }); + + const pePatientSignatureField = prescriberPatientEnrollmentSmartPage.getByRole('row', { name: 'Signature * Name (Printed) * Date * Show date picker NPI *' }).getByLabel('Signature *'); + await pePatientSignatureField.fill('Jane Doe'); + + await prescriberPatientEnrollmentSmartPage.getByText('Form Loaded:').click(); + await testUtilFillOutForm({ page: prescriberPatientEnrollmentSmartPage, submitButton: prescriberPatientEnrollmentSubmitButton }); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Patient Enrollment')).toBeVisible() + await page.getByRole('button', { name: 'Status' }).click(); + await page.getByRole('menuitem', { name: 'completed' }).click(); + + await page.getByRole('button', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await page.getByRole('button', { name: 'Refresh' }).click(); + + await expect(page.locator('#simple-tabpanel-0').getByText('Complete Patient Enrollment')).toBeHidden() + + const pharmacyPage = await context.newPage(); // Create new page in Playwright's browser + await pharmacyPage.goto("http://localhost:5050/"); + await pharmacyPage.waitForLoadState("networkidle"); + + /* 21a: Make sure pharmacy page loaded. */ + await expect(pharmacyPage.getByRole("heading", { name: "Pharmacy" })).toBeVisible(); + + /* 21b. Log in again -- is this necessary?. */ + //await testUtilKeycloakLogin({ page: pharmacyPage }); + + /* 21c. Click **Doctor Orders** in the top hand navigation menu on the screen */ + await pharmacyPage.getByRole("button", { name: /doctor orders/i }).click(); + const pharmacyMedCard = pharmacyPage.locator(".MuiPaper-root", { hasText: medication }).first(); + + /* 21d: Verify we are looking at New Orders */ + await expect(pharmacyMedCard).toBeVisible(); + await expect(pharmacyMedCard.getByText("Pending")).toBeVisible(); + + /* 21e: View etasu, ensure its been met and hit verify order */ + await pharmacyPage.getByRole('button', { name: 'VIEW ETASU' }).click(); + await expect(pharmacyPage.getByText('✅').first()).toBeVisible(); + const numChecks = await pharmacyPage.getByText('✅').count(); + expect( + (numChecks), + `REMS Status panel showed wrong number of green check icons (${numChecks} checks)` + ).toEqual(5); + + await pharmacyPage.getByRole('button', { name: 'Close' }).click(); + + await pharmacyPage.getByRole('button', { name: 'VERIFY ORDER' }).click(); + + await expect(pharmacyMedCard).not.toBeVisible(); + + /* 21f: Go to verify order tab */ + await pharmacyPage.getByRole('tab', { name: 'Verified Orders' }).click(); + await expect(pharmacyMedCard).toBeVisible(); + + await expect(pharmacyMedCard.getByText("Approved")).toBeVisible(); + await pharmacyPage.getByRole('button', { name: 'Mark as Picked Up' }).click(); + await expect(pharmacyMedCard).not.toBeVisible(); + + + /* 21g: Go to mark as pickedup */ + await pharmacyPage.getByRole('tab', { name: 'Picked Up Orders' }).click(); + await expect(pharmacyMedCard).toBeVisible(); + + await expect(pharmacyMedCard.getByText("Picked Up")).toBeVisible(); + +}); \ No newline at end of file diff --git a/tests/rems-workflow.spec.ts b/tests/rems-workflow.spec.ts new file mode 100644 index 0000000..11b9d58 --- /dev/null +++ b/tests/rems-workflow.spec.ts @@ -0,0 +1,547 @@ +import { expect } from "@playwright/test"; +import { test } from './util/test-fixture'; +import { DRUG_CONFIGS, TEST_USERS, TEST_PATIENTS, TEST_PRESCRIBERS } from "./util/configs"; +import { SmartAppPage } from "./util/smartApp"; +import { testUtilKeycloakLogin } from "./util/keycloakLogin"; + +/** + * Test suite for REMS workflow tests + */ +test.describe('REMS Workflows', () => { + /** + * Tests for each drug/patient configuration + */ + for (const drugConfig of DRUG_CONFIGS) { + for (const patient_key in TEST_PATIENTS) { + const patient_name = TEST_PATIENTS[patient_key] + /** + * Synchronous workflow test + * Tests the direct completion of forms without using the task system + */ + test(`EHR workflow for ${patient_name} - ${drugConfig.name}`, async ({ + ehrPage, + pharmacyPage, + }) => { + + // Setup and login + await ehrPage.navigate(); + await ehrPage.login(TEST_USERS.PRESCRIBER.username, TEST_USERS.PRESCRIBER.password); + await ehrPage.resetDatabases(); + + // Select patient and medication + await ehrPage.selectPatient(patient_name); + await ehrPage.selectMedication(drugConfig.searchTerm); + + // Send prescription to pharmacy and sign the order + await ehrPage.sendPrescriptionToPharmacy(); + await ehrPage.signOrder(); + + // Verify CDS cards + await ehrPage.verifyCDSCards(drugConfig.name); + + // Complete all required forms in synchronous workflow + for (const form of drugConfig.forms) { + // Skip Patient Status Update - we'll do this later as a follow-up + if (form.name === "Patient Status Update" || !form.buttonText) continue; + + // Launch SMART app for the form + const smartPage = await ehrPage.launchSmartOnFhirApp(form.buttonText); + const smartAppPage = new SmartAppPage(smartPage); + await smartAppPage.verifyAppLoaded(); + + // Fill out and submit the form + await smartAppPage.fillOutForm(); + + if (form.requiresPatientSignature) { + await smartAppPage.fillPatientSignatureFields(patient_name) + } + + if (form.requiresPrescriberSignature) { + await smartAppPage.fillPrescriberSignatureFields(TEST_PRESCRIBERS.JANE_DOE) + } + + await smartAppPage.submitForm() + } + + // Open pharmacy and verify prescription + await pharmacyPage.navigate(); + await pharmacyPage.navigateToDoctorOrders(); + + // Verify ETASU requirements + await pharmacyPage.findMedicationCard(drugConfig.searchTerm); + const etasuChecks = await pharmacyPage.verifyETASU(); + + // Calculate expected checks based on completed forms + const expectedChecks = drugConfig.forms.filter(f => f.name !== "Patient Status Update").length; + expect(etasuChecks).toEqual(expectedChecks); + + // Process order through pharmacy workflow + await pharmacyPage.verifyOrder(); + await pharmacyPage.switchTab("Verified Orders"); + await pharmacyPage.verifyMedicationStatus(drugConfig.searchTerm, "Approved"); + await pharmacyPage.markAsPickedUp(); + await pharmacyPage.switchTab("Picked Up Orders"); + await pharmacyPage.verifyMedicationStatus(drugConfig.searchTerm, "Picked Up"); + + // Return to EHR to check status + await ehrPage.navigate(); + await ehrPage.login(TEST_USERS.PRESCRIBER.username, TEST_USERS.PRESCRIBER.password); + await ehrPage.selectPatient(patient_name); + await ehrPage.selectMedication(drugConfig.searchTerm); + await ehrPage.signOrder(); + + // Check medication status + const medicationStatus = await ehrPage.checkMedicationStatus(); + expect(medicationStatus).toContain("Picked Up"); + + // If the drug has a Patient Status Update form, submit it as a follow-up + const patientStatusForm = drugConfig.forms.find(f => f.name === "Patient Status Update"); + if (patientStatusForm) { + // Launch and fill out the Patient Status Update form + const updatePage = await ehrPage.launchSmartOnFhirApp(patientStatusForm.buttonText); + const updateAppPage = new SmartAppPage(updatePage); + await updateAppPage.verifyAppLoaded(); + + // Fill out and submit the form + await updateAppPage.fillOutForm(); + + if (patientStatusForm.requiresPatientSignature) { + await updateAppPage.fillPatientSignatureFields(patient_name) + } + + if (patientStatusForm.requiresPrescriberSignature) { + await updateAppPage.fillPrescriberSignatureFields(TEST_PRESCRIBERS.JANE_DOE) + } + + await updateAppPage.submitForm() + + // Verify the Patient Status Update appears in ETASU list + await ehrPage.verifyEtasuRequirement("Patient Status Update"); + + // Verify pharmacy sees the update + const updatedEtasuChecks = await pharmacyPage.verifyETASU(); + expect(updatedEtasuChecks).toEqual(drugConfig.forms.length); + } + }); + + /** + * Task-based workflow test + * Tests completion of forms using the task system with different roles + */ + test(`Task workflow for ${patient_name} - ${drugConfig.name}`, async ({ + ehrPage, + nurseEhrPage, + patientPortalPage, + pharmacyPage, + }) => { + + // Setup and login + await ehrPage.navigate(); + await ehrPage.login(TEST_USERS.PRESCRIBER.username, TEST_USERS.PRESCRIBER.password); + await ehrPage.resetDatabases(); + + // Select patient and medication + await ehrPage.selectPatient(patient_name); + await ehrPage.selectMedication(drugConfig.searchTerm); + + // Send prescription to pharmacy and sign the order + await ehrPage.sendPrescriptionToPharmacy(); + await ehrPage.signOrder(); + + // Verify CDS cards + await ehrPage.verifyCDSCards(drugConfig.name); + + // Filter forms that are task-enabled + const taskForms = drugConfig.forms.filter(form => form.taskDescription); + + // Add forms to task list + for (const form of taskForms) { + // Skip Patient Status Update - we'll do this later as a follow-up + if (form.name === "Patient Status Update") continue; + + if (form.taskDescription) { + await ehrPage.addTaskToList(form.taskDescription) + } + } + + // Go to tasks tab and verify tasks were created + await ehrPage.goToTasks(); + await ehrPage.switchToTaskTab("UNASSIGNED TASKS"); + + // Verify all expected tasks are present + for (const form of taskForms) { + // Skip Patient Status Update - we'll do this later as a follow-up + if (form.name === "Patient Status Update") continue; + + if (form.taskDescription) { + await ehrPage.verifyTaskPresent(form.taskDescription); + } + } + + // ToDo: Once implemented, directly assign tasks to the nurse + + // Handle Prescriber Knowledge Assessment task as the prescriber + const pkaForm = taskForms.find(f => f.name === "Prescriber Knowledge Assessment"); + if (pkaForm && pkaForm.taskDescription) { + + // Assign to self and verify + await ehrPage.assignTask(pkaForm.taskDescription, `Assign to me (${TEST_PRESCRIBERS.JANE_DOE})`); + await ehrPage.switchToTaskTab("MY TASKS"); + await ehrPage.verifyTaskPresent(pkaForm.taskDescription); + await ehrPage.verifyTaskStatus(pkaForm.taskDescription, "READY"); + + // Launch and complete the form + const pkaPage = await ehrPage.launchTaskForm(pkaForm.taskDescription); + const pkaApp = new SmartAppPage(pkaPage); + + await pkaApp.verifyAppLoaded(); + + // Fill out and submit the form + await pkaApp.fillOutForm(); + + if (pkaForm.requiresPrescriberSignature) { + await pkaApp.fillPrescriberSignatureFields(TEST_PRESCRIBERS.JANE_DOE) + } + + await pkaApp.submitForm() + + // Update task status and delete it + await ehrPage.verifyTaskStatus(pkaForm.taskDescription, "IN-PROGRESS"); + await ehrPage.updateTaskStatus("completed"); + await ehrPage.verifyTaskStatus(pkaForm.taskDescription, "COMPLETED"); + await ehrPage.deleteTask(); + await ehrPage.verifyTaskPresent(pkaForm.taskDescription, false); + } + + + // Set up nurse view + await nurseEhrPage.navigate(); + await nurseEhrPage.login(TEST_USERS.NURSE.username, TEST_USERS.NURSE.password); + await nurseEhrPage.selectPatient(patient_name); + await nurseEhrPage.refreshTasks(); + + // Set up patient portal + await patientPortalPage.navigate(); + await patientPortalPage.login(TEST_USERS.PATIENT[patient_key].username, TEST_USERS.PATIENT[patient_key].password); + + // Handle Prescriber Enrollment + const peForm = taskForms.find(f => f.name === "Prescriber Enrollment"); + if (peForm && peForm.taskDescription) { + // Handle Prescriber Enrollment completion as nurse + + // go to unassigned tasks + await nurseEhrPage.switchToTaskTab("UNASSIGNED TASKS"); + + // Nurse assigns to self and partially completes + await nurseEhrPage.assignTask(peForm.taskDescription, "Assign to me (Alice Nurse)"); + await nurseEhrPage.switchToTaskTab("MY TASKS"); + + // Launch and save (not submit) the form + const pePage = await nurseEhrPage.launchTaskForm(peForm.taskDescription); + const peApp = new SmartAppPage(pePage); + + await peApp.verifyAppLoaded(); + + // Fill out and submit the form + await peApp.fillOutForm(); + + await peApp.submitForm("Save to EHR") + + + // Assign back to prescriber + await nurseEhrPage.assignTask(peForm.taskDescription, `Assign to requester (${TEST_PRESCRIBERS.JANE_DOE})`); + + //verify the task is no longer present + await nurseEhrPage.refreshTasks(); + await nurseEhrPage.verifyTaskPresent(peForm.taskDescription, false); + + + + + // Handle Prescriber Enrollment completion as prescriber + // validate the task + await ehrPage.switchToTaskTab("MY TASKS"); + await ehrPage.refreshTasks() + await ehrPage.verifyTaskPresent(peForm.taskDescription); + await ehrPage.verifyTaskStatus(peForm.taskDescription, "IN-PROGRESS"); + + // Complete the form + const peCompletePage = await ehrPage.launchTaskForm(peForm.taskDescription); + const peCompleteApp = new SmartAppPage(peCompletePage); + await peCompleteApp.verifyAppLoaded(); + await peCompleteApp.handleInProgressForms(); + + if (peForm.requiresPrescriberSignature) { + await peCompleteApp.fillPrescriberSignatureFields(TEST_PRESCRIBERS.JANE_DOE) + } + + await peCompleteApp.submitForm() + + // Mark as completed and delete + await ehrPage.updateTaskStatus("completed"); + await ehrPage.verifyTaskStatus(peForm.taskDescription, "COMPLETED"); + await ehrPage.deleteTask(); + await ehrPage.verifyTaskPresent(peForm.taskDescription, false); + } + + + // Handle Patient Enrollment + const patientForm = taskForms.find(f => f.name === "Patient Enrollment"); + if (patientForm && patientForm.taskDescription) { + // Handle Patient Enrollment completion as nurse + // go to unassigned tasks + await nurseEhrPage.switchToTaskTab("UNASSIGNED TASKS"); + + // Nurse assigns to self and partially completes + await nurseEhrPage.assignTask(patientForm.taskDescription, "Assign to me (Alice Nurse)"); + await nurseEhrPage.switchToTaskTab("MY TASKS"); + + // validate the task + await nurseEhrPage.switchToTaskTab("MY TASKS"); + await nurseEhrPage.refreshTasks() + await nurseEhrPage.verifyTaskPresent(patientForm.taskDescription); + await nurseEhrPage.verifyTaskStatus(patientForm.taskDescription, "READY"); + + + // Launch and save the form + const patientPage = await nurseEhrPage.launchTaskForm(patientForm.taskDescription); + const patientApp = new SmartAppPage(patientPage); + + await patientApp.verifyAppLoaded(); + + // Fill out and submit the form + await patientApp.fillOutForm(); + + await patientApp.submitForm("Save to EHR") + + // Assign to patient + await nurseEhrPage.assignTask(patientForm.taskDescription, `Assign to patient (${patient_name})`); + + //verify the task is no longer present + await nurseEhrPage.refreshTasks(); + await nurseEhrPage.verifyTaskPresent(patientForm.taskDescription, false); + + + // Patient continues the enrollment form + // go to the tasks page + await patientPortalPage.goToTasks(); + await patientPortalPage.refreshTasks(); + + // validate the task + await patientPortalPage.verifyTaskPresent(patientForm.taskDescription); + await patientPortalPage.verifyTaskStatus(patientForm.taskDescription, "IN-PROGRESS"); + + // Launch patient enrollment form + const enrollmentPage = await patientPortalPage.launchTaskForm(patientForm.taskDescription); + + // Login to keycloak as patient if needed + await testUtilKeycloakLogin({ + page: enrollmentPage, + username: TEST_USERS.PATIENT[patient_key].username, + password: TEST_USERS.PATIENT[patient_key].password + }); + + // Fill out form + const enrollmentApp = new SmartAppPage(enrollmentPage); + await enrollmentApp.verifyAppLoaded(); + await enrollmentApp.handleInProgressForms(); + + if (patientForm.requiresPatientSignature) { + await enrollmentApp.fillPatientSignatureFields(patient_name) + } + + await enrollmentApp.submitForm("Save to EHR") + + // Assign back to prescriber + await patientPortalPage.assignTask(patientForm.taskDescription, `Assign to requester (${TEST_PRESCRIBERS.JANE_DOE})`); + + // verify that the task is removed + // refresh the page + await patientPortalPage.refreshTasks() + await patientPortalPage.verifyTaskPresent(patientForm.taskDescription, false); + + + + // Handle Patient Enrollment completion as prescriber + + // validate the task + await ehrPage.switchToTaskTab("MY TASKS"); + await ehrPage.refreshTasks() + await ehrPage.verifyTaskPresent(patientForm.taskDescription); + await ehrPage.verifyTaskStatus(patientForm.taskDescription, "IN-PROGRESS"); + + // Complete the form + const patientCompletePage = await ehrPage.launchTaskForm(patientForm.taskDescription); + const patientCompleteApp = new SmartAppPage(patientCompletePage); + await patientCompleteApp.verifyAppLoaded(); + await patientCompleteApp.handleInProgressForms(); + + // Bug Fix: make sure all fields are filled out + await patientCompleteApp.fillOutForm(); + + if (patientForm.requiresPrescriberSignature) { + await patientCompleteApp.fillPrescriberSignatureFields(TEST_PRESCRIBERS.JANE_DOE) + } + + await patientCompleteApp.submitForm() + + // Mark as completed and delete + // Mark as completed and delete + await ehrPage.updateTaskStatus("completed"); + await ehrPage.verifyTaskStatus(patientForm.taskDescription, "COMPLETED"); + await ehrPage.deleteTask(); + await ehrPage.verifyTaskPresent(patientForm.taskDescription, false); + } + + // Open pharmacy and verify prescription + await pharmacyPage.navigate(); + await pharmacyPage.navigateToDoctorOrders(); + + // Verify ETASU requirements + await pharmacyPage.findMedicationCard(drugConfig.searchTerm); + const etasuChecks = await pharmacyPage.verifyETASU(); + + // Calculate expected checks based on completed forms + const expectedChecks = drugConfig.forms.filter(f => f.name !== "Patient Status Update").length; + expect(etasuChecks).toEqual(expectedChecks); + + // Process order through pharmacy workflow + await pharmacyPage.verifyOrder(); + await pharmacyPage.switchTab("Verified Orders"); + await pharmacyPage.verifyMedicationStatus(drugConfig.searchTerm, "Approved"); + await pharmacyPage.markAsPickedUp(); + await pharmacyPage.switchTab("Picked Up Orders"); + await pharmacyPage.verifyMedicationStatus(drugConfig.searchTerm, "Picked Up"); + + // Return to EHR to check status + await ehrPage.navigate(); + await ehrPage.login(TEST_USERS.PRESCRIBER.username, TEST_USERS.PRESCRIBER.password); + await ehrPage.selectPatient(patient_name); + await ehrPage.selectMedication(drugConfig.searchTerm); + await ehrPage.signOrder(); + + // Check medication status + const medicationStatus = await ehrPage.checkMedicationStatus(); + expect(medicationStatus).toContain("Picked Up"); + + // If the drug has a Patient Status Update form, submit it as a follow-up + const patientStatusForm = drugConfig.forms.find(f => f.name === "Patient Status Update"); + if (patientStatusForm && patientStatusForm.taskDescription) { + // assigning the patient status form to the nurse + await ehrPage.addTaskToList(patientStatusForm.taskDescription) + + + // Handle Patient Status Update completion as nurse + // go to unassigned tasks + await nurseEhrPage.refreshTasks() + await nurseEhrPage.switchToTaskTab("UNASSIGNED TASKS"); + + // Nurse assigns to self and partially completes + await nurseEhrPage.assignTask(patientStatusForm.taskDescription, "Assign to me (Alice Nurse)"); + await nurseEhrPage.switchToTaskTab("MY TASKS"); + + // validate the task + await nurseEhrPage.switchToTaskTab("MY TASKS"); + await nurseEhrPage.refreshTasks() + await nurseEhrPage.verifyTaskPresent(patientStatusForm.taskDescription); + await nurseEhrPage.verifyTaskStatus(patientStatusForm.taskDescription, "READY"); + + + // Launch and save the form + const patientPage = await nurseEhrPage.launchTaskForm(patientStatusForm.taskDescription); + const patientApp = new SmartAppPage(patientPage); + + await patientApp.verifyAppLoaded(); + + // Fill out and submit the form + await patientApp.fillOutForm(); + + await patientApp.submitForm("Save to EHR") + + // Assign to patient + await nurseEhrPage.assignTask(patientStatusForm.taskDescription, `Assign to patient (${patient_name})`); + + //verify the task is no longer present + await nurseEhrPage.refreshTasks(); + await nurseEhrPage.verifyTaskPresent(patientStatusForm.taskDescription, false); + + // Patient continues the status form + // go to the tasks page + await patientPortalPage.refreshTasks(); + + // validate the task + await patientPortalPage.verifyTaskPresent(patientStatusForm.taskDescription); + await patientPortalPage.verifyTaskStatus(patientStatusForm.taskDescription, "IN-PROGRESS"); + + // Launch patient enrollment form + const enrollmentPage = await patientPortalPage.launchTaskForm(patientStatusForm.taskDescription); + + // Fill out form + const enrollmentApp = new SmartAppPage(enrollmentPage); + await enrollmentApp.verifyAppLoaded(); + await enrollmentApp.handleInProgressForms(); + + if (patientStatusForm.requiresPatientSignature) { + await enrollmentApp.fillPatientSignatureFields(patient_name) + } + + await enrollmentApp.submitForm("Save to EHR") + + // Assign back to prescriber + await patientPortalPage.assignTask(patientStatusForm.taskDescription, `Assign to requester (${TEST_PRESCRIBERS.JANE_DOE})`); + + // verify that the task is removed + // refresh the page + await patientPortalPage.refreshTasks() + await patientPortalPage.verifyTaskPresent(patientStatusForm.taskDescription, false); + + + // Handle Patient Enrollment completion as prescriber + // validate the task + await ehrPage.goToTasks(); + await ehrPage.switchToTaskTab("MY TASKS"); + await ehrPage.refreshTasks() + await ehrPage.verifyTaskPresent(patientStatusForm.taskDescription); + await ehrPage.verifyTaskStatus(patientStatusForm.taskDescription, "IN-PROGRESS"); + + // Complete the form + const patientCompletePage = await ehrPage.launchTaskForm(patientStatusForm.taskDescription); + const patientCompleteApp = new SmartAppPage(patientCompletePage); + await patientCompleteApp.verifyAppLoaded(); + await patientCompleteApp.handleInProgressForms(); + + // Bug Fix: make sure all fields are filled out + await patientCompleteApp.fillOutForm(); + + if (patientStatusForm.requiresPrescriberSignature) { + await patientCompleteApp.fillPrescriberSignatureFields(TEST_PRESCRIBERS.JANE_DOE) + } + + await patientCompleteApp.submitForm() + + // Mark as completed and delete + // Mark as completed and delete + await ehrPage.updateTaskStatus("completed"); + await ehrPage.verifyTaskStatus(patientStatusForm.taskDescription, "COMPLETED"); + await ehrPage.deleteTask(); + await ehrPage.verifyTaskPresent(patientStatusForm.taskDescription, false); + + + // Return to EHR to check status + await ehrPage.navigate(); + await ehrPage.login(TEST_USERS.PRESCRIBER.username, TEST_USERS.PRESCRIBER.password); + await ehrPage.selectPatient(patient_name); + await ehrPage.selectMedication(drugConfig.searchTerm); + await ehrPage.signOrder(); + + // Verify the Patient Status Update appears in ETASU list + await ehrPage.verifyEtasuRequirement("Patient Status Update"); + + // Verify pharmacy sees the update + const updatedEtasuChecks = await pharmacyPage.verifyETASU(); + expect(updatedEtasuChecks).toEqual(drugConfig.forms.length); + } + }); + } + } +}); \ No newline at end of file diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts deleted file mode 100644 index 883f4f9..0000000 --- a/tests/smoke.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from "@playwright/test"; - -// Smoke test to ensure GUI is actually up and running. - -test("checks if a page is visible at localhost:3000", async ({ page }) => { - await page.goto("localhost:3000"); - await expect(page).toHaveTitle(/EHR/); -}); diff --git a/tests/util/configs.ts b/tests/util/configs.ts new file mode 100644 index 0000000..a4d7024 --- /dev/null +++ b/tests/util/configs.ts @@ -0,0 +1,110 @@ +import { DrugConfig } from "./interfaces"; + +/** + * Drugs configuration for testing + */ +export const DRUG_CONFIGS: DrugConfig[] = [ + { + name: "Turalio", + searchTerm: "Turalio 200 MG Oral Capsule", + forms: [ + { + name: "Patient Enrollment", + buttonText: "Patient Enrollment Form", + taskDescription: "Patient Enrollment Questionnaire", + requiresPatientSignature: true, + requiresPrescriberSignature: true + }, + { + name: "Prescriber Enrollment", + buttonText: "Prescriber Enrollment Form", + taskDescription: "Prescriber Enrollment Questionnaire", + requiresPrescriberSignature: true + }, + { + name: "Prescriber Knowledge Assessment", + buttonText: "Prescriber Knowledge Assessment Form", + taskDescription: "Prescriber Knowledge Assessment Questionnaire", + }, + { + name: "Patient Status Update", + buttonText: "Patient Status Update Form", + requiresPrescriberSignature: true, + taskDescription: "Patient Status Update Questionnaire" + }, + { + name: "Pharmacist Enrollment", + } + ] + }, + { + name: "TIRF", + searchTerm: "TIRF 200 UG Oral Transmucosal", + forms: [ + { + name: "Patient Enrollment", + buttonText: "Patient Enrollment Form", + taskDescription: "Patient Enrollment Questionnaire", + requiresPatientSignature: true, + requiresPrescriberSignature: true + }, + { + name: "Prescriber Enrollment", + buttonText: "Prescriber Enrollment Form", + taskDescription: "Prescriber Enrollment Questionnaire", + requiresPrescriberSignature: true + }, + { + name: "Prescriber Knowledge Assessment", + buttonText: "Prescriber Knowledge Assessment Form", + taskDescription: "Prescriber Knowledge Assessment Questionnaire", + requiresPrescriberSignature: true + }, + { + name: "Pharmacist Enrollment", + }, + { + name: "Pharmacist Knowledge Assessment", + } + ] + }, + { + name: "iPledge/Isotretinoin", + searchTerm: "Isotretinoin 20 MG Oral Capsule", + forms: [ + { + name: "Patient Enrollment", + buttonText: "Patient Enrollment Form", + taskDescription: "Patient Enrollment Questionnaire", + requiresPatientSignature: true, + requiresPrescriberSignature: true + }, + { + name: "Prescriber Enrollment", + buttonText: "Prescriber Enrollment Form", + taskDescription: "Prescriber Enrollment Questionnaire", + }, + { + name: "Pharmacist Enrollment", + } + ] + } +]; + +// Common user accounts for testing +export const TEST_USERS = { + PRESCRIBER: { username: "janedoe", password: "jane" }, + NURSE: { username: "alice", password: "alice" }, + PATIENT: { JON_SNOW: { username: "jonsnow", password: "jon" }, ALICE_SMITH: { username: "alicesmith", password: "alice" }} +}; + +// Common patient data +export const TEST_PATIENTS = { + JON_SNOW: "Jon Snow", + ALICE_SMITH: "Alice Smith" +}; + +// Common patient data +export const TEST_PRESCRIBERS = { + JANE_DOE: "Jane Doe" +}; diff --git a/tests/util/ehr.ts b/tests/util/ehr.ts new file mode 100644 index 0000000..603b1c8 --- /dev/null +++ b/tests/util/ehr.ts @@ -0,0 +1,270 @@ +import { Page, expect, Locator } from "@playwright/test"; +import { testUtilKeycloakLogin } from "./keycloakLogin"; + +/** + * EhrPage: Represents interactions with the Electronic Health Record (EHR) application + */ +export class EhrPage { + // Make page protected so it can be accessed by child classes + protected page: Page; + + constructor(page: Page) { + this.page = page + } + + /** + * Navigate to the EHR application + */ + async navigate() { + await this.page.goto("localhost:3000"); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Login to the EHR application + */ + async login(username: string, password: string) { + await this.page.getByRole('button', { name: 'Launch', exact: true }).click(); + await this.page.waitForTimeout(1000); // Small buffer + + if (await this.page.getByText('Sign in to your account').isVisible()) { + await testUtilKeycloakLogin({ page: this.page, username, password }); + } + + await this.page.waitForLoadState("networkidle"); + + // Verify successful login + await expect(this.page).toHaveTitle(/EHR/); + await expect(this.page.getByText("Select A Patient")).toBeVisible(); + await expect(this.page.getByRole("button", { name: "Send RX to PIMS" })).not.toBeVisible(); + } + + /** + * Reset all databases to ensure clean test state + */ + async resetDatabases() { + await this.page.getByRole('button', { name: 'Settings' }).click(); + await this.page.getByRole('button', { name: 'Reset PIMS Database' }).click(); + await this.page.getByRole('button', { name: 'Clear EHR In-Progress Forms' }).click(); + await this.page.getByRole('button', { name: 'Reset REMS-Admin Database' }).click(); + await this.page.getByRole('button', { name: 'Clear EHR Dispense Statuses' }).click(); + await this.page.getByRole('button', { name: 'Clear EHR Tasks' }).click(); + await this.page.getByRole('button', { name: 'Reconnect EHR' }).click(); + } + + /** + * Select a patient in the EHR + */ + async selectPatient(patientName: string) { + await this.page.getByRole('button', { name: 'Select a Patient' }).click(); + const searchField = await this.page.getByLabel('Search'); + await searchField.fill(patientName); + await this.page.getByRole('option', { name: patientName }).click(); + + // Verify patient selection - check first name + await expect(this.page.getByText(`Full Name: ${patientName.split(' ')[0]}`)).toBeVisible(); + } + + /** + * Select a medication for the current patient + */ + async selectMedication(medicationName: string) { + await this.page.getByRole('button', { name: 'Request New Medication' }).click(); + await this.page.getByRole('rowheader', { name: new RegExp(medicationName, 'i') }).click(); + + // Verify medication selection + await expect(this.page.getByText(`Display: ${medicationName}`)).toBeVisible(); + } + + /** + * Send the prescription to the pharmacy + */ + async sendPrescriptionToPharmacy() { + await this.page.getByRole('button', { name: 'Send Rx to Pharmacy' }).click(); + await this.page.getByText('Success! NewRx Received By').click(); + } + + /** + * Sign the medication order + */ + async signOrder() { + await this.page.getByRole("button", { name: "Sign Order" }).click(); + } + + /** + * Verify CDS cards are displayed correctly + */ + async verifyCDSCards(medication: string) { + await expect(this.page.getByText("No Cards")).not.toBeVisible({ timeout: 500 }); + + const patientRequirementsCard = this.page.locator(".MuiCardContent-root", { + hasText: `${medication} REMS Patient Requirements`, + }); + + const prescriberRequirementsCard = this.page.locator(".MuiCardContent-root", { + hasText: `${medication} REMS Prescriber Requirements`, + }); + + await expect(patientRequirementsCard).toBeInViewport(); + await expect(prescriberRequirementsCard).toBeVisible(); + + return { patientRequirementsCard, prescriberRequirementsCard }; + } + + /** + * Launch SMART on FHIR app from a button + */ + async launchSmartOnFhirApp(buttonText: string = "Patient Enrollment Form") { + const smartOnFHIRPagePromise = this.page.waitForEvent("popup"); + await this.page.getByRole("button", { name: new RegExp(buttonText, "i") }).click(); + const smartPage = await smartOnFHIRPagePromise; + await smartPage.waitForLoadState("networkidle"); + return smartPage; + } + + /** + * Check medication status + * Returns the status text + */ + async checkMedicationStatus() { + await this.page.waitForLoadState("networkidle"); + + await this.page.getByRole("button", { name: /MEDICATION:/i }).click(); + await this.page.waitForTimeout(5000); // Small buffer + await this.page.waitForLoadState("networkidle"); + + const pharmacyPopup = this.page.locator(".MuiBox-root", { hasText: "Medication Status" }); + await expect(pharmacyPopup.getByRole("heading", { name: "Medication Status" })).toBeVisible(); + + // Get status text + const statusText = await pharmacyPopup.getByText(/Status:/i).textContent() || ''; + + // Dismiss popup + await pharmacyPopup.press("Escape"); + + return statusText; + } + + /** + * Verify that a specific ETASU requirement appears in the list + */ + async verifyEtasuRequirement(requirementName: string) { + await this.page.getByRole("button", { name: /ETASU:/i }).click(); + await this.page.waitForLoadState("networkidle"); + await expect(this.page.getByRole('list')).toContainText(requirementName); + + // Dismiss popup + await this.page.locator(".MuiBox-root", { hasText: "REMS Status" }).press("Escape"); + } + + /** + * Add forms as tasks to the task list + */ + async addTaskToList(formName: string) { + // Try to find and click all available "Add to task list" buttons + await this.page.getByRole('button', { + name: `Add "Completion of ${formName}" to task list` + }).click(); + } + + /** + * Navigate to the tasks view + */ + async goToTasks() { + await this.page.locator('div:nth-child(2) > .MuiButtonBase-root').first().click(); + await this.refreshTasks(); + } + + async refreshTasks() { + await this.page.getByRole('button', { name: 'Refresh' }).click(); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Switch to a specific task tab + */ + async switchToTaskTab(tabName: string) { + await this.page.getByRole('tab', { name: new RegExp(tabName, 'i') }).click(); + } + + /** + * Assign a task to a user + */ + async assignTask(taskDescription: string, assignTo: string) { + // Find the task card by its description + const taskCard = this.getTaskCardByDescription(taskDescription); + + // Click the assign button and select the target + await taskCard.locator('button:has-text("Assign")').first().click(); + await this.page.getByRole('menuitem', { name: assignTo }).click(); + } + + /** + * Launch a form from a task + */ + async launchTaskForm(taskDescription: string) { + const smartOnFHIRPagePromise = this.page.waitForEvent("popup"); + + const taskCard = this.getTaskCardByDescription(taskDescription); + await taskCard.locator('button:has-text("Launch")').first().click(); + + const smartPage = await smartOnFHIRPagePromise; + await smartPage.waitForLoadState("networkidle"); + return smartPage; + } + + /** + * Update the status of the current task + */ + async updateTaskStatus(status: 'completed' | 'in-progress' | 'ready') { + await this.page.getByRole('button', { name: 'Status' }).click(); + await this.page.getByRole('menuitem', { name: status }).click(); + } + + /** + * Delete the current task + */ + async deleteTask() { + await this.page.getByRole('button', { name: 'Delete' }).click(); + await this.page.getByRole('button', { name: 'Yes' }).click(); + this.refreshTasks(); + } + /** + * Verify a task is present in the nurse UI + */ + async verifyTaskPresent(taskDescription: string, shouldBePresent = true) { + const taskCard = this.getTaskCardByDescription(taskDescription).first(); + + if (shouldBePresent) { + await expect(taskCard).toBeVisible(); + } else { + await expect(taskCard).toBeHidden(); + } + } + + /** + * Verify the status of a task + */ + async verifyTaskStatus(taskDescription: string, status: string) { + const taskCard = this.getTaskCardByDescription(taskDescription); + + // Find the header element inside the task card that contains the status + const statusHeader = taskCard.locator('div[class*="taskTabHeader"]').last(); + await expect(statusHeader).toContainText(`STATUS: ${status}`); + } + + /** + * Helper to get a task card based on its description + * This uses structure and content rather than specific class names + */ + protected getTaskCardByDescription(taskDescription: string): Locator { + // Find the text content first + const textElement = this.page + .getByText(`Complete ${taskDescription}`) + .first(); + + // Then navigate up to the card container (usually 2 levels up) + // This is more reliable than using class names + return textElement.locator('xpath=./ancestor::div[contains(@class, "MuiGrid-container")][1]'); + } +} \ No newline at end of file diff --git a/tests/util/fillOutForm.ts b/tests/util/fillOutForm.ts index 7de5be7..3876035 100644 --- a/tests/util/fillOutForm.ts +++ b/tests/util/fillOutForm.ts @@ -1,31 +1,122 @@ import { type Page, type Locator, expect } from "@playwright/test"; -/** Fills out all blank text, numeric, and date fields found on the given page, then clicks the submit button. */ +/** Fills out all visible input fields on a Lforms-based page. + * This function handles checkboxes, numeric fields, date fields, dropdowns, and text fields. + * It then clicks the submit button. + */ export async function testUtilFillOutForm(props: { number?: string; text?: string; date?: string; page: Page; - submitButton: Locator; }) { - const { page, submitButton, number = "1", text = "TEST", date = "12/31/2000" } = props; - const emptyNumericFields = page.locator("input:visible").getByPlaceholder("Type a number").getByText("").all(); - for (const emptyField of await emptyNumericFields) { - await emptyField.fill(number); + const { + page, + number = "25", + text = "TEST", + date = "10/31/2024", + } = props; + + // scroll to the bottom of the page, load entire form + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + await page.waitForTimeout(1000); // Small buffer + + + // If some checkboxes must remain in a given state (e.g. "Only Show Unfilled Fields" should be unchecked) + await page.locator('label:has-text("Only Show Unfilled Fields")') + .locator('..') + .locator('input[type="checkbox"]') + .first().uncheck(); + + let checkboxLocators = await page.locator('input[type="checkbox"]').all(); + + for (let i = 0; i < checkboxLocators.length; i++) { + const field = checkboxLocators[i] + const checkboxId = await field.getAttribute('id'); + + if (checkboxId && (checkboxId.startsWith('filterCheckbox') || checkboxId.startsWith('required-fields-checkbox'))) { + continue; // Skip these checkboxes + } + + await field.check(); + + await page.waitForTimeout(500); + + // handle new checkboxes that appear + const newCheckboxes = await page.locator('input[type="checkbox"]').all(); + if (newCheckboxes.length > checkboxLocators.length) { + // Update our array with the new full set + checkboxLocators = newCheckboxes; + } } - expect(await emptyNumericFields, `Unable to fill all numeric fields with ${number}`).toHaveLength(0); - const emptyTextFields = page.locator("input:visible").getByPlaceholder("Type a number").getByText("").all(); - for (const emptyField of await emptyTextFields) { - await emptyField.fill(text); + await page.getByText('Form Loaded:').click(); + + + // --- Fill numeric fields --- + const numericFields = await page.locator('input[placeholder="Type a number"]').all(); + for (const field of numericFields) { + const value = await field.inputValue(); // Get current value + if (!value) { // Check if empty + await field.fill(number); + } } - expect(await emptyTextFields).toHaveLength(0); + await page.getByText('Form Loaded:').click(); - const emptyDateFields = page.locator("input:visible").getByPlaceholder("MM/DD/YYYY").getByText("").all(); - for (const emptyField of await emptyDateFields) { - await emptyField.fill(date); + // --- Fill date fields --- + // Matches both native date fields and text fields with MM/DD/YYYY placeholder. + const dateFields = await page.locator('input[type="date"]:visible, input[placeholder*="MM/DD/YYYY"]').all(); + for (const field of dateFields) { + const value = await field.inputValue(); // Get current value + if (!value) { // Check if empty + await field.fill(date); + } } - expect(await emptyDateFields).toHaveLength(0); - - await submitButton.click(); + await page.getByText('Form Loaded:').click(); + + // --- Handle dropdowns (autocomplete/CWE style) --- + const dropdowns = await page.locator('input[placeholder^="Select one"]').all(); + for (const dropdown of dropdowns) { + const value = await dropdown.inputValue(); + if (!value) { // Check if empty + await dropdown.click() + + // Wait for the dropdown to appear + await page.waitForSelector('#completionOptions ul li'); + + // Select the first option from the list + const firstOption = await page.locator('#completionOptions ul li').first(); + await firstOption.click(); // Click the first option + + await dropdown.press('Escape'); + } + } + await page.getByText('Form Loaded:').click(); + + // --- Fill text fields --- + const textFields = await page.locator('input[placeholder="Type a value"]').all(); + + for (const field of textFields) { + + const fieldName = await field.getAttribute('name'); + + // Skip signature fields explicitly + if (fieldName && fieldName.toLowerCase().includes('signature')) { + continue; // Skip signature fields + } + + const currentValue = await field.inputValue(); + if (!currentValue) { + await field.fill(text); + } + } + await page.getByText('Form Loaded:').click(); + + await page.locator('label:has-text("Only Show Unfilled Fields")') + .locator('..') + .locator('input[type="checkbox"]') + .first().check(); + + await page.getByText('Form Loaded:').click(); } diff --git a/tests/util/interfaces.ts b/tests/util/interfaces.ts new file mode 100644 index 0000000..a94a7cd --- /dev/null +++ b/tests/util/interfaces.ts @@ -0,0 +1,27 @@ +export interface DrugConfig { + name: string; // Display name for the drug + searchTerm: string; // Term to use in row header search + forms: FormConfig[]; // Forms required for this drug +} + +export interface FormConfig { + name: string; // Display name of the form + buttonText?: string; // Text on the button to launch the form + taskDescription?: string; // Description shown in task list + requiresPatientSignature?: boolean; // Whether patient must sign + requiresPrescriberSignature?: boolean; // Whether prescriber must sign +} + +export interface FormConfig { + name: string; // Display name of the form + buttonText?: string; // Text on the button to launch the form + taskDescription?: string; // Description shown in task list + requiresPatientSignature?: boolean; // Whether patient must sign + requiresPrescriberSignature?: boolean; // Whether prescriber must sign +} + +export interface WorkflowOptions { + useTaskWorkflow?: boolean; + skipLogin?: boolean; + resetDatabases?: boolean; +} \ No newline at end of file diff --git a/tests/util/nurseEhr.ts b/tests/util/nurseEhr.ts new file mode 100644 index 0000000..f79bb92 --- /dev/null +++ b/tests/util/nurseEhr.ts @@ -0,0 +1,24 @@ +/** + * NurseEhrPage class specifically for the nurse view of the EHR application + * Extends the base EhrPage with nurse-specific selectors and methods + */ + +import { Page, expect, Locator } from "@playwright/test"; +import { EhrPage } from "./ehr"; + +export class NurseEhrPage extends EhrPage { + constructor(page: Page) { + super(page); + } + + /** + * Override selectPatient method for nurse-specific UI + */ + async selectPatient(patientName: string) { + await this.page.getByRole('button', { name: 'Select a patient' }).first().click(); + await this.page.getByLabel('Search').click(); + await this.page.getByRole('combobox', { name: 'Search' }).fill(patientName); + await this.page.getByRole('button', { name: 'Select Patient' }).click(); + + } +} \ No newline at end of file diff --git a/tests/util/paitentPortal.ts b/tests/util/paitentPortal.ts new file mode 100644 index 0000000..79b06f7 --- /dev/null +++ b/tests/util/paitentPortal.ts @@ -0,0 +1,54 @@ +import { type Page, type Locator, expect } from "@playwright/test"; +import { EhrPage } from "./ehr"; + + +/** + * PatientPortalPage: Represents interactions with the Patient Portal + */ +export class PatientPortalPage extends EhrPage { + constructor(page: Page) { + super(page); + } + + /** + * Override getTaskCardByDescription to use patient-specific routes + */ + async navigate() { + await this.page.goto('http://localhost:3000/patient-portal'); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Override getTaskCardByDescription to use patient-specific class names + */ + async login(username: string, password: string) { + await this.page.getByLabel('Username').click(); + await this.page.getByLabel('Username').fill(username); + await this.page.getByLabel('Password').click(); + await this.page.getByLabel('Password').fill(password); + await this.page.getByRole('button', { name: 'Log In' }).click(); + } + + /** + * Override getTaskCardByDescription to use patient-specific class names + */ + async goToTasks() { + await this.page.getByRole('button', { name: 'Tasks' }).click(); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Override getTaskCardByDescription to use patient-specific class names + */ + async refreshTasks() { + await this.goToMedications() + await this.goToTasks() + } + + // go to the medications page + async goToMedications() { + await this.page.getByRole('button', { name: 'Medications' }).click(); + } + + +} \ No newline at end of file diff --git a/tests/util/pharmacy.ts b/tests/util/pharmacy.ts new file mode 100644 index 0000000..10dbe5b --- /dev/null +++ b/tests/util/pharmacy.ts @@ -0,0 +1,76 @@ +import { Page, expect, Locator } from "@playwright/test"; + +export class PharmacyPage { + constructor(private page: Page) { } + + /** + * Navigate to the pharmacy application + */ + async navigate() { + await this.page.goto("http://localhost:5050/"); + await this.page.waitForLoadState("networkidle"); + + // Verify pharmacy page loaded + await expect(this.page.getByRole("heading", { name: "Pharmacy" })).toBeVisible(); + } + + /** + * Navigate to the Doctor Orders section + */ + async navigateToDoctorOrders() { + await this.page.getByRole("button", { name: /doctor orders/i }).click(); + } + + /** + * Find a medication card by name + */ + async findMedicationCard(searchTerm: string) { + const medCard = this.page.locator(".MuiPaper-root", { hasText: searchTerm }).first(); + await expect(medCard).toBeVisible(); + return medCard; + } + + /** + * Verify ETASU requirements + * Returns the number of checkmarks found + */ + async verifyETASU() { + await this.page.getByRole('button', { name: 'VIEW ETASU' }).click(); + await expect(this.page.getByText('✅').first()).toBeVisible(); + const numChecks = await this.page.getByText('✅').count(); + + // Close the dialog + await this.page.getByRole('button', { name: 'Close' }).click(); + + return numChecks; + } + + /** + * Verify the current order + */ + async verifyOrder() { + await this.page.getByRole('button', { name: 'VERIFY ORDER' }).click(); + } + + /** + * Switch to a different tab in the pharmacy UI + */ + async switchTab(tabName: string) { + await this.page.getByRole('tab', { name: tabName }).click(); + } + + /** + * Mark the current order as picked up + */ + async markAsPickedUp() { + await this.page.getByRole('button', { name: 'Mark as Picked Up' }).click(); + } + + /** + * Verify the status of a medication + */ + async verifyMedicationStatus(searchTerm: string, status: string) { + const medCard = await this.findMedicationCard(searchTerm); + await expect(medCard.getByText(status)).toBeVisible(); + } +} diff --git a/tests/util/smartApp.ts b/tests/util/smartApp.ts new file mode 100644 index 0000000..2386737 --- /dev/null +++ b/tests/util/smartApp.ts @@ -0,0 +1,136 @@ +import { Page, expect, Locator } from "@playwright/test"; +import { testUtilFillOutForm } from "./fillOutForm"; + + +/** + * SmartAppPage: Represents interactions with a SMART on FHIR app + */ +export class SmartAppPage { + constructor(private page: Page) { } + + /** + * Verify that the SMART on FHIR app loaded correctly + */ + async verifyAppLoaded() { + await this.page.waitForTimeout(1000); // Small buffer + + await expect(this.page).toHaveTitle("REMS SMART on FHIR app"); + } + + /** + * Handle any in-progress forms dialog that might appear + */ + async handleInProgressForms() { + // Check if dialog is present + const dialog = await this.page.waitForSelector('div[role="dialog"]'); + + // Click the second to last button (usually the most recent form) + await this.page.locator('div[role="dialog"] .MuiButtonBase-root').nth(-2).click(); + + } + + /** + * Fill out a form and submit it or save it + */ + async fillOutForm() { + // Use the utility to fill out the rest of the form + await testUtilFillOutForm({ page: this.page }); + } + + async submitForm(submitButtonText: string = "Submit REMS Bundle") { + // locate the submit button + const submitButton = this.page.getByRole("button", { name: submitButtonText }); + // --- Click the submit button --- + await submitButton.click(); + // close the dialog if present + // Check if the dialog is visible, with a short timeout + const isDialogVisible = await this.page.getByRole('button', { name: 'OK' }) + .isVisible({ timeout: 500 }) + .catch(() => false); // If any error occurs (like timeout), return false + + // Only click if it's visible + if (isDialogVisible) { + await this.page.getByRole('button', { name: 'OK' }).click(); + + } + } + + /** + * Fill in any prescriber signature fields found on the form + */ + async fillPrescriberSignatureFields(name: string) { + // find the prescriber signature field + const prescriberSignatureSection = this.page.locator('div.lf-form-horizontal-table-title:has-text("Provider Signature")'); + const doctorSignatureSection = this.page.locator('div.lf-de-label-button:has-text("Doctor Signature")'); + + + // Check if either is visible + const isDoctorVisible = await doctorSignatureSection.isVisible(); + + // Use the one that's visible + const prescriberSectionToUse = isDoctorVisible ? doctorSignatureSection : prescriberSignatureSection + + // Get the parent container + const container = prescriberSectionToUse.locator('..'); + + // Find the signature input within this container (first input in the table) + const signatureInput = container.locator('input[name*="signature" i]'); + await signatureInput.fill(name); + + await this.page.getByText('Form Loaded:').click(); + } + + /** +* Fill in any patient signature fields found on the form +*/ + async fillPatientSignatureFields(name: string) { + // find the prescriber signature field + const patientSignatureSection = this.page.locator('div.lf-form-horizontal-table-title:has-text("Patient Signature")'); + + const patientSignatureAltSection = this.page.locator('div.lf-de-label-button:has-text("Patient Signature")'); + + + // Check if either is visible + const isPatientAltVisible = await patientSignatureAltSection.isVisible(); + + // Use the one that's visible + const patientSectionToUse = isPatientAltVisible ? patientSignatureAltSection : patientSignatureSection; + + // Get the parent container + const container = patientSectionToUse.locator('..'); + + // Find the signature input within this container (first input in the table) + const signatureInput = container.locator('input[name*="signature" i]'); + await signatureInput.fill(name); + + + await this.page.getByText('Form Loaded:').click(); + } + + /** + * Check ETASU status + * Returns counts of checkmarks and close icons + */ + async checkEtasuStatus() { + await this.page.getByRole("button", { name: /ETASU:/i }).click(); + await this.page.waitForLoadState("networkidle"); + + const remsPopup = this.page.locator(".MuiBox-root", { hasText: "REMS Status" }); + await expect(remsPopup.getByText("Patient Enrollment")).toBeVisible(); + + const checksCount = await remsPopup.getByTestId("CheckCircleIcon").count(); + const closeCount = await remsPopup.getByTestId("CloseIcon").count(); + + // Dismiss popup + await remsPopup.press("Escape"); + + return { checksCount, closeCount }; + } + + /** + * Navigate to a specific tab in the SMART app + */ + async navigateToTab(tabName: string) { + await this.page.getByRole('tab', { name: new RegExp(tabName, 'i') }).click(); + } +} \ No newline at end of file diff --git a/tests/util/test-fixture.ts b/tests/util/test-fixture.ts new file mode 100644 index 0000000..845d00d --- /dev/null +++ b/tests/util/test-fixture.ts @@ -0,0 +1,74 @@ + +import { test as base, expect } from '@playwright/test'; +import { EhrPage } from './ehr'; +import { PharmacyPage } from './pharmacy'; +import { SmartAppPage } from './smartApp'; +import { PatientPortalPage } from './paitentPortal'; +import { NurseEhrPage } from './nurseEhr'; +import { TEST_USERS, TEST_PATIENTS } from './configs'; + +// Define the fixture types +type WorkflowFixtures = { + ehrPage: EhrPage; + pharmacyPage: PharmacyPage; + nurseEhrPage: NurseEhrPage; + patientPortalPage: PatientPortalPage; + setupEnvironment: void; +}; + +/** + * Create properly typed test fixture extension + */ +export const test = base.extend({ + /** + * Sets up an EhrPage object for the test + */ + ehrPage: async ({ page }, use) => { + const ehrPage = new EhrPage(page); + await use(ehrPage); + }, + + /** + * Creates a pharmacy page in a new tab + */ + pharmacyPage: async ({ context }, use) => { + const pharmacyPage = await context.newPage(); + const pharmacy = new PharmacyPage(pharmacyPage); + await use(pharmacy); + }, + + /** + * Sets up a nurse context with logged-in EHR using NurseEhrPage + */ + nurseEhrPage: async ({ browser }, use) => { + // Create new context and page for nurse + const nurseContext = await browser.newContext(); + const nursePage = await nurseContext.newPage(); + + // Create NurseEhrPage instead of EhrPage + const nurseEhr = new NurseEhrPage(nursePage); + + await use(nurseEhr); + + // Clean up + await nurseContext.close(); + }, + + /** + * Sets up a patient portal with login + */ + patientPortalPage: async ({ browser }, use) => { + // Create new context and page for patient + const patientContext = await browser.newContext(); + const patientPageObj = await patientContext.newPage(); + + // Create and set up patient portal + const patientPortal = new PatientPortalPage(patientPageObj); + + await use(patientPortal); + + // Clean up + await patientContext.close(); + } +}); +