E2E Testing with Selenium
End-to-end testing skill for Lobbi using Selenium WebDriver. Activates when working with browser automation, E2E tests, integration testing, or UI testing.
Triggers: selenium, e2e, testing, automation, webdriver, end-to-end, integration-test, browser-test, page-object
Use this skill when:
- •Setting up E2E testing infrastructure
- •Writing browser automation tests
- •Implementing Page Object pattern
- •Testing authentication flows
- •Testing multi-step workflows
- •Debugging test failures
Allowed Tools
- •selenium-webdriver (browser automation)
- •jest (test runner)
- •typescript (type safety)
- •docker (test environment)
- •chromedriver (Chrome automation)
Instructions
Core Principles
- •
Page Object Pattern
- •One class per page/component
- •Encapsulate selectors and actions
- •Return page objects for method chaining
- •Keep tests DRY and maintainable
- •
Wait Strategies
- •Use explicit waits (not implicit or sleep)
- •Wait for elements to be clickable/visible
- •Handle dynamic content loading
- •Set reasonable timeouts
- •
Test Independence
- •Each test should be independent
- •Clean up test data after tests
- •Use test isolation (separate accounts/tenants)
- •Don't depend on test execution order
- •
Error Handling
- •Take screenshots on failure
- •Log browser console errors
- •Provide meaningful error messages
- •Handle flaky tests gracefully
Test Architecture
code
tests/ ├── e2e/ │ ├── setup/ # Test environment setup │ ├── pages/ # Page Object Models │ ├── helpers/ # Test utilities │ ├── fixtures/ # Test data │ └── specs/ # Test specifications │ ├── auth/ │ ├── members/ │ └── subscriptions/
Implementation Checklist
- • Install Selenium and WebDriver dependencies
- • Configure test environment (local + CI)
- • Create base Page Object class
- • Implement authentication helper
- • Create page objects for key pages
- • Write test specifications
- • Add screenshot capture on failure
- • Configure parallel test execution
- • Add tests to CI/CD pipeline
Code Examples
1. Selenium Setup
typescript
// tests/e2e/setup/driver.ts
import { Builder, WebDriver, until, By } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
export const DEFAULT_TIMEOUT = 10000;
export class DriverManager {
private static driver: WebDriver;
static async getDriver(): Promise<WebDriver> {
if (!this.driver) {
const options = new chrome.Options();
// Headless mode for CI
if (process.env.CI === 'true') {
options.addArguments('--headless');
options.addArguments('--no-sandbox');
options.addArguments('--disable-dev-shm-usage');
}
options.addArguments('--window-size=1920,1080');
options.addArguments('--disable-gpu');
this.driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.build();
// Set implicit wait
await this.driver.manage().setTimeouts({ implicit: DEFAULT_TIMEOUT });
}
return this.driver;
}
static async quitDriver(): Promise<void> {
if (this.driver) {
await this.driver.quit();
this.driver = null;
}
}
static async takeScreenshot(filename: string): Promise<void> {
if (this.driver) {
const screenshot = await this.driver.takeScreenshot();
const fs = require('fs');
const path = require('path');
const dir = path.join(__dirname, '..', 'screenshots');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filepath = path.join(dir, `${filename}-${Date.now()}.png`);
fs.writeFileSync(filepath, screenshot, 'base64');
console.log(`Screenshot saved: ${filepath}`);
}
}
}
2. Base Page Object
typescript
// tests/e2e/pages/base.page.ts
import { WebDriver, By, until, WebElement } from 'selenium-webdriver';
import { DEFAULT_TIMEOUT } from '../setup/driver';
export abstract class BasePage {
constructor(protected driver: WebDriver) {}
/**
* Navigate to URL
*/
async navigateTo(url: string): Promise<void> {
await this.driver.get(url);
}
/**
* Wait for element to be visible
*/
async waitForElement(
locator: By,
timeout: number = DEFAULT_TIMEOUT
): Promise<WebElement> {
return this.driver.wait(until.elementLocated(locator), timeout);
}
/**
* Wait for element to be clickable
*/
async waitForClickable(
locator: By,
timeout: number = DEFAULT_TIMEOUT
): Promise<WebElement> {
const element = await this.waitForElement(locator, timeout);
await this.driver.wait(until.elementIsVisible(element), timeout);
await this.driver.wait(until.elementIsEnabled(element), timeout);
return element;
}
/**
* Click element
*/
async click(locator: By): Promise<void> {
const element = await this.waitForClickable(locator);
await element.click();
}
/**
* Type into input field
*/
async type(locator: By, text: string): Promise<void> {
const element = await this.waitForClickable(locator);
await element.clear();
await element.sendKeys(text);
}
/**
* Get text from element
*/
async getText(locator: By): Promise<string> {
const element = await this.waitForElement(locator);
return element.getText();
}
/**
* Check if element is displayed
*/
async isDisplayed(locator: By): Promise<boolean> {
try {
const element = await this.driver.findElement(locator);
return element.isDisplayed();
} catch {
return false;
}
}
/**
* Wait for URL to contain text
*/
async waitForUrl(urlFragment: string, timeout: number = DEFAULT_TIMEOUT): Promise<void> {
await this.driver.wait(until.urlContains(urlFragment), timeout);
}
/**
* Get current URL
*/
async getCurrentUrl(): Promise<string> {
return this.driver.getCurrentUrl();
}
/**
* Execute JavaScript
*/
async executeScript<T>(script: string, ...args: any[]): Promise<T> {
return this.driver.executeScript(script, ...args);
}
/**
* Scroll to element
*/
async scrollToElement(locator: By): Promise<void> {
const element = await this.waitForElement(locator);
await this.executeScript('arguments[0].scrollIntoView(true);', element);
}
/**
* Wait for loading to complete
*/
async waitForPageLoad(): Promise<void> {
await this.driver.wait(
async () => {
const readyState = await this.executeScript<string>(
'return document.readyState'
);
return readyState === 'complete';
},
DEFAULT_TIMEOUT
);
}
/**
* Get browser console logs
*/
async getConsoleLogs(): Promise<any[]> {
const logs = await this.driver.manage().logs().get('browser');
return logs;
}
}
3. Login Page Object
typescript
// tests/e2e/pages/login.page.ts
import { WebDriver, By } from 'selenium-webdriver';
import { BasePage } from './base.page';
export class LoginPage extends BasePage {
// Locators
private emailInput = By.id('email');
private passwordInput = By.id('password');
private loginButton = By.css('button[type="submit"]');
private errorMessage = By.css('[role="alert"]');
private forgotPasswordLink = By.css('a[href="/forgot-password"]');
constructor(driver: WebDriver) {
super(driver);
}
/**
* Navigate to login page
*/
async open(): Promise<void> {
await this.navigateTo(`${process.env.BASE_URL}/login`);
await this.waitForPageLoad();
}
/**
* Perform login
*/
async login(email: string, password: string): Promise<void> {
await this.type(this.emailInput, email);
await this.type(this.passwordInput, password);
await this.click(this.loginButton);
}
/**
* Check if login was successful (redirected to dashboard)
*/
async isLoginSuccessful(): Promise<boolean> {
try {
await this.waitForUrl('/dashboard', 5000);
return true;
} catch {
return false;
}
}
/**
* Get error message
*/
async getErrorMessage(): Promise<string> {
return this.getText(this.errorMessage);
}
/**
* Check if error is displayed
*/
async hasError(): Promise<boolean> {
return this.isDisplayed(this.errorMessage);
}
/**
* Click forgot password
*/
async clickForgotPassword(): Promise<void> {
await this.click(this.forgotPasswordLink);
}
}
4. Members Page Object
typescript
// tests/e2e/pages/members.page.ts
import { WebDriver, By } from 'selenium-webdriver';
import { BasePage } from './base.page';
export class MembersPage extends BasePage {
// Locators
private addMemberButton = By.css('[data-testid="add-member-btn"]');
private searchInput = By.css('[data-testid="search-input"]');
private memberRows = By.css('[data-testid="member-row"]');
private firstNameInput = By.id('firstName');
private lastNameInput = By.id('lastName');
private emailInput = By.id('email');
private saveButton = By.css('[data-testid="save-member-btn"]');
private successMessage = By.css('[data-testid="success-message"]');
constructor(driver: WebDriver) {
super(driver);
}
/**
* Navigate to members page
*/
async open(): Promise<void> {
await this.navigateTo(`${process.env.BASE_URL}/members`);
await this.waitForPageLoad();
}
/**
* Click add member button
*/
async clickAddMember(): Promise<void> {
await this.click(this.addMemberButton);
}
/**
* Fill member form
*/
async fillMemberForm(data: {
firstName: string;
lastName: string;
email: string;
}): Promise<void> {
await this.type(this.firstNameInput, data.firstName);
await this.type(this.lastNameInput, data.lastName);
await this.type(this.emailInput, data.email);
}
/**
* Save member
*/
async saveMember(): Promise<void> {
await this.click(this.saveButton);
}
/**
* Create member (full flow)
*/
async createMember(data: {
firstName: string;
lastName: string;
email: string;
}): Promise<void> {
await this.clickAddMember();
await this.fillMemberForm(data);
await this.saveMember();
}
/**
* Search for member
*/
async searchMember(query: string): Promise<void> {
await this.type(this.searchInput, query);
// Wait for search to complete
await this.driver.sleep(500);
}
/**
* Get member count
*/
async getMemberCount(): Promise<number> {
const elements = await this.driver.findElements(this.memberRows);
return elements.length;
}
/**
* Check if success message is displayed
*/
async hasSuccessMessage(): Promise<boolean> {
return this.isDisplayed(this.successMessage);
}
/**
* Get member by email
*/
async getMemberByEmail(email: string): Promise<string | null> {
const locator = By.xpath(`//tr[contains(., '${email}')]`);
try {
const element = await this.waitForElement(locator, 3000);
return element.getText();
} catch {
return null;
}
}
/**
* Click member row
*/
async clickMemberByEmail(email: string): Promise<void> {
const locator = By.xpath(`//tr[contains(., '${email}')]`);
await this.click(locator);
}
}
5. Authentication Helper
typescript
// tests/e2e/helpers/auth.helper.ts
import { WebDriver } from 'selenium-webdriver';
import { LoginPage } from '../pages/login.page';
export interface TestUser {
email: string;
password: string;
organizationId: string;
}
export class AuthHelper {
constructor(private driver: WebDriver) {}
/**
* Login as test user
*/
async login(user: TestUser): Promise<void> {
const loginPage = new LoginPage(this.driver);
await loginPage.open();
await loginPage.login(user.email, user.password);
// Wait for redirect to dashboard
const isSuccess = await loginPage.isLoginSuccessful();
if (!isSuccess) {
throw new Error('Login failed');
}
}
/**
* Login via API and set cookies (faster for test setup)
*/
async loginViaApi(user: TestUser): Promise<void> {
// Make API call to get auth token
const response = await fetch(`${process.env.API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: user.email,
password: user.password,
}),
});
const data = await response.json();
const token = data.accessToken;
// Navigate to app
await this.driver.get(process.env.BASE_URL);
// Set auth token in localStorage
await this.driver.executeScript(
`localStorage.setItem('authToken', '${token}');`
);
// Refresh to apply token
await this.driver.navigate().refresh();
}
/**
* Logout
*/
async logout(): Promise<void> {
await this.driver.get(`${process.env.BASE_URL}/logout`);
}
/**
* Check if user is logged in
*/
async isLoggedIn(): Promise<boolean> {
const url = await this.driver.getCurrentUrl();
return !url.includes('/login');
}
}
6. Test Specification Example
typescript
// tests/e2e/specs/members/create-member.spec.ts
import { WebDriver } from 'selenium-webdriver';
import { DriverManager } from '../../setup/driver';
import { AuthHelper, TestUser } from '../../helpers/auth.helper';
import { MembersPage } from '../../pages/members.page';
describe('Create Member E2E Test', () => {
let driver: WebDriver;
let authHelper: AuthHelper;
let membersPage: MembersPage;
const testUser: TestUser = {
email: process.env.TEST_USER_EMAIL!,
password: process.env.TEST_USER_PASSWORD!,
organizationId: process.env.TEST_ORG_ID!,
};
beforeAll(async () => {
driver = await DriverManager.getDriver();
authHelper = new AuthHelper(driver);
membersPage = new MembersPage(driver);
}, 30000);
afterAll(async () => {
await DriverManager.quitDriver();
}, 30000);
beforeEach(async () => {
// Login before each test
await authHelper.loginViaApi(testUser);
}, 15000);
afterEach(async function () {
// Take screenshot on failure
if (this.currentTest?.state === 'failed') {
await DriverManager.takeScreenshot(this.currentTest.title);
}
});
it('should create a new member successfully', async () => {
// Navigate to members page
await membersPage.open();
// Get initial member count
const initialCount = await membersPage.getMemberCount();
// Create new member
const newMember = {
firstName: 'Test',
lastName: 'User',
email: `test-${Date.now()}@example.com`,
};
await membersPage.createMember(newMember);
// Verify success message
const hasSuccess = await membersPage.hasSuccessMessage();
expect(hasSuccess).toBe(true);
// Verify member appears in list
const member = await membersPage.getMemberByEmail(newMember.email);
expect(member).not.toBeNull();
expect(member).toContain(newMember.firstName);
expect(member).toContain(newMember.lastName);
// Verify count increased
const newCount = await membersPage.getMemberCount();
expect(newCount).toBe(initialCount + 1);
}, 30000);
it('should show error for duplicate email', async () => {
await membersPage.open();
// Create first member
const member = {
firstName: 'First',
lastName: 'Member',
email: `duplicate-${Date.now()}@example.com`,
};
await membersPage.createMember(member);
await membersPage.hasSuccessMessage();
// Try to create duplicate
await membersPage.createMember(member);
// Should show error (implement error check in page object)
// const hasError = await membersPage.hasError();
// expect(hasError).toBe(true);
}, 30000);
it('should search members by email', async () => {
await membersPage.open();
// Create test member
const member = {
firstName: 'Search',
lastName: 'Test',
email: `search-${Date.now()}@example.com`,
};
await membersPage.createMember(member);
// Search for member
await membersPage.searchMember(member.email);
// Verify member appears in results
const foundMember = await membersPage.getMemberByEmail(member.email);
expect(foundMember).not.toBeNull();
}, 30000);
});
7. Jest Configuration
javascript
// jest.e2e.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/tests/e2e/**/*.spec.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/e2e/setup/jest.setup.ts'],
testTimeout: 30000,
maxWorkers: 1, // Run tests sequentially
bail: false, // Continue running tests after failures
verbose: true,
};
8. Test Setup File
typescript
// tests/e2e/setup/jest.setup.ts
import { DriverManager } from './driver';
// Global setup
beforeAll(async () => {
console.log('Starting E2E test suite...');
// Verify environment variables
const requiredEnvVars = [
'BASE_URL',
'API_URL',
'TEST_USER_EMAIL',
'TEST_USER_PASSWORD',
'TEST_ORG_ID',
];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
});
// Global teardown
afterAll(async () => {
await DriverManager.quitDriver();
console.log('E2E test suite completed.');
});
// Handle unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
9. Docker Compose for Test Environment
yaml
# docker-compose.e2e.yml
version: '3.8'
services:
chrome:
image: selenium/standalone-chrome:latest
ports:
- "4444:4444"
shm_size: 2gb
environment:
- SE_NODE_MAX_SESSIONS=5
test-runner:
build:
context: .
dockerfile: Dockerfile.e2e
depends_on:
- chrome
environment:
- BASE_URL=http://frontend:3000
- API_URL=http://backend:5000
- SELENIUM_REMOTE_URL=http://chrome:4444/wd/hub
- TEST_USER_EMAIL=${TEST_USER_EMAIL}
- TEST_USER_PASSWORD=${TEST_USER_PASSWORD}
- TEST_ORG_ID=${TEST_ORG_ID}
volumes:
- ./tests/e2e:/app/tests/e2e
- ./tests/e2e/screenshots:/app/screenshots
10. CI/CD Integration
yaml
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Start services
run: docker-compose -f docker-compose.e2e.yml up -d
- name: Wait for services
run: |
timeout 60 bash -c 'until curl -f http://localhost:4444/wd/hub/status; do sleep 2; done'
- name: Run E2E tests
env:
BASE_URL: http://localhost:3000
API_URL: http://localhost:5000
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
TEST_ORG_ID: ${{ secrets.TEST_ORG_ID }}
CI: true
run: npm run test:e2e
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: e2e-screenshots
path: tests/e2e/screenshots/
- name: Stop services
if: always()
run: docker-compose -f docker-compose.e2e.yml down
Best Practices
- •Use Page Objects - Encapsulate page logic
- •Explicit Waits - Wait for specific conditions
- •Test Independence - Each test should be self-contained
- •Meaningful Selectors - Use data-testid attributes
- •Screenshot on Failure - Capture evidence for debugging
- •Clean Test Data - Create unique test data per run
- •Parallel Execution - Run tests in parallel when possible
Common Pitfalls
- •❌ Using implicit waits or sleep()
- •❌ Tests depending on each other
- •❌ Not handling async operations properly
- •❌ Using fragile CSS selectors
- •❌ Not cleaning up test data
- •❌ Ignoring flaky tests
- •❌ Not running tests in CI/CD
Debugging Tips
- •Run in non-headless mode - Remove
--headlessflag - •Use browser DevTools - Inspect elements
- •Add console logs - Log test progress
- •Check screenshots - Review failure screenshots
- •Slow down execution - Add pauses for debugging
- •Check browser console - Review JS errors
Related Skills
- •testing - Unit and integration testing patterns
- •authentication - Auth flow testing
- •member-management - Member CRUD testing
- •stripe-payments - Payment flow testing