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
-
Install Node.js, which is required for running JavaScript- and TypeScript-based tests.
-
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 fromfoundation-testing
package.- The
loginPage
fixture is defined, which creates an instance ofLoginPage
for each test. - The
use
function is called to provide theloginPage
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 thegenx
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();
});