From 83c7efbf1563d8f584992e9a6b396bde408ef7a1 Mon Sep 17 00:00:00 2001 From: Randila Premarathne Date: Sat, 18 Nov 2023 16:59:26 +0530 Subject: [PATCH] Setup the template repository with playwright --- .github/e2e.yml | 63 +++++++++++++ .gitignore | 6 ++ e2e/README.md | 115 +++++++++++++++++++++++ e2e/core/global-setup.ts | 32 +++++++ e2e/core/index.ts | 1 + e2e/core/test.ts | 20 ++++ e2e/fixtures/api.ts | 26 +++++ e2e/fixtures/index.ts | 1 + e2e/pages/home-page.ts | 9 ++ e2e/pages/index.ts | 1 + e2e/specs/initiate-test.spec.ts | 9 ++ e2e/support/github/Dockerfile | 34 +++++++ e2e/support/github/docker-compose.yml | 24 +++++ e2e/support/github/run-e2e-docker-env.sh | 49 ++++++++++ package.json | 5 +- playwright.config.ts | 32 +++++++ yarn.lock | 48 +++++++++- 17 files changed, 472 insertions(+), 3 deletions(-) create mode 100644 .github/e2e.yml create mode 100644 e2e/README.md create mode 100644 e2e/core/global-setup.ts create mode 100644 e2e/core/index.ts create mode 100644 e2e/core/test.ts create mode 100644 e2e/fixtures/api.ts create mode 100644 e2e/fixtures/index.ts create mode 100644 e2e/pages/home-page.ts create mode 100644 e2e/pages/index.ts create mode 100644 e2e/specs/initiate-test.spec.ts create mode 100644 e2e/support/github/Dockerfile create mode 100644 e2e/support/github/docker-compose.yml create mode 100644 e2e/support/github/run-e2e-docker-env.sh create mode 100644 playwright.config.ts diff --git a/.github/e2e.yml b/.github/e2e.yml new file mode 100644 index 0000000..62dddab --- /dev/null +++ b/.github/e2e.yml @@ -0,0 +1,63 @@ +name: E2E Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + main: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Copy test environment variables + run: cp example.env .env + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: "16.x" + + - name: Cache dependencies + id: cache + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: yarn install --immutable + + - name: Install Playwright Browsers + run: npx playwright install chromium --with-deps + + - 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@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index e17d7de..8e72bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,9 @@ dist/ # Turborepo .turbo + +# Playwright and e2e tests +/test-results/ +/playwright-report/ +/playwright/.cache/ +e2e/storageState.json diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..25c45ce --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,115 @@ +# E2E Tests + +This directory contains an E2E test suite using the [Playwright](https://playwright.dev) +framework. + +## Getting Started + +Please ensure that you have followed the basic installation guide in the +[root README](../README.md). +Once everything is set up, make sure the dev server is running by using: + +```sh +yarn start --sources 'packages/esm-*-app/' +``` +Then, in a separate terminal, run: + +```sh +yarn test-e2e --headed +``` + +By default, the test suite will run against the http://localhost:8080. +You can override this by exporting `E2E_BASE_URL` environment variables beforehand: + +```sh +# Ex: Set the server URL to dev3: +export E2E_BASE_URL=https://dev3.openmrs.org/openmrs + +# Run all e2e tests: +yarn test-e2e --headed +``` +To run a specific test by title: +```sh +yarn test-e2e --headed -g "title of the test" +``` +Check [this documentation](https://playwright.dev/docs/running-tests#command-line) for more running options. + +It is also highly recommended to install the companion VS Code extension: +https://playwright.dev/docs/getting-started-vscode + + +## Writing New Tests + +In general, it is recommended to read through the official [Playwright docs](https://playwright.dev/docs/intro) +before writing new test cases. The project uses the official Playwright test runner and, +generally, follows a very simple project structure: + +``` +e2e +|__ commands +| ^ Contains "commands" (simple reusable functions) that can be used in test cases/specs, +| e.g. generate a random patient. +|__ core +| ^ Contains code related to the test runner itself, e.g. setting up the custom fixtures. +| You probably need to touch this infrequently. +|__ fixtures +| ^ Contains fixtures (https://playwright.dev/docs/test-fixtures) which are used +| to run reusable setup/teardown tasks +|__ pages +| ^ Contains page object model classes for interacting with the frontend. +| See https://playwright.dev/docs/test-pom for details. +|__ specs +| ^ Contains the actual test cases/specs. New tests should be placed in this folder. +|__ support + ^ Contains support files that requires to run e2e tests, e.g. docker compose files. +``` + +When you want to write a new test case, start by creating a new spec in `./specs`. +Depending on what you want to achieve, you might want to create new fixtures and/or +page object models. To see examples, have a look at the existing code to see how these +different concepts play together. + +## Open reports from GitHub Actions / Bamboo + +To download the report from the GitHub action/Bamboo plan, follow these steps: + +1. Go to the artifact section of the action/plan and locate the report file. +2. Download the report file and unzip it using a tool of your choice. +3. Open the index.html file in a web browser to view the report. + +The report will show you a full summary of your tests, including information on which +tests passed, failed, were skipped, or were flaky. You can filter the report by browser +and explore the details of individual tests, including any errors or failures, video +recordings, and the steps involved in each test. Simply click on a test to view its details. + +## Debugging Tests + +Refer to [this documentation](https://playwright.dev/docs/debug) on how to debug a test. + +## Configuration + +This is very much underdeveloped/WIP. At the moment, there exists a (git-shared) `.env` +file which can be used for configuring certain test attributes. This is most likely +about to change in the future. Stay tuned for updates! + + +## Github Action integration +The e2e.yml workflow is made up of two jobs: one for running on pull requests (PRs) and +one for running on commits. + +1. When running on PRs, the workflow will start the dev server, use dev3.openmrs.org as the backend, +and run tests only on chromium. This is done in order to quickly provide feedback to the developer. +The tests are designed to generate their own data and clean up after themselves once they are finished. +This ensures that the tests will have minimum effect from changes made to dev3 by other developers. +In the future, we plan to use a docker container to run the tests in an isolated environment once we +figure out a way to spin up the container within a small amount of time. +2. When running on commits, the workflow will spin up a docker container and run the dev server against +it in order to provide a known and isolated environment. In addition, tests will be run on multiple +browsers (chromium, firefox, and WebKit) to ensure compatibility. + +## Troubleshooting tips + +On MacOS, you might run into the following error: +```browserType.launch: Executable doesn't exist at /Users//Library/Caches/ms-playwright/chromium-1015/chrome-mac/Chromium.app/Contents/MacOS/Chromium``` +In order to fix this, you can attempt to force the browser reinstallation by running: +```PLAYWRIGHT_BROWSERS_PATH=/Users/$USER/Library/Caches/ms-playwright npx playwright install``` diff --git a/e2e/core/global-setup.ts b/e2e/core/global-setup.ts new file mode 100644 index 0000000..4238f7b --- /dev/null +++ b/e2e/core/global-setup.ts @@ -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: { + 'Content-Type': 'application/json', + Authorization: `Basic ${token}`, + }, + }); + await requestContext.storageState({ path: 'e2e/storageState.json' }); + await requestContext.dispose(); +} + +export default globalSetup; diff --git a/e2e/core/index.ts b/e2e/core/index.ts new file mode 100644 index 0000000..607718c --- /dev/null +++ b/e2e/core/index.ts @@ -0,0 +1 @@ +export * from './test'; diff --git a/e2e/core/test.ts b/e2e/core/test.ts new file mode 100644 index 0000000..dd3e40b --- /dev/null +++ b/e2e/core/test.ts @@ -0,0 +1,20 @@ +import { APIRequestContext, 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({ + api: [api, { scope: 'worker' }], +}); diff --git a/e2e/fixtures/api.ts b/e2e/fixtures/api.ts new file mode 100644 index 0000000..6afd8d5 --- /dev/null +++ b/e2e/fixtures/api.ts @@ -0,0 +1,26 @@ +import { APIRequestContext, PlaywrightWorkerArgs, 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 = 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, + password: process.env.E2E_USER_ADMIN_PASSWORD, + }, + }); + + await use(ctx); +}; diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts new file mode 100644 index 0000000..b1c13e7 --- /dev/null +++ b/e2e/fixtures/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/e2e/pages/home-page.ts b/e2e/pages/home-page.ts new file mode 100644 index 0000000..94aa548 --- /dev/null +++ b/e2e/pages/home-page.ts @@ -0,0 +1,9 @@ +import { Page } from '@playwright/test'; + +export class HomePage { + constructor(readonly page: Page) {} + + async goto() { + await this.page.goto(`home`); + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts new file mode 100644 index 0000000..e74822d --- /dev/null +++ b/e2e/pages/index.ts @@ -0,0 +1 @@ +export * from './home-page'; \ No newline at end of file diff --git a/e2e/specs/initiate-test.spec.ts b/e2e/specs/initiate-test.spec.ts new file mode 100644 index 0000000..cd380a5 --- /dev/null +++ b/e2e/specs/initiate-test.spec.ts @@ -0,0 +1,9 @@ +import test from "@playwright/test"; +import { HomePage } from "../pages"; +import { expect } from "@playwright/test"; + +test("initiate test", async ({ page}) => { + const homePage = new HomePage(page); + await homePage.goto(); + await expect(homePage.page.getByRole('link', { name: 'Home' })).toBeVisible(); +}); \ No newline at end of file diff --git a/e2e/support/github/Dockerfile b/e2e/support/github/Dockerfile new file mode 100644 index 0000000..6823603 --- /dev/null +++ b/e2e/support/github/Dockerfile @@ -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"] diff --git a/e2e/support/github/docker-compose.yml b/e2e/support/github/docker-compose.yml new file mode 100644 index 0000000..6a82976 --- /dev/null +++ b/e2e/support/github/docker-compose.yml @@ -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 diff --git a/e2e/support/github/run-e2e-docker-env.sh b/e2e/support/github/run-e2e-docker-env.sh new file mode 100644 index 0000000..ffcce96 --- /dev/null +++ b/e2e/support/github/run-e2e-docker-env.sh @@ -0,0 +1,49 @@ +#!/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 a list of all the apps in this workspace +apps=$(yarn workspaces list --json | jq -r 'if ((.location == ".") or (.location | test("-app") | not)) then halt else .name end') +# this array will hold all of the packed app names +app_names=() + +echo "Creating packed archives of apps..." +# for each app +for app in $apps +do + # @openmrs/esm-whatever -> _openmrs_esm_whatever + app_name=$(echo "$app" | tr '[:punct:]' '_'); + # add to our array + app_names+=("$app_name.tgz"); + # run yarn pack for our app and add it to the working directory + yarn workspace "$app" pack -o "$working_dir/$app_name.tgz" >/dev/null; +done; +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 apps "$apps" \ + --arg app_names "$(echo ${app_names[@]})" \ + '{"@openmrs/esm-primary-navigation-app": "next", "@openmrs/esm-home-app": "next"} + ( + ($apps | split("\n")) as $apps | ($app_names | split(" ") | map("/app/" + .)) as $app_files + | [$apps, $app_files] + | transpose + | map({"key": .[0], "value": .[1]}) + | from_entries + )' | 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 diff --git a/package.json b/package.json index 54ebacc..3b8b073 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "verify": "turbo lint typescript coverage", "coverage": "yarn test --coverage", "prepare": "husky install", - "extract-translations": "i18next 'src/**/*.component.tsx' --config ./i18next-parser.config.js" + "extract-translations": "i18next 'src/**/*.component.tsx' --config ./i18next-parser.config.js", + "test-e2e": "playwright test" }, "husky": { "hooks": { @@ -59,6 +60,7 @@ "devDependencies": { "@openmrs/esm-framework": "next", "@openmrs/esm-styleguide": "next", + "@playwright/test": "^1.30.0", "@swc/cli": "^0.1.62", "@swc/core": "^1.3.68", "@swc/jest": "^0.2.26", @@ -75,6 +77,7 @@ "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", "css-loader": "^6.8.1", + "dotenv": "^16.0.3", "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-config-ts-react-important-stuff": "^3.0.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d61b8cd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,32 @@ +import { devices, 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; diff --git a/yarn.lock b/yarn.lock index dacf0dc..7ea9523 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5317,6 +5317,7 @@ __metadata: "@carbon/react": ^1.33.1 "@openmrs/esm-framework": next "@openmrs/esm-styleguide": next + "@playwright/test": ^1.30.0 "@swc/cli": ^0.1.62 "@swc/core": ^1.3.68 "@swc/jest": ^0.2.26 @@ -5333,6 +5334,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.61.0 "@typescript-eslint/parser": ^5.61.0 css-loader: ^6.8.1 + dotenv: ^16.0.3 eslint: ^8.44.0 eslint-config-prettier: ^8.8.0 eslint-config-ts-react-important-stuff: ^3.0.0 @@ -5406,6 +5408,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.30.0": + version: 1.39.0 + resolution: "@playwright/test@npm:1.39.0" + dependencies: + playwright: 1.39.0 + bin: + playwright: cli.js + checksum: e93e58fc1af4239f239b890374f066c9a758e2492d25e2c1a532f3f00782ab8e7706956a07540fd14882c74e75f5de36273621adce9b79afb8e36e6c15f1d539 + languageName: node + linkType: hard + "@pnpm/config.env-replace@npm:^1.1.0": version: 1.1.0 resolution: "@pnpm/config.env-replace@npm:1.1.0" @@ -11107,6 +11120,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.0.3": + version: 16.3.1 + resolution: "dotenv@npm:16.3.1" + checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd + languageName: node + linkType: hard + "downshift@npm:5.2.1": version: 5.2.1 resolution: "downshift@npm:5.2.1" @@ -12784,7 +12804,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:2.3.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" dependencies: @@ -12794,7 +12814,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": +"fsevents@patch:fsevents@2.3.2#~builtin, fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" dependencies: @@ -18842,6 +18862,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.39.0": + version: 1.39.0 + resolution: "playwright-core@npm:1.39.0" + bin: + playwright-core: cli.js + checksum: 556e78dee4f9890facf2af8249972e0d6e01a5ae98737b0f6b0166c660a95ffee4cb79350335b1ef96430a0ef01d3669daae9099fa46c8d403d11c623988238b + languageName: node + linkType: hard + +"playwright@npm:1.39.0": + version: 1.39.0 + resolution: "playwright@npm:1.39.0" + dependencies: + fsevents: 2.3.2 + playwright-core: 1.39.0 + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 96d8ca5aa25465c1c5d554d0d6071981d55e22477800ff8f5d47a53ca75193d60ece2df538a01b7165b3277dd5493c67603a5acda713029df7fbd95ce2417bc9 + languageName: node + linkType: hard + "pngjs@npm:^3.0.0, pngjs@npm:^3.3.3": version: 3.4.0 resolution: "pngjs@npm:3.4.0"