Skip to content

Commit

Permalink
setup e2e testing
Browse files Browse the repository at this point in the history
  • Loading branch information
dilankavishka committed Feb 12, 2025
1 parent 865a157 commit 839332b
Show file tree
Hide file tree
Showing 18 changed files with 404 additions and 4 deletions.
102 changes: 102 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: E2E Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

env:
TURBO_API: 'http://127.0.0.1:9080'
TURBO_TOKEN: 'turbo-token'
TURBO_TEAM: ${{ github.repository_owner }}

jobs:
run_e2e_tests:
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout repo
uses: actions/checkout@v4

- name: 📝 Copy test environment variables
run: cp example.env .env

- name: 🛠️ Setup node
uses: actions/setup-node@v4
with:
node-version: 18

- name: 💾 Cache dependencies
id: cache-dependencies
uses: actions/cache@v4
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}

- name: 📦 Install dependencies
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: yarn install --immutable

- name: 🎭 Get installed Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(grep '@playwright/test@' yarn.lock | sed -n 's/.*npm:\([^":]*\).*/\1/p' | head -n 1)" >> $GITHUB_ENV

- name: 💾 Cache Playwright binaries
id: playwright-cache
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}

- name: 🎭 Install Playwright Browsers
run: npx playwright install chromium --with-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'

- name: 🚀 Setup local cache server for Turborepo
uses: felixmosh/turborepo-gh-artifacts@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
server-token: ${{ env.TURBO_TOKEN }}

- name: 🏗️ Build apps
run: yarn turbo run build --color --concurrency=5

- name: 🚀 Run dev server
run: bash e2e/support/github/run-e2e-docker-env.sh

- name: ⏳ Wait for OpenMRS instance to start
run: while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8080/openmrs/login.htm)" != "200" ]]; do sleep 10; done

- name: 🧪 Run E2E tests
run: yarn test-e2e

- name: 🛑 Stop dev server
if: '!cancelled()'
run: docker stop $(docker ps -a -q)

- name: 📤 Upload report
uses: actions/upload-artifact@v4
if: '!cancelled()'
with:
name: playwright-report
path: playwright-report/
retention-days: 30
overwrite: true

- name: 📝 Capture Server Logs
if: always()
uses: jwalton/gh-docker-logs@v2
with:
dest: './logs'

- name: 📤 Upload Logs as Artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: server-logs
path: './logs'
retention-days: 2
overwrite: true
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,9 @@ dist

