-
Notifications
You must be signed in to change notification settings - Fork 16
E2e testing #1290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
david-roper
wants to merge
32
commits into
DouglasNeuroInformatics:main
Choose a base branch
from
david-roper:e2e-testing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+329
−6
Open
E2e testing #1290
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
adb9542
test: add datahub page to testing
david-roper 39b9d24
test: add test for datahub page
david-roper 49ab018
test: add start session test
david-roper 3a5b44f
test: remove rowActionsTrigger
david-roper 1632515
test: additions to start session form tests
david-roper a4bf5b0
test: add content check for session form
david-roper 7055449
test: sample docker config
david-roper c4e8411
test: add test ids to disclaimer, test local storage in session test
david-roper e3d4444
test: add a accept disclaimer test
david-roper b3312bf
fix: add a wait to make sure test does go without loading auth file
david-roper 5c2b5d6
test: fix start session test to navigate to personal info form
david-roper 301f790
add test to fill in lastname and dob to session form
david-roper 3ce8965
test: add subject DOB test
david-roper da2ec7f
add test for retrospective session
david-roper 9acbe04
comment out current submit button code
david-roper fdfefba
test: adjust submit button test
david-roper e2cb443
test: adkist session submission code
david-roper 6c4a8de
test: adjust timeout in playwright config
david-roper 0ee1258
test: add fix to start session test, ignore tutorial/disclaimer
david-roper 34481b1
chore: adjust config for e2e tests
david-roper d9cf0c8
test: add test for custom identifier
david-roper fe7ece6
chore: linting fixes
david-roper 39ba2df
chore: go back to 10 sec timeout
david-roper c6a65d0
Update testing/e2e/playwright.docker.config.ts
david-roper fa3c963
chore: fix name of datahub test
david-roper 731f98f
chore: move custom indentifier test to start session test suite
david-roper f880a93
test: add a dynamic date to test dates
david-roper 8750b32
Update testing/e2e/src/helpers/fixtures.ts
david-roper 3360944
fix: remove force submit
david-roper 6eeb967
chore: move global vars to top of file, specify wait time in error msg
david-roper 42aeee0
chore: remove redundant comments
david-roper 844cd8e
chore: remove redundant comments
david-roper File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import * as crypto from 'node:crypto'; | ||
| import * as path from 'node:path'; | ||
|
|
||
| import { parseNumber, range, unwrap } from '@douglasneuroinformatics/libjs'; | ||
| import { defineConfig, devices } from '@playwright/test'; | ||
| import type { Project } from '@playwright/test'; | ||
|
|
||
| import { AUTH_STORAGE_DIR } from './src/helpers/constants'; | ||
|
|
||
| import type { BrowserTarget, ProjectMetadata } from './src/helpers/types'; | ||
|
|
||
| const appPort = parseNumber(process.env.APP_PORT); | ||
| const gatewayPort = parseNumber(process.env.GATEWAY_PORT); | ||
|
|
||
| if (Number.isNaN(appPort)) { | ||
| throw new Error(`Expected APP_PORT to be number, got ${process.env.APP_PORT}`); | ||
| } else if (Number.isNaN(gatewayPort)) { | ||
| throw new Error(`Expected GATEWAY_PORT to be number, got ${process.env.GATEWAY_PORT}`); | ||
| } | ||
|
|
||
| const baseURL = `http://localhost:${appPort}`; | ||
|
|
||
| const browsers: { target: BrowserTarget; use: Project['use'] }[] = [ | ||
| { target: 'Desktop Chrome', use: { ...devices['Desktop Chrome'], channel: 'chromium', headless: true } }, | ||
| { target: 'Desktop Firefox', use: { ...devices['Desktop Firefox'], headless: true } } | ||
| ] as const; | ||
|
|
||
| export default defineConfig({ | ||
| globalSetup: path.resolve(import.meta.dirname, 'src/global/global.setup.ts'), | ||
| globalTeardown: path.resolve(import.meta.dirname, 'src/global/global.teardown.ts'), | ||
| maxFailures: 1, | ||
| outputDir: path.resolve(import.meta.dirname, '.playwright/output'), | ||
| projects: [ | ||
| { | ||
| name: 'Global Setup', | ||
| teardown: 'Global Teardown', | ||
| testMatch: '**/global/global.setup.spec.ts', | ||
| use: { | ||
| baseURL | ||
| } | ||
| }, | ||
| { | ||
| name: 'Global Teardown', | ||
| testMatch: '**/global/global.teardown.spec.ts', | ||
| use: { | ||
| baseURL | ||
| } | ||
| }, | ||
| ...unwrap(range(1, 4)).flatMap((i) => { | ||
| return browsers.map((browser) => { | ||
| const browserId = crypto.createHash('sha256').update(browser.target).digest('hex'); | ||
| return { | ||
| dependencies: i === 1 ? ['Global Setup'] : [`${i - 1}.x - ${browser.target}`], | ||
| metadata: { | ||
| authStorageFile: path.resolve(AUTH_STORAGE_DIR, `${browserId}.json`), | ||
| browserId, | ||
| browserTarget: browser.target | ||
| } satisfies ProjectMetadata, | ||
| name: `${i}.x - ${browser.target}`, | ||
| testMatch: `**/${i}.*.spec.ts`, | ||
| use: { | ||
| ...browser.use, | ||
| baseURL | ||
| } | ||
| }; | ||
| }); | ||
| }) | ||
| ], | ||
| reporter: [['html', { open: 'never', outputFolder: path.resolve(import.meta.dirname, '.playwright/report') }]], | ||
| testDir: path.resolve(import.meta.dirname, 'src'), | ||
| webServer: [ | ||
| { | ||
| command: 'true', // Dummy command since services are assumed running in Docker | ||
| reuseExistingServer: true, | ||
| timeout: 10_000, | ||
| url: `http://localhost:${appPort}/api/v1/setup` | ||
| }, | ||
| { | ||
| command: 'true', // Dummy command since services are assumed running in Docker | ||
| reuseExistingServer: true, | ||
| timeout: 10_000, | ||
| url: `http://localhost:${gatewayPort}/api/healthcheck` | ||
| }, | ||
| { | ||
| command: 'true', // Dummy command since services are assumed running in Docker | ||
| reuseExistingServer: true, | ||
| timeout: 10_000, | ||
| url: `http://localhost:${appPort}` | ||
| } | ||
| ], | ||
| workers: process.env.CI ? 1 : undefined | ||
| }); | ||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { expect, test } from './helpers/fixtures'; | ||
|
|
||
| test.describe('disclaimer', () => { | ||
| test('should accept the disclaimer', async ({ getProjectAuth, page }) => { | ||
| // Get the auth token | ||
| const auth = await getProjectAuth(); | ||
|
|
||
| // Set localStorage to ensure disclaimer appears and set auth | ||
| await page.addInitScript((accessToken) => { | ||
| window.__PLAYWRIGHT_ACCESS_TOKEN__ = accessToken; | ||
| // Set the app localStorage item to ensure disclaimer is not accepted | ||
| localStorage.setItem('app', JSON.stringify({ state: { isDisclaimerAccepted: false }, version: 1 })); | ||
| }, auth.accessToken); | ||
|
|
||
| await page.goto('/dashboard'); | ||
|
|
||
| const disclaimerDialog = page.getByRole('dialog', { name: 'Disclaimer' }); | ||
| await expect(disclaimerDialog).toBeVisible(); | ||
|
|
||
| // Click the accept disclaimer button | ||
| const acceptButton = page.getByRole('button', { name: 'Accept' }); | ||
| await expect(acceptButton).toBeVisible(); | ||
| await acceptButton.click(); | ||
|
|
||
| await expect(disclaimerDialog).not.toBeVisible(); | ||
|
|
||
| const pageHeader = page.getByTestId('page-header'); | ||
| await expect(pageHeader).toBeVisible(); | ||
| await expect(pageHeader).toContainText('Dashboard'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { expect, test } from './helpers/fixtures'; | ||
|
|
||
| test.describe('dashhub', () => { | ||
| test('should display the dashhub header', async ({ getPageModel }) => { | ||
| const datahubPage = await getPageModel('/datahub'); | ||
| await expect(datahubPage.pageHeader).toBeVisible(); | ||
| await expect(datahubPage.pageHeader).toContainText('Data Hub'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { expect, test } from './helpers/fixtures'; | ||
|
|
||
| test.describe('start session', () => { | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| test('should display the start session form header', async ({ getPageModel }) => { | ||
| const startSessionPage = await getPageModel('/session/start-session'); | ||
| await expect(startSessionPage.pageHeader).toBeVisible(); | ||
| await expect(startSessionPage.pageHeader).toContainText('Start Session'); | ||
| await expect(startSessionPage.sessionForm).toBeVisible(); | ||
| }); | ||
|
|
||
| test('should fill subject personal information input', async ({ getPageModel, page }) => { | ||
| await page.addInitScript(() => { | ||
| localStorage.setItem( | ||
| 'app', | ||
| JSON.stringify({ state: { isDisclaimerAccepted: true, isWalkthroughComplete: true }, version: 1 }) | ||
| ); | ||
| }); | ||
|
|
||
| const startSessionPage = await getPageModel('/session/start-session'); | ||
|
|
||
| await startSessionPage.sessionForm.waitFor({ state: 'visible' }); | ||
| await startSessionPage.selectIdentificationMethod('PERSONAL_INFO'); | ||
|
|
||
| await expect(startSessionPage.selectField).toHaveValue('PERSONAL_INFO'); | ||
|
|
||
| await startSessionPage.fillSessionForm('firstNameTest', 'lastNameTest', 'Male'); | ||
|
|
||
| const firstNameField = startSessionPage.sessionForm.locator('[name="subjectFirstName"]'); | ||
| await expect(firstNameField).toHaveValue('firstNameTest'); | ||
|
|
||
| const lastNameField = startSessionPage.sessionForm.locator('[name="subjectLastName"]'); | ||
| await expect(lastNameField).toHaveValue('lastNameTest'); | ||
|
|
||
| const dobField = startSessionPage.sessionForm.locator('[name="subjectDateOfBirth"]'); | ||
| await expect(dobField).toHaveValue('1990-01-01'); | ||
|
|
||
| const sexField = startSessionPage.sessionForm.locator('[name="subjectSex"]'); | ||
| await expect(sexField).toHaveValue('MALE'); | ||
|
|
||
| const sessionTypeSelector = startSessionPage.sessionForm.locator('[name="sessionType"]'); | ||
| await expect(sessionTypeSelector).toHaveValue('RETROSPECTIVE'); | ||
|
|
||
| const expectedSessionDate = new Date().toISOString().split('T')[0]!; | ||
| const sessionDate = startSessionPage.sessionForm.locator('[name="sessionDate"]'); | ||
| await expect(sessionDate).toHaveValue(expectedSessionDate); | ||
|
|
||
| await startSessionPage.submitForm(); | ||
|
|
||
| await expect(startSessionPage.successMessage).toBeVisible(); | ||
| }); | ||
|
|
||
| test('should fill custom identifier input', async ({ getPageModel, page }) => { | ||
| await page.addInitScript(() => { | ||
| localStorage.setItem( | ||
| 'app', | ||
| JSON.stringify({ state: { isDisclaimerAccepted: true, isWalkthroughComplete: true }, version: 1 }) | ||
| ); | ||
| }); | ||
|
|
||
| const startSessionPage = await getPageModel('/session/start-session'); | ||
|
|
||
| await startSessionPage.sessionForm.waitFor({ state: 'visible' }); | ||
| await startSessionPage.selectIdentificationMethod('CUSTOM_ID'); | ||
|
|
||
| await expect(startSessionPage.selectField).toHaveValue('CUSTOM_ID'); | ||
|
|
||
| await startSessionPage.fillCustomIdentifier('customIdentifierTest', 'Male'); | ||
|
|
||
| const subjectIdField = startSessionPage.sessionForm.locator('[name="subjectId"]'); | ||
| await expect(subjectIdField).toHaveValue('customIdentifierTest'); | ||
|
|
||
| const sessionTypeSelector = startSessionPage.sessionForm.locator('[name="sessionType"]'); | ||
| await expect(sessionTypeSelector).toHaveValue('RETROSPECTIVE'); | ||
|
|
||
| const sessionDate = startSessionPage.sessionForm.locator('[name="sessionDate"]'); | ||
| const expectedSessionDate = new Date().toISOString().split('T')[0]!; | ||
| await expect(sessionDate).toHaveValue(expectedSessionDate); | ||
|
|
||
| await startSessionPage.submitForm(); | ||
|
|
||
| await expect(startSessionPage.successMessage).toBeVisible(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { Locator, Page } from '@playwright/test'; | ||
|
|
||
| import { AppPage } from '../_app.page'; | ||
|
|
||
| export class DatahubPage extends AppPage { | ||
| readonly pageHeader: Locator; | ||
| readonly rowActionsTrigger: Locator; | ||
| constructor(page: Page) { | ||
| super(page); | ||
| this.pageHeader = page.getByTestId('page-header'); | ||
| this.rowActionsTrigger = page.getByTestId('row-actions-trigger').first(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import type { Locator, Page } from '@playwright/test'; | ||
|
|
||
| import { AppPage } from './_app.page'; | ||
|
|
||
| export class StartSessionPage extends AppPage { | ||
| readonly pageHeader: Locator; | ||
| readonly selectField: Locator; | ||
| readonly sessionForm: Locator; | ||
| readonly successMessage: Locator; | ||
|
|
||
| constructor(page: Page) { | ||
| super(page); | ||
| this.pageHeader = page.getByTestId('page-header'); | ||
| this.sessionForm = page.getByTestId('start-session-form'); | ||
| this.selectField = page.locator('[name="subjectIdentificationMethod"]'); | ||
| this.successMessage = page.getByRole('heading', { name: 'Session Successfully Started' }); | ||
| } | ||
|
|
||
| async fillCustomIdentifier(customIdentifier: string, sex: string) { | ||
| const subjectIdField = this.sessionForm.locator('[name="subjectId"]'); | ||
| const dateOfBirthField = this.sessionForm.locator('[name="subjectDateOfBirth"]'); | ||
| const sexSelector = this.sessionForm.locator('[name="subjectSex"]'); | ||
| const sessionTypeSelector = this.sessionForm.locator('[name="sessionType"]'); | ||
| const sessionDate = this.sessionForm.locator('[name="sessionDate"]'); | ||
|
|
||
| await subjectIdField.waitFor({ state: 'visible' }); | ||
| await subjectIdField.fill(customIdentifier); | ||
|
|
||
| await dateOfBirthField.waitFor({ state: 'visible' }); | ||
| await dateOfBirthField.fill('1990-01-01'); | ||
|
|
||
| await sexSelector.selectOption(sex); | ||
|
|
||
| await sessionTypeSelector.selectOption('Retrospective'); | ||
|
|
||
| await sessionDate.waitFor({ state: 'visible' }); | ||
| const expectedSessionDate = new Date().toISOString().split('T')[0]!; | ||
| await sessionDate.fill(expectedSessionDate); | ||
| } | ||
|
|
||
| async fillSessionForm(firstName: string, lastName: string, sex: string) { | ||
| const firstNameField = this.sessionForm.locator('[name="subjectFirstName"]'); | ||
| const lastNameField = this.sessionForm.locator('[name="subjectLastName"]'); | ||
| const dateOfBirthField = this.sessionForm.locator('[name="subjectDateOfBirth"]'); | ||
| const sexSelector = this.sessionForm.locator('[name="subjectSex"]'); | ||
| const sessionTypeSelector = this.sessionForm.locator('[name="sessionType"]'); | ||
| const sessionDate = this.sessionForm.locator('[name="sessionDate"]'); | ||
|
|
||
| await firstNameField.waitFor({ state: 'visible' }); | ||
| await firstNameField.fill(firstName); | ||
|
|
||
| await lastNameField.waitFor({ state: 'visible' }); | ||
| await lastNameField.fill(lastName); | ||
|
|
||
| await dateOfBirthField.waitFor({ state: 'visible' }); | ||
| await dateOfBirthField.fill('1990-01-01'); | ||
|
|
||
| await sexSelector.selectOption(sex); | ||
|
|
||
| await sessionTypeSelector.selectOption('Retrospective'); | ||
|
|
||
| await sessionDate.waitFor({ state: 'visible' }); | ||
| const expectedSessionDate = new Date().toISOString().split('T')[0]!; | ||
| await sessionDate.fill(expectedSessionDate); | ||
| } | ||
|
|
||
| async selectIdentificationMethod(methodName: string) { | ||
| await this.selectField.selectOption(methodName); | ||
| } | ||
|
|
||
| async submitForm() { | ||
| const submitButton = this.sessionForm.getByLabel('Submit'); | ||
|
|
||
| await submitButton.waitFor({ state: 'visible' }); | ||
|
|
||
| await submitButton.click(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.