End-to-end testing framework for web dashboard, using Playwright and Cucumber.
- First of all, git clone e2e boilerplate:
git clone git@github.com:c0x12c/spartan-e2e-web-boilerplate.git
- Install dependencies
yarn install
- Install Playwright browser
npx playwright install
- The environment variables starting with
E2E_
in.env.example
are required for success execution.- On local, these environment variables should be placed inside file
.env
, e.g.
- On local, these environment variables should be placed inside file
E2E_BASE_URL=
E2E_SKIP_PROJECTS=
E2E_ONLY_PROJECTS=
E2E_TAGS=
Environment variables description
name | description |
---|---|
E2E_BASE_URL | URL to web dashboard |
E2E_SKIP_PROJECTS | Projects to skip (all available projects are in playwright.config.ts ). |
E2E_ONLY_PROJECTS | Projects to be executed (projects that is not included will not be executed). This option is valid only if E2E_SKIP_PROJECTS is not set. |
E2E_TAGS | Cucumber tag expression of scenarios to be executed on CI environment. |
Common commands are defined in package.json
Command explaination
To make use of cucumber BDD nature and Playwright test runner, test cases are defined in feature files (*.feature
), and before execution these feature files will be translated into Playwright spec files (in folder <project_root>/.features-gen
), then these spec files will be executed using Playwright runner.
Therefore the run command consists of 2 parts, i.e. generation of spec files and test execution, e.g.
// File package.json
{
// ...
"scripts" : {
// ...
"e2e" : "yarn bddgen && yarn playwright test",
}
}
Please contact authorized team members for file data/user.local.json
. It stores credentials to login to web dashboard.
Then use below commands for local execution:
- Run all tests
yarn e2e:local
- Run tests filtered by Cucumber tag expression: Specify tag expression in command, e.g.
E2E_TAGS="@Debug" yarn e2e:tag-local
- Run tests in UI mode
E2E_TAGS="@Debug" yarn e2e:ui
- Show last execution report
yarn e2e:report
- Run all tests
yarn e2e
- Run tests filtered by Cucumber tag expression: Modify tag expression in env file, e.g.
E2E_TAGS="@Feature:A or @Feature:B"
, then
yarn e2e:tag
- Bundle two files
src/features/common.step.ts
andsrc/pages/base.page.ts
which can be reusedyarn build
- The
dist
folder will created as:dist ├── page.d.mts ├── page.d.ts ├── page.js ├── page.mjs ├── step.d.mts ├── step.d.ts ├── step.js ├── step.mjs
- In the file
package.json
, export the entry as:{ "main": "./dist/page.js", "module": "./dist/page.mjs", "types": "./dist/page.d.ts", "files": ["dist"], "exports": { ".": "./dist/page.js", "./step": "./dist/step.js" }, }
src
├── common
│ ├── constant.ts
│ ├── enum.ts
│ ├── fixtures.ts
│ ├── types
│ │ ├── common.ts
│ │ ├── data.ts
│ │ ├── exception.ts
│ │ └── secrets.ts
│ └── utils.ts
├── features
│ ├── <group>
│ │ ├── <group>.data.ts
│ │ ├── <group>.feature
│ │ └── <group>.step.ts
│ ├── common.step.ts
├── pages
│ ├── base.page.ts
│ ├── <group>.page.ts
├── common.json
├── secrets.json
-
Scenarios are defined in feature file
*.feature
under folderfeatures/<group>
.<group>
is the group of features, or it can be the name of page where these scenarios will be applied.
-
The common steps should be used if applicable (see
features/common.step.ts
). If there is a new step, it should follow the naming convention:- It should be unique.
- Therefore, it should contain the context of step like the feature description or part of page that will be affected by that step.
- e.g.
I select random FROM date in UM Filter modal
UM
is the feature (User management)Filter modal
is where the action select FROM date occurs
- e.g.
-
The feature step (the step can be only executed in specified scenario) should be placed in
features/<group>/<group>.step.ts
. For example: Login Feature
- For non-confidential data, it should be placed inside feature file or in
common.json
and it is passed directly to step as paramters. Note that these data should also not be different in different environments, otherwise it should be treated as confidential data.
Scenario Outline: Sample scenario
Given I go to homepage
And I click sort by "<criteria>"
Then Items in homepage should be sorted by "<criteria>"
Examples:
| criteria |
| name |
| date |
- We have 2 types of external data: common and secrets test data. For common action like click button, input text. We need to define data for each feature in json like:
// File common.json
{
"address": {
"name": "E2E4",
"address": "0x8881bA8f386431661C90D89EEB87C15f0Ea85FbF"
}
}
- For confidential (secrets) data, it should be placed inside an external JSON file
secrets.json
, and then the data is loaded and used in step function of page (see below section). To get JSON file for runnning locally, please ask authorized members in team.
// File secrets.json
{
"admin": {
"email": "admin@gmail.com",
"password": "password"
},
"user": {
"email": "user@gmail.com",
"password": "password"
}
}
- To use the data in a scenario, refer to its name in the JSON file depending on whether it's secret or common.
Scenario: Login successfully with correct account
Given I go to login page
When I type secret data with key "user.email" to input with locator "input[name='email']"
And I type secret data with key "user.password" to input with locator "input[name='password']"
And I click button with locator "form > button"
Then I should be in home page
After defining the scenario, we have to implement functions that are embedded with the content of the step.
- Step:
When I click button with locator "form > button"
will have step function that implemented incommon.step.ts
:
When('I click button with locator {string} at index {int}', async ({ basePage }, locator, index) => {
await basePage.clickByLocator(locator, index)
})
- Test function should only contain calls to page objects and assertion logic.
- A page object class:
- Located at
pages/<page_name>.page.ts
- Must extends
BasePage
- Contains locators and actions that can be performed in that page.
- Locators should be public so it can be accessed by step for assertions.
- Actions should be defined as public methods in the class. If there is a new common methods for all pages, it should be placed inside
BasePage
instead. - If a dynamic locator is needed, a method should be created to returns the locator based on given variables.
- Located at
- Constants are placed in
common/enum.ts
. All constants related to a page object should belong to a single enum.
export class UserManagementPage extends BasePage {
firstSearchEntry: Locator;
constructor(page: Page) {
super(page, USER_MANAGEMENT_PAGE.PATH);
// constant USER_MANAGEMENT_PAGE.PATH is defined in enum file
// initialize locator in constructor
this.firstSearchEntry = this.page.locator(
`table tr:nth-child(1) td:nth-child(1) p`,
);
// ...
}
// dynamic locator
async getCellValue(rowIndex: number, columnIndex: number) {
return this.page.locator(
`table tr:nth-child(${rowIndex + 1}) td:nth-child(${columnIndex})`,
).innerText();
}
// an action
async gotoFirstSearchResult() {
const searchEntryText = await this.firstSearchEntry.innerText();
await this.clickByText(searchEntryText);
// this.clickByText is a common method/action, placed in BasePage
}
// ...
- After page object class is defined, there should be a corresponding fixture for it (in
common/fixtures.ts
):
// ...
export type PageObjectFixtures = {
basePage: BasePage;
userManagementPage: UserManagementPage; // define fixture for UserManagementPage
// ...
};
export const test = base.extend<
DataFixtures & HooksFixtures & PageObjectFixtures
>({
// ...
basePage: ({ page }, use) => use(new BasePage(page, '')),
// creation logic of fixture userManagementPage
userManagementPage: ({ page }, use) => use(new UserManagementPage(page)),
// ...
});
// these Given, When, Then function will be use instead of default ones
export const { Given, When, Then } = createBdd(test);
Note
From above example, it can be seen that all page object fixtures use the same instance of Playwright page
object, therefore a scenario can contains steps of different pages (But previous steps should navigate to the corresponding page in the next step).
- The page object fixture will then be available in any step function:
When(
'I click first UM search result',
async ({ userManagementPage }) => {
// use fixture userManagementPage when refer to page UserManagementPage
await userManagementPage.gotoFirstSearchResult();
},
);
- Shared data between steps: There is a dedicated fixture
secretDataProvider
orcommonDataProvider
for storing secret and common data between steps in a scenario.
When(
'I type secret data with key {string} to input with role {string}',
async ({ secretDataProvider, basePage }, dataKey, inputName) => {
const dataContent = getDataByKey(secretDataProvider.secretsData ?? dataKey, dataKey) ?? dataKey
await basePage.fillByRoleTextbox(inputName, dataContent)
},
)
A new shared data should be defined as an attribute of class ScenarioData
(in common/types/data.ts
)