# i18next parser creating moduleName folder when parsing index.ts
moduleName

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
e2e/storageState.json
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ By default, `turbo` will cache test runs. This means that re-running tests witho
yarn turbo run test --force
```

## To run end-to-end tests, run:

```bash
yarn test-e2e
```

Read the [e2e testing guide](https://openmrs.atlassian.net/wiki/spaces/docs/pages/150962731/Testing+Frontend+Modules+O3) to learn more about End-to-End tests in this project.

## Troubleshooting

If you notice that your local version of the application is not working or that there's a mismatch between what you see locally versus what's in [dev3](https://dev3.openmrs.org/openmrs/spa), you likely have outdated versions of core libraries. To update core libraries, run the following commands:
Expand Down
32 changes: 32 additions & 0 deletions e2e/core/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { request } from '@playwright/test';
import * as dotenv from 'dotenv';

dotenv.config();

/**
* This configuration is to reuse the signed-in state in the tests
* by log in only once using the API and then skip the log in step for all the tests.
*
* https://playwright.dev/docs/auth#reuse-signed-in-state
*/

async function globalSetup() {
const requestContext = await request.newContext();
const token = Buffer.from(`${process.env.E2E_USER_ADMIN_USERNAME}:${process.env.E2E_USER_ADMIN_PASSWORD}`).toString(
'base64',
);
await requestContext.post(`${process.env.E2E_BASE_URL}/ws/rest/v1/session`, {
data: {
sessionLocation: process.env.E2E_LOGIN_DEFAULT_LOCATION_UUID,
locale: 'en',
},
headers: {
Accept: 'application/json',
Authorization: `Basic ${token}`,
},
});
await requestContext.storageState({ path: 'e2e/storageState.json' });
await requestContext.dispose();
}

export default globalSetup;
1 change: 1 addition & 0 deletions e2e/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './test';
20 changes: 20 additions & 0 deletions e2e/core/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type APIRequestContext, type Page, test as base } from '@playwright/test';
import { api } from '../fixtures';

// This file sets up our custom test harness using the custom fixtures.
// See https://playwright.dev/docs/test-fixtures#creating-a-fixture for details.
// If a spec intends to use one of the custom fixtures, the special `test` function
// exported from this file must be used instead of the default `test` function
// provided by playwright.

export interface CustomTestFixtures {
loginAsAdmin: Page;
}

export interface CustomWorkerFixtures {
api: APIRequestContext;
}

export const test = base.extend<CustomTestFixtures, CustomWorkerFixtures>({
api: [api, { scope: 'worker' }],
});
26 changes: 26 additions & 0 deletions e2e/fixtures/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type APIRequestContext, type PlaywrightWorkerArgs, type WorkerFixture } from '@playwright/test';

/**
* A fixture which initializes an [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext)
* that is bound to the configured OpenMRS API server. The context is automatically authenticated
* using the configured admin account.
*
* Use the request context like this:
* ```ts
* test('your test', async ({ api }) => {
* const res = await api.get('patient/1234');
* await expect(res.ok()).toBeTruthy();
* });
* ```
*/
export const api: WorkerFixture<APIRequestContext, PlaywrightWorkerArgs> = async ({ playwright }, use) => {
const ctx = await playwright.request.newContext({
baseURL: `${process.env.E2E_BASE_URL}/ws/rest/v1/`,
httpCredentials: {
username: process.env.E2E_USER_ADMIN_USERNAME || 'admin',
password: process.env.E2E_USER_ADMIN_PASSWORD || 'Admin123',
},
});

await use(ctx);
};
1 change: 1 addition & 0 deletions e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './api';
1 change: 1 addition & 0 deletions e2e/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sample-test';
9 changes: 9 additions & 0 deletions e2e/pages/sample-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type Page } from '@playwright/test';

export class HomePage {
constructor(readonly page: Page) {}

async goto() {
await this.page.goto('home');
}
}
10 changes: 10 additions & 0 deletions e2e/specs/sample-test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test, expect } from '@playwright/test';
import { HomePage } from '../pages';

// This test is a sample E2E test. You can delete it.

test('sample-test', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
await expect(page.getByText('Clinic')).toBeVisible();
});
34 changes: 34 additions & 0 deletions e2e/support/github/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1.3
FROM --platform=$BUILDPLATFORM node:18-alpine as dev

ARG APP_SHELL_VERSION=next

RUN mkdir -p /app
WORKDIR /app

COPY . .

RUN npm_config_legacy_peer_deps=true npm install -g openmrs@${APP_SHELL_VERSION:-next}
ARG CACHE_BUST
RUN npm_config_legacy_peer_deps=true openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spa

FROM --platform=$BUILDPLATFORM openmrs/openmrs-reference-application-3-frontend:nightly as frontend
FROM nginx:1.23-alpine

RUN apk update && \
apk upgrade && \
# add more utils for sponge to support our startup script
apk add --no-cache moreutils

# clear any default files installed by nginx
RUN rm -rf /usr/share/nginx/html/*

COPY --from=frontend /etc/nginx/nginx.conf /etc/nginx/nginx.conf
# this assumes that NOTHING in the framework is in a subdirectory
COPY --from=frontend /usr/share/nginx/html/* /usr/share/nginx/html/
COPY --from=frontend /usr/local/bin/startup.sh /usr/local/bin/startup.sh
RUN chmod +x /usr/local/bin/startup.sh

COPY --from=dev /app/spa/ /usr/share/nginx/html/

CMD ["/usr/local/bin/startup.sh"]
24 changes: 24 additions & 0 deletions e2e/support/github/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This docker compose file is used to create a backend environment for the e2e.yml workflow.
version: '3.7'

services:
gateway:
image: openmrs/openmrs-reference-application-3-gateway:${TAG:-nightly}
ports:
- '8080:80'

frontend:
build:
context: .
environment:
SPA_PATH: /openmrs/spa
API_URL: /openmrs

backend:
image: openmrs/openmrs-reference-application-3-backend:nightly-with-data
depends_on:
- db

# MariaDB
db:
image: openmrs/openmrs-reference-application-3-db:nightly-with-data
37 changes: 37 additions & 0 deletions e2e/support/github/run-e2e-docker-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash -eu

# get the dir containing the script
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# create a temporary working directory
working_dir=$(mktemp -d "${TMPDIR:-/tmp/}openmrs-e2e-frontends.XXXXXXXXXX")
# get the app name
app_name=$(jq -r '.name' "$script_dir/../../../package.json")

echo "Creating packed archive of the app..."
# @openmrs/esm-whatever -> _openmrs_esm_whatever
packed_app_name=$(echo "$app_name" | tr '[:punct:]' '_');
# run yarn pack for our app and add it to the working directory
yarn pack -o "$working_dir/$packed_app_name.tgz" >/dev/null;
echo "Created packed app archives"

echo "Creating dynamic spa-assemble-config.json..."
# dynamically assemble our list of frontend modules, prepending the login app and
# primary navigation apps; apps will all be in the /app directory of the Docker
# container
jq -n \
--arg app_name "$app_name" \
--arg app_file "/app/$packed_app_name.tgz" \
'{"@openmrs/esm-primary-navigation-app": "next"} + {
($app_name): $app_file
}' | jq '{"frontendModules": .}' > "$working_dir/spa-assemble-config.json"
echo "Created dynamic spa-assemble-config.json"

echo "Copying Docker configuration..."
cp "$script_dir/Dockerfile" "$working_dir/Dockerfile"
cp "$script_dir/docker-compose.yml" "$working_dir/docker-compose.yml"

cd $working_dir
echo "Starting Docker containers..."
# CACHE_BUST to ensure the assemble step is always run
docker compose build --build-arg CACHE_BUST=$(date +%s) frontend
docker compose up -d
6 changes: 6 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This is an example environment file for configuring dynamic values.
E2E_BASE_URL=http://localhost:8080/openmrs
E2E_USER_ADMIN_USERNAME=admin
E2E_USER_ADMIN_PASSWORD=Admin123
E2E_LOGIN_DEFAULT_LOCATION_UUID=44c3efb0-2583-4c80-a79e-1f756a03c0a1
# The above location UUID is for the "Outpatient Clinic" location in the reference application
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
"release": "yarn workspaces foreach --all --topological version",
"postinstall": "husky install",
"start": "openmrs develop --sources 'packages/esm-*-app/'",
"verify": "turbo lint typescript test"
"verify": "turbo lint typescript test",
"test-e2e": "playwright test"
},
"devDependencies": {
"@carbon/react": "^1.71.0",
"@openmrs/esm-framework": "next",
"@playwright/test": "^1.50.1",
"@swc/cli": "^0.1.65",
"@swc/core": "^1.7.14",
"@swc/jest": "^0.2.36",
Expand Down Expand Up @@ -62,5 +64,9 @@
"*.{ts,tsx}": "eslint --cache --fix --max-warnings 0",
"*.{css,scss,ts,tsx}": "prettier --write --list-different"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"dotenv": "^16.4.7",
"playwright": "^1.50.1"
}
}
32 changes: 32 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { devices, type PlaywrightTestConfig } from '@playwright/test';
import * as dotenv from 'dotenv';
dotenv.config();

// See https://playwright.dev/docs/test-configuration.
const config: PlaywrightTestConfig = {
testDir: './e2e/specs',
timeout: 3 * 60 * 1000,
expect: {
timeout: 40 * 1000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
reporter: process.env.CI ? [['junit', { outputFile: 'results.xml' }], ['html']] : [['html']],
globalSetup: require.resolve('./e2e/core/global-setup'),
use: {
baseURL: `${process.env.E2E_BASE_URL}/spa/`,
storageState: 'e2e/storageState.json',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
};

export default config;
Loading

0 comments on commit 839332b

Please sign in to comment.