Skip to main content

How to add end-to-end tests

The foundation-testing package is a comprehensive framework for implementing End-to-End (E2E) testing. Use it to ensure that your Genesis Web Application provides a seamless user experience by testing workflows and user interactions across various scenarios.

Define test scenarios in the tests/e2e folder to cover all the critical user journeys in your application, and all the key functionality.

  • Use Playwright for browser automation.
  • Use Lighthouse for performance auditing within the same test suite.
  • Execute tests via your preferred test runner or CI pipeline.

Set up Playwright

Install and configure Playwright as part of your project dependencies. Follow the set-up guide provided by the foundation-testing package. You can then write and run tests using the Playwright API to simulate real-world user interactions. These include clicking buttons, filling in forms, and navigating from page to page.

Incorporate Lighthouse

Integrate Lighthouse into your E2E tests to run performance audits alongside functional tests. The insights from these reports enables you to identify and address performance issues, so that you can optimse your application for speed and user experience.

Setting up end-to-end tests

Installing dependencies

  1. Install Node.js, which is required for running JavaScript- and TypeScript-based tests.

  2. Install Foundation Testing. This provides utilities for testing within your project, and integrates Playwright, which has Behavior-Driven Development (BDD) tools, and enables you to use Gherkin syntax in tests.

npm install --save-dev @genesislcap/foundation-testing

A typical project structure

Ideally, you want your features files, steps files, and pages files organised in separate folders for clarity and easy access.

A typical project structure for E2E testing has three main folders, plus a test-config.ts file:

e2e/
├── features/
│ ├── example.feature
│ └── another-feature.feature
├── steps/
│ ├── fixtures.ts
│ ├── index.ts
│ └── specific-feature/
│ └── index.ts
├── pages/
│ ├── login-page.ts
│ └── home-page.ts
└── test-config.ts
  • features contains .feature files written in Gherkin syntax, describing the tests.
  • steps contains TypeScript files that define the steps corresponding to the Gherkin steps in the feature files.
  • pages contains Page Object Models (POMs) that encapsulate interactions with different parts of the application.
  • test-config.ts specifies the configuration for your testing environment.

Writing tests

Features

Feature files are written in Gherkin syntax, a business-readable, domain-specific language. Each feature file represents a function of the application, described in scenarios.

Example (example.feature):

Feature: Example Feature

Scenario: Example scenario
Given I am on the example page
When I perform an example action
Then I should see the expected result

Step definitions

Step definitions are implemented in TypeScript and correspond to the steps you have defined in your feature files. They are written using Playwright and the foundation-testing libraries. Functions are structured as arrow functions.

Example (steps/example-steps.ts):

import { expect } from '@genesislcap/foundation-testing/e2e';
import { Given, When, Then } from '../fixtures';

Given('I am on the example page', async ({ examplePage }) => {
await examplePage.goto();
});

When('I perform an example action', async ({ examplePage }) => {
await examplePage.performAction();
});

Then('I should see the expected result', async ({ examplePage }) => {
const result = await examplePage.getResult();
expect(result).toBe('Expected Result');
});

Working with fixtures

Configuring pages

In an E2E test set-up, you need an organised way of interacting with different parts of your application. This is typically done using Page Object Models (POMs). Each POM represents a page or a significant component of your application and encapsulates all interactions with it.

Here is an example of a page model for a login page (login-page.ts):

import { Locator, Page } from '@genesislcap/foundation-testing/e2e';

export class LoginPage {
readonly page: Page;
readonly username: Locator;
readonly password: Locator;
readonly submitButton: Locator;

constructor(page: Page) {
this.page = page;
this.username = page.locator('[data-test-id="username"]');
this.password = page.locator('[data-test-id="password"]');
this.submitButton = page.locator('[data-test-id="submit-login"]');
}

async goto() {
await this.page.goto('/login');
}

async fillUsernameAndPassword(username: string, password: string) {
await this.username.fill(username);
await this.password.fill(password);
}

async submit() {
await this.submitButton.click();
}
}

Using fixtures

Playwright uses fixtures to set up and maintain the context for your tests. They allow you to define common setup and teardown logic that can be reused across your tests.

In the provided codebase, fixtures are configured to provide a consistent testing environment, including setting up pages like LoginPage and configuring test settings.

Here is an example fixture set-up (fixtures.ts):

import { Locator, Page } from '@genesislcap/foundation-testing/e2e';
import { test as base } from '@genesislcap/foundation-testing/playwright-bdd';
import { LoginPage } from '../pages/login-page';

type Fixtures = {
loginPage: LoginPage;
};

export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
});

export const { Given, When, Then } = PlaywrightBDD.createBdd(test);

In this example:

  • base.extend is used to extend the default test environment provided by Playwright BDD export from foundation-testing package.
  • The loginPage fixture is defined, which creates an instance of LoginPage for each test.
  • The use function is called to provide the loginPage instance to the test.
  • The PlaywrightBDD.createBdd function is used to create the Gherkin steps (Given, When, Then) that use the fixtures.

Setting up environment variables

Proper environment configuration is crucial for ensuring that your end-to-end (E2E) tests run smoothly across different environments.

There are three different ways to set the environment variables that you access from within your fixture and page set-ups.

We shall look at each of these methods in detail.

Accessing environment variables in fixtures and pages

After setting up your environment variables through any of the above methods, you can easily access them in your fixtures or page models.

Here is an example of using environment variables in a fixture:

export const test = base.extend({
loginPage: async ({ page, config }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.fillUsernameAndPassword(
process.env.DEFAULT_USER || config.DEFAULT_USER,
process.env.DEFAULT_PASSWORD || config.DEFAULT_PASSWORD
);
await use(loginPage);
},
});

