https://github.com/balukov/e2e-best-practices
The boilerplate project with advice best practices for e2e tests
https://github.com/balukov/e2e-best-practices
best-practices boilerplate e2e e2e-tests husky prettier tslint
Last synced: about 1 month ago
JSON representation
The boilerplate project with advice best practices for e2e tests
- Host: GitHub
- URL: https://github.com/balukov/e2e-best-practices
- Owner: balukov
- License: mit
- Created: 2019-10-15T06:29:03.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2026-04-08T02:09:29.000Z (2 months ago)
- Last Synced: 2026-04-08T04:12:08.292Z (2 months ago)
- Topics: best-practices, boilerplate, e2e, e2e-tests, husky, prettier, tslint
- Language: JavaScript
- Homepage:
- Size: 541 KB
- Stars: 8
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# The boilerplate with advice best practices for e2e tests
> **Note (2026):** This guide was written in 2019 using WebdriverIO v5, TSLint, Mocha, and TypeScript 3.6.
> The **architectural concepts** (Page Object Model, typed tests, component structure, `data-test` attributes, test organization) remain best practices.
> The **tooling** is outdated — the modern stack is **Playwright + TypeScript 5 + ESLint + Prettier**.
> Each outdated section below is marked with a ⚠️ note.
## 1. Introduction
This project is not just the instruction about the best practices or just the boilerplate with ready to use code.
It's an alliance for both.
Code = advices, advices = code.
The code was written a total with this advice which contains specific examples from boilerplate.
All examples are hidden under text with arrow like this:
Real example (click for expand)
---
```
example of the code
```
Explanation if necessary
[Link to the real place in the file with this code]()
---
If you don't understand the guide - look the code with full files.
If you don't understand how something works in the code - look this guide with the description and reasons why I did that.
The project is written using framework Webdriver.io for Node.js and Typescript.
But every advice can be used for other programming languages with adjustments.
The project uses the mock site https://www.saucedemo.com for demo tests.
To test your project you should modify the pages, components and, of course, tests.
All tests 100% work.
Feel free to create issues with questions and additions.
### 1.1. Installation
```
npm i
```
### 1.2. Running tests
```
npm test
```
## 2. Developers standards
The important thing that tests are a **project**. Not just a suite of tests which do some magic and talk "green" or "red". It's a real project with its architecture, rules of code, running, configuration. And for convenient maintenance, development, and teamwork you need to use the same rules as for the common project.
### 🧩 Typings
If your language support typings it is necessary to use it - fewer bugs in developing, smart suggests. This project uses Typescript and typings uses everywhere.
Real example
---
```typescript
getButton(productName: string): string {
const productIndex = this.getProductIndex(productName);
return this.productButtons()[productIndex].getText();
}
```
Function getButton() can take an only string and return only strings.
[pages/components/list.page.ts](pages/components/list.page.ts#L34)
---
### 🧩 Static code analysis (Linter)
> ⚠️ **Outdated:** TSLint was deprecated in 2019. Use **ESLint** with `@typescript-eslint` instead. For the `.only()` rule, use `eslint-plugin-no-only-tests`.
It helps to avoid common language mistakes, make rules specifically for the project.
Real example
---
```json
"mocha-avoid-only": true
```
The rule does not allow us to commit to the repository .only (). Because only tests with .only () will be run. And we want to run all.
[tslint.json](tslint.json#L13)
---
### 🧩 Prettier
Prettier makes code beautiful and consistently if many developers work with the project.
### 🧩 Autorun
> ⚠️ **Outdated config:** Husky v9+ uses a `.husky/` directory with shell scripts instead of `package.json` hooks. The concept of pre-commit automation is still a best practice.
Husky runs linter and prettier automatically before commit because without autorun everybody forgets runs.
## 3. Folders structure
### 🧩 Choose type of storing tests
There are two options for storing tests in files: app-pages and app-actions. The type of storing depends on the project. Think about the future when you choose. If you want to run tests particularly, which tests should be run? "Profile page tests" which do all checks for the profile page or "Purchase tests" which check all stages for buying. It's not strong definitions and your project can use both simultaneously, it's complicated but it's possible. I recommend starting from app-pages and if you understand this doesn't fit then to switch to app-actions.
🔘 App-pages
The app divides into real pages, each file contains tests for the page where did the last action in the test. Simple to start, complicate continue.
Real example
---
```
.
├── ...
├── specs # Test files
│ ├── index.spec # Tests for index page
│ ├── products.spec # Tests for product page
│ └── cart.spec # Tests for cart page
└── ...
```
---
🔘 App-actions
The app divides to user action parts, imagine how a user can use your app. Complicate to start, simple to continue.
Example
---
```
.
├── ...
├── specs # Test files
│ ├── download.spec # Tests for downloads files
│ ├── crud-goods.spec # Create-Read-Update-Delete for item
│ └── purchase-cancel.spec # Cancelation actions
└── ...
```
---
## 4. Test structure
### 🧩 Import pages what you need in the test at top file
Real example
---
```typescript
import index from '@pages/index.page';
import products from '@pages/products.page';
```
[specs/products.spec.ts](specs/products.spec.ts#L7)
---
### 🧩 One describe per file
Real example
---
```typescript
describe('Products page', () => {
// tests
}
```
[specs/products.spec.ts](specs/products.spec.ts#L12)
---
### 🧩 Use preconditions
Actions should be done before the test starts. Usually, these are: open app URL -> authorization -> open page for a test (if it is needed)
Real example
---
```typescript
before('Open index page', () => {
browser.url('');
index.loginForm().authStandardUser();
});
```
[specs/products.spec.ts](specs/products.spec.ts#L13)
---
### 🧩 Test step structure: page.component.step
Real example
---
```typescript
index.loginForm().authStandardUser();
```
page is "index"
component is "loginForm"
step is "authStandardUser"
[specs/products.spec.ts](specs/products.spec.ts#L15)
```typescript
products.list().addToCart(productName);
```
page is "products"
component is "list"
step is "addToCart"
[specs/products.spec.ts](specs/products.spec.ts#L19)
---
## 4. Page structure
### 🧩 Import all components for page
Real example
---
```typescript
import list from '@components/list.page';
```
[pages/products.page.ts](pages/products.page.ts#L1)
---
## 5. Component structure
### 🧩 All selectors begin with the component selector
Let xPath for components and add the path to begin the every interact element
Real example
---
```typescript
private list = () => $('//*[@class="inventory_list"]');
private productNames = () => this.list().$$(`//*[@class="inventory_item_name"]`);
private productButtons = () => this.list().$$(`//*[contains(@class,"btn_inventory")]`);
```
[pages/components/list.page.ts](pages/components/list.page.ts#L6)
---
### 🧩 Component file contains steps, selector actions, selectors
## 6. Selector structure
### 🧩 Use XPath
> ⚠️ **Outdated:** XPath is now a last resort. Modern frameworks provide semantic locators — Playwright offers `getByRole()`, `getByText()`, `getByTestId()` which are far more readable and resilient. Prefer CSS selectors or framework-native locators.
### 🧩 Set asterisk for selectors
> ⚠️ **Outdated:** The `//*[@class="..."]` pattern is brittle with class name changes. Prefer `data-testid` attributes with framework-native locators like `page.getByTestId('inventory-list')`.
It helps if the app will be refactored
Real example
---
```typescript
private list = () => $('//*[@class="inventory_list"]');
```
[pages/components/list.page.ts](pages/components/list.page.ts#L6)
---
### 🧩 Add "data-test" for elements where it's possible.
Ask developers or do it yourself. It makes the call selectors easier.
Real example
---
```html
```
```typescript
// selector
private username = () => $('//*[@data-test="username"]');
```
[pages/components/login-form.page.ts](pages/components/login-form.page.ts#L6)
---
## 7. Modern Stack (2026)
If starting a new E2E project today, consider this stack:
- **Playwright** — Fast, reliable, auto-waiting, built-in test runner, trace viewer, codegen
- **TypeScript 5** — Strict typing with modern features
- **ESLint** + **Prettier** — Linting and formatting
- **Husky v9** + **lint-staged** — Pre-commit automation
- **`data-testid` attributes** — Stable selectors (unchanged from this guide)
- **Page Object Model** — Still the recommended pattern (unchanged from this guide)
### Key features available in modern frameworks that this guide doesn't cover
- **Auto-waiting** — No more explicit waits or sleep
- **API mocking** — `page.route()` to intercept network requests
- **Visual regression** — Built-in screenshot comparison
- **Accessibility testing** — `@axe-core/playwright`
- **Parallel execution** — Isolated browser contexts by default
- **Trace & video recording** — Built-in debugging tools
## Coming soon
> ⚠️ **Update:** Most of these features are now built into modern frameworks like Playwright:
> - ~~Accessibility tests~~ → `@axe-core/playwright`
> - ~~Visual comparison tests~~ → `expect(page).toHaveScreenshot()`
> - ~~Docker container for CI~~ → Playwright provides official Docker images
> - ~~Clear and readable report~~ → Playwright HTML Reporter, Trace Viewer
- Pictures for visualizing the structure