Using config in package.json

Your package.json includes a config section that can be used to define environment variables:

"config": {
"API_HOST": "ws://localhost:9064",
"PORT": 6060,
"ENABLE_SSO": false
}

These variables can be accessed in your TS/JS files as follows:

const apiHost = process.env.npm_package_config_API_HOST;
const port = process.env.npm_package_config_PORT;
const enableSSO = process.env.npm_package_config_ENABLE_SSO;

They can also be used in your test set-up, for example, within your fixture or page set-up:

export const test = base.extend({
config: async ({}, use) => {
const config = {
API_HOST: process.env.npm_package_config_API_HOST,
PORT: process.env.npm_package_config_PORT,
ENABLE_SSO: process.env.npm_package_config_ENABLE_SSO,
};
await use(config);
},
});

Using the .env.local file

Your .env.local file at the root of your project contains sensitive environment variables like DEFAULT_USER and DEFAULT_PASSWORD. These variables can be loaded using a package like dotenv.

genx automatically loads environment variables from .env.local (or .env) when running tests.

To access these variables in your test set-up, load the variables into your test set-up, for example:

export const test = base.extend({
config: async ({}, use) => {
const config = {
DEFAULT_USER: process.env.DEFAULT_USER,
DEFAULT_PASSWORD: process.env.DEFAULT_PASSWORD,
};
await use(config);
},
});

With this set-up, your test environment will have access to the variables defined in .env.local, allowing you to configure user credentials and other sensitive data.

Overriding environment variables from the command line

Sometimes, you need to override certain environment variables for specific test runs. You can do this directly via the command line:

genx test -e API_HOST=ws://localhost:9064,DEFAULT_USER=testuser --e2e

This approach temporarily sets API_HOST and DEFAULT_USER for that specific test run, overriding the values defined in package.json or .env.local.


E2E scripts in-depth

Below is a comprehensive guide on how to use these scripts to run your E2E tests effectively. Each script is summarised separately.

Example scripts

Here are the key scripts that you can add to your package.json for E2E testing:

"scripts": {
"test:e2e": "npx bddgen && genx test --e2e --watch",
"test:e2e:report": "npx playwright show-report",
"test:e2e:debug": "genx test --e2e --debug",
"test:e2e:ui": "genx test --e2e --interactive",
"test:e2e:watch:bdd": "npx -y nodemon -w ./test/e2e/features -w ./test/e2e/features/steps -e feature,ts --exec \"npx bddgen\"",
"test:e2e:watch:pw": "genx test --e2e --interactive",
"test:e2e:watch": "npx run-p test:e2e:watch:*"
}

Running E2E tests

To run all E2E tests in your project, use the following command:

npm run test:e2e
  • What it does. This script runs all the E2E tests while watching for any changes in the files. The npx bddgen command regenerates the BDD files before running the tests, ensuring everything is up-to-date. The tests are then executed using the genx tool with the --e2e flag.

Debugging E2E tests

If you need to debug your E2E tests, use the following command:

npm run test:e2e:debug
  • What it does. This script runs the E2E tests in debug mode. The genx test --e2e --debug command allows you to pause the execution and inspect the state of your application during the tests.

Running E2E tests with UI

For an interactive testing experience where you can manually interact with the tests and see them running, use:

npm run test:e2e:ui
  • What it does. This script runs the E2E tests in interactive mode, where you can see the tests as they are executed and you can control the flow manually. The genx test --e2e --interactive command opens an interface for this purpose.

Viewing E2E test reports

After running your E2E tests, you can view detailed reports:

npm run test:e2e:report
  • What it does. This script uses npx playwright show-report to open the Playwright report UI, where you can review the results of your tests in detail.

Watching E2E tests

If you want to watch and run your E2E tests whenever there are changes, use:

npm run test:e2e:watch
  • What it does. This script combines multiple watch commands to monitor changes in your E2E test files and automatically rerun the tests. It includes watching for changes in the feature files and step definitions and regenerating BDD files as needed.

Detailed examples

Login test

A common test scenario for login functionality might include both successful and unsuccessful login attempts.

Feature File:

Feature: Login

Scenario: Successful login
Given I am on the login page
When I enter valid credentials
Then I should be redirected to the home page

Scenario: Unsuccessful login
Given I am on the login page
When I enter invalid credentials
Then I should see an error message

Step Definitions:

import { expect } from '@genesislcap/foundation-testing/e2e';
import { Given, When, Then } from '../fixtures';

Given('I am on the login page', async ({ loginPage }) => {
await loginPage.goto();
});

When('I enter valid credentials', async ({ loginPage }) => {
await loginPage.fillUsernameAndPassword('user', 'password');
});

Then('I should be redirected to the home page', async ({ loginPage }) => {
await expect(loginPage.page).toHaveURL('/home');
});

Authenticated page test

This test ensures that authenticated users can access the home page.

Feature File:

Feature: Authenticated Home Page

Scenario: Accessing the home page
Given I am logged in
When I navigate to the home page
Then I should see the home page content

Step Definitions:

import { expect } from '@genesislcap/foundation-testing/e2e';
import { Given, When, Then } from '../fixtures';

Given('I am logged in', async ({ loginPage }) => {
await loginPage.login('user', 'password');
});

When('I navigate to the home page', async ({ homePage }) => {
await homePage.goto();
});

Then('I should see the home page content', async ({ homePage }) => {
await expect(homePage.isContentVisible()).toBeTruthy();
});