End-to-End Testing
Learn about End-to-End (E2E) testing with Selenium WebDriver, Playwright, and Cypress. Understand when to use each tool and how to implement comprehensive E2E test strategies.
What is End-to-End Testing?
End-to-End (E2E) testing validates your entire application flow from start to finish, simulating real user scenarios across multiple systems and components.
Characteristics
- Full Stack: Tests the complete application, including frontend, backend, and databases
- User Perspective: Simulates real user interactions and workflows
- High Confidence: Catches integration issues that unit and integration tests might miss
- Slower Execution: Takes more time to run compared to unit tests
- Environment Dependent: Requires a complete testing environment
E2E tests should focus on critical user journeys and business workflows rather than covering every possible scenario. Use unit and integration tests for detailed coverage.
Testing Tools Comparison
| Feature | Selenium | Playwright | Cypress |
|---|---|---|---|
| Language Support | Java, Python, C#, Ruby, JavaScript | JavaScript, TypeScript, Python, Java, .NET | JavaScript, TypeScript |
| Browser Support | Chrome, Firefox, Safari, Edge, IE | Chrome, Firefox, Safari, Edge | Chrome, Firefox, Edge |
| Mobile Testing | Yes (Appium integration) | Yes (Device emulation) | Limited (viewport only) |
| Auto-Wait | No (manual waits) | Yes | Yes |
| Speed | Moderate | Fast | Fast |
| Network Mocking | No | Yes | Yes |
| Component Testing | No | Yes | Yes |
| Debugging | Standard browser DevTools | Playwright Inspector, Trace Viewer | Time-travel debugging |
| Cross-browser | Excellent | Excellent | Good |
| Learning Curve | Steep | Moderate | Easy |
| Parallel Testing | Grid required | Built-in | Dashboard required |
| Best For | Cross-browser, legacy apps | Modern apps, API testing | Developer experience, fast feedback |
Selenium WebDriver
Selenium is the most mature and widely-used browser automation framework, supporting multiple languages and browsers.
What is Selenium?
Selenium WebDriver is a browser automation framework that provides a programming interface to create and execute test scripts in real browsers. It supports multiple programming languages and browsers, making it ideal for cross-browser testing.
Key Components
- WebDriver: API for browser automation
- Selenium Grid: Distributed test execution across multiple machines
- Browser Drivers: Browser-specific implementations (ChromeDriver, GeckoDriver, etc.)
- Selenium IDE: Record and playback tool for creating tests
Selenium Architecture
┌─────────────────────┐
│ Test Script │
│ (Java/Python/JS) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ WebDriver API │
│ (Language Binding) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ JSON Wire Protocol │
│ /W3C WebDriver │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Browser Driver │
│ (ChromeDriver, │
│ GeckoDriver, etc.)│
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Browser │
│ (Chrome, Firefox) │
└─────────────────────┘Selenium communicates with browsers through browser-specific drivers using the W3C WebDriver protocol, allowing language-agnostic browser automation.
WebDriver Setup
Chrome Setup
// npm install selenium-webdriver chromedriver
import { Builder, Browser, By, until } from 'selenium-webdriver'
import chrome from 'selenium-webdriver/chrome'
async function setupChrome() {
const options = new chrome.Options()
// Headless mode (no UI)
options.addArguments('--headless')
// Window size
options.addArguments('--window-size=1920,1080')
// Disable GPU (for CI environments)
options.addArguments('--disable-gpu')
// Disable dev shm (for Docker)
options.addArguments('--disable-dev-shm-usage')
// No sandbox (for CI)
options.addArguments('--no-sandbox')
const driver = await new Builder()
.forBrowser(Browser.CHROME)
.setChromeOptions(options)
.build()
return driver
}
// Usage
const driver = await setupChrome()
try {
await driver.get('https://example.com')
// Test code here
} finally {
await driver.quit()
}Firefox Setup
import { Builder, Browser } from 'selenium-webdriver'
import firefox from 'selenium-webdriver/firefox'
async function setupFirefox() {
const options = new firefox.Options()
// Headless mode
options.addArguments('-headless')
// Set preferences
options.setPreference('dom.webnotifications.enabled', false)
options.setPreference('geo.enabled', false)
const driver = await new Builder()
.forBrowser(Browser.FIREFOX)
.setFirefoxOptions(options)
.build()
return driver
}Edge Setup
import { Builder, Browser } from 'selenium-webdriver'
import edge from 'selenium-webdriver/edge'
async function setupEdge() {
const options = new edge.Options()
options.addArguments('--headless')
options.addArguments('--disable-gpu')
const driver = await new Builder()
.forBrowser(Browser.EDGE)
.setEdgeOptions(options)
.build()
return driver
}Cross-Browser Test Setup
import { describe, it, beforeEach, afterEach } from 'vitest'
import { WebDriver, Builder, Browser } from 'selenium-webdriver'
const browsers = [Browser.CHROME, Browser.FIREFOX, Browser.EDGE]
browsers.forEach(browserName => {
describe(`Tests on ${browserName}`, () => {
let driver: WebDriver
beforeEach(async () => {
driver = await new Builder()
.forBrowser(browserName)
.build()
})
afterEach(async () => {
await driver.quit()
})
it('should work on all browsers', async () => {
await driver.get('https://example.com')
const title = await driver.getTitle()
expect(title).toBe('Example Domain')
})
})
})Locator Strategies
Selenium provides multiple strategies for locating elements on a page.
By ID
The most reliable and fastest locator strategy.
import { By } from 'selenium-webdriver'
// HTML: <input id="email" type="email" />
const emailInput = await driver.findElement(By.id('email'))
await emailInput.sendKeys('user@example.com')
// Multiple elements (returns first match)
const element = await driver.findElement(By.id('unique-id'))IDs should be unique on the page. If an ID appears multiple times, findElement returns the first match, which may not be the element you want.
By Name
Useful for form elements.
// HTML: <input name="username" />
const usernameInput = await driver.findElement(By.name('username'))
await usernameInput.sendKeys('john_doe')
// Find all elements with the same name (radio buttons, checkboxes)
const checkboxes = await driver.findElements(By.name('interests'))
for (const checkbox of checkboxes) {
await checkbox.click()
}By Class Name
Locates elements by CSS class.
// HTML: <button class="btn btn-primary">Submit</button>
const button = await driver.findElement(By.className('btn-primary'))
await button.click()
// Multiple elements
const allButtons = await driver.findElements(By.className('btn'))
console.log(`Found ${allButtons.length} buttons`)By Tag Name
Locates elements by HTML tag.
// Find all links
const links = await driver.findElements(By.tagName('a'))
console.log(`Found ${links.length} links`)
// Find first h1
const heading = await driver.findElement(By.tagName('h1'))
const text = await heading.getText()By Link Text
Locates links by their exact text content.
// HTML: <a href="/about">About Us</a>
const aboutLink = await driver.findElement(By.linkText('About Us'))
await aboutLink.click()
// Case-sensitive exact match required
const link = await driver.findElement(By.linkText('Sign In'))By Partial Link Text
Locates links by partial text match.
// HTML: <a href="/products">View All Products</a>
const productsLink = await driver.findElement(By.partialLinkText('Products'))
await productsLink.click()
// Useful when link text is dynamic
const link = await driver.findElement(By.partialLinkText('Welcome'))By CSS Selector
Powerful and flexible locator using CSS selectors.
// By ID
await driver.findElement(By.css('#email'))
// By class
await driver.findElement(By.css('.btn-primary'))
// By attribute
await driver.findElement(By.css('[data-testid="submit-button"]'))
await driver.findElement(By.css('input[type="email"]'))
// Descendant selectors
await driver.findElement(By.css('form .error-message'))
await driver.findElement(By.css('#login-form input[name="password"]'))
// Pseudo-classes
await driver.findElement(By.css('button:enabled'))
await driver.findElement(By.css('li:first-child'))
await driver.findElement(By.css('input:not([disabled])'))
// Attribute contains
await driver.findElement(By.css('[class*="btn"]'))
await driver.findElement(By.css('[href^="https"]'))
await driver.findElement(By.css('[href$=".pdf"]'))
// Complex selectors
await driver.findElement(By.css('nav > ul > li:nth-child(2) > a'))By XPath
Most powerful but also most fragile locator strategy.
// Absolute XPath (avoid - brittle)
await driver.findElement(By.xpath('/html/body/div[1]/form/input[2]'))
// Relative XPath (preferred)
await driver.findElement(By.xpath('//input[@id="email"]'))
// By text content
await driver.findElement(By.xpath('//button[text()="Submit"]'))
await driver.findElement(By.xpath('//h1[contains(text(), "Welcome")]'))
// By attribute
await driver.findElement(By.xpath('//input[@name="username"]'))
await driver.findElement(By.xpath('//a[@href="/about"]'))
// Contains
await driver.findElement(By.xpath('//div[contains(@class, "error")]'))
await driver.findElement(By.xpath('//span[contains(text(), "Error")]'))
// Parent selection
await driver.findElement(By.xpath('//input[@id="email"]/..'))
await driver.findElement(By.xpath('//button/parent::form'))
// Following sibling
await driver.findElement(By.xpath('//label[text()="Email"]/following-sibling::input'))
// Preceding sibling
await driver.findElement(By.xpath('//input[@id="email"]/preceding-sibling::label'))
// Axes combinations
await driver.findElement(By.xpath('//form//input[@type="text"][1]'))
await driver.findElement(By.xpath('//div[@id="container"]//button[last()]'))
// Multiple conditions
await driver.findElement(By.xpath('//input[@type="text" and @name="email"]'))
await driver.findElement(By.xpath('//button[@type="submit" or @class="submit"]'))XPath Best Practices:
- Avoid absolute XPaths - they break easily when page structure changes
- Prefer relative XPaths starting with
// - Use
contains()for partial matches - Combine with unique attributes when possible
- Test XPaths in browser DevTools before using in code
Locator Strategy Priority
- ID - Fastest and most reliable
- Name - Good for form elements
- CSS Selector - Flexible and performant
- XPath - Most powerful but slower and fragile
- Class/Tag - Use when combined with other strategies
- Link Text - Good for navigation links
// ✅ Good - Specific and reliable
await driver.findElement(By.id('submit-btn'))
await driver.findElement(By.css('[data-testid="user-profile"]'))
// ⚠️ Acceptable - Less specific
await driver.findElement(By.css('.user-profile'))
await driver.findElement(By.name('email'))
// ❌ Avoid - Fragile and slow
await driver.findElement(By.xpath('/html/body/div[3]/div[2]/form/button'))
await driver.findElement(By.className('btn'))Basic Commands and Navigation
Navigation
import { WebDriver } from 'selenium-webdriver'
async function navigationExamples(driver: WebDriver) {
// Navigate to URL
await driver.get('https://example.com')
// Get current URL
const currentUrl = await driver.getCurrentUrl()
console.log(`Current URL: ${currentUrl}`)
// Get page title
const title = await driver.getTitle()
console.log(`Page Title: ${title}`)
// Navigate back
await driver.navigate().back()
// Navigate forward
await driver.navigate().forward()
// Refresh page
await driver.navigate().refresh()
// Navigate to URL (alternative)
await driver.navigate().to('https://example.com/page')
}Element Interactions
import { By, Key } from 'selenium-webdriver'
async function elementInteractions(driver: WebDriver) {
// Click element
const button = await driver.findElement(By.id('submit'))
await button.click()
// Type text
const input = await driver.findElement(By.name('username'))
await input.sendKeys('john_doe')
// Clear input
await input.clear()
// Send special keys
await input.sendKeys('test', Key.ENTER)
await input.sendKeys(Key.chord(Key.CONTROL, 'a')) // Ctrl+A
// Get text content
const heading = await driver.findElement(By.tagName('h1'))
const text = await heading.getText()
// Get attribute value
const link = await driver.findElement(By.tagName('a'))
const href = await link.getAttribute('href')
const className = await link.getAttribute('class')
// Get CSS property value
const color = await button.getCssValue('color')
const fontSize = await button.getCssValue('font-size')
// Check if element is displayed
const isDisplayed = await button.isDisplayed()
// Check if element is enabled
const isEnabled = await button.isEnabled()
// Check if checkbox/radio is selected
const checkbox = await driver.findElement(By.id('terms'))
const isSelected = await checkbox.isSelected()
// Submit form
const form = await driver.findElement(By.tagName('form'))
await form.submit()
}Working with Multiple Elements
async function multipleElements(driver: WebDriver) {
// Find all matching elements
const listItems = await driver.findElements(By.tagName('li'))
console.log(`Found ${listItems.length} items`)
// Iterate through elements
for (const item of listItems) {
const text = await item.getText()
console.log(text)
}
// Click specific element
if (listItems.length > 0) {
await listItems[0].click()
}
// Find nested elements
const container = await driver.findElement(By.id('container'))
const buttons = await container.findElements(By.tagName('button'))
}Handling Forms and Inputs
Text Inputs
async function handleTextInputs(driver: WebDriver) {
// Basic text input
const nameInput = await driver.findElement(By.id('name'))
await nameInput.sendKeys('John Doe')
// Email input
const emailInput = await driver.findElement(By.css('input[type="email"]'))
await emailInput.clear()
await emailInput.sendKeys('john@example.com')
// Password input
const passwordInput = await driver.findElement(By.name('password'))
await passwordInput.sendKeys('secretPassword123')
// Textarea
const commentBox = await driver.findElement(By.tagName('textarea'))
await commentBox.sendKeys('This is a long comment\nwith multiple lines')
// Verify value
const value = await nameInput.getAttribute('value')
expect(value).toBe('John Doe')
}Checkboxes and Radio Buttons
async function handleCheckboxesAndRadios(driver: WebDriver) {
// Check a checkbox
const termsCheckbox = await driver.findElement(By.id('terms'))
if (!await termsCheckbox.isSelected()) {
await termsCheckbox.click()
}
// Verify checkbox is checked
expect(await termsCheckbox.isSelected()).toBe(true)
// Uncheck checkbox
if (await termsCheckbox.isSelected()) {
await termsCheckbox.click()
}
// Select radio button
const maleRadio = await driver.findElement(By.css('input[value="male"]'))
await maleRadio.click()
// Verify radio is selected
expect(await maleRadio.isSelected()).toBe(true)
// Work with multiple checkboxes
const checkboxes = await driver.findElements(By.css('input[type="checkbox"]'))
for (const checkbox of checkboxes) {
if (!await checkbox.isSelected()) {
await checkbox.click()
}
}
}Dropdown Selects
import { Select } from 'selenium-webdriver/lib/select'
async function handleDropdowns(driver: WebDriver) {
const selectElement = await driver.findElement(By.id('country'))
const select = new Select(selectElement)
// Select by visible text
await select.selectByVisibleText('United States')
// Select by value
await select.selectByValue('us')
// Select by index
await select.selectByIndex(1)
// Get selected option
const selectedOption = await select.getFirstSelectedOption()
const selectedText = await selectedOption.getText()
console.log(`Selected: ${selectedText}`)
// Get all options
const options = await select.getOptions()
console.log(`Total options: ${options.length}`)
// Multi-select dropdown
const multiSelect = new Select(await driver.findElement(By.id('skills')))
// Select multiple options
await multiSelect.selectByVisibleText('JavaScript')
await multiSelect.selectByVisibleText('TypeScript')
await multiSelect.selectByVisibleText('Python')
// Get all selected options
const selectedOptions = await multiSelect.getAllSelectedOptions()
console.log(`Selected ${selectedOptions.length} skills`)
// Deselect options
await multiSelect.deselectByVisibleText('Python')
await multiSelect.deselectAll()
}File Uploads
import path from 'path'
async function handleFileUpload(driver: WebDriver) {
const fileInput = await driver.findElement(By.css('input[type="file"]'))
// Path to file (absolute path required)
const filePath = path.resolve(__dirname, 'test-files', 'document.pdf')
// Send file path to input
await fileInput.sendKeys(filePath)
// Verify file is selected
const fileName = await fileInput.getAttribute('value')
expect(fileName).toContain('document.pdf')
// Submit form
const uploadButton = await driver.findElement(By.id('upload-btn'))
await uploadButton.click()
}Handling Alerts and Popups
JavaScript Alerts
import { until } from 'selenium-webdriver'
async function handleAlerts(driver: WebDriver) {
// Trigger alert
const alertButton = await driver.findElement(By.id('alert-btn'))
await alertButton.click()
// Wait for alert to appear
await driver.wait(until.alertIsPresent(), 5000)
// Switch to alert
const alert = await driver.switchTo().alert()
// Get alert text
const alertText = await alert.getText()
console.log(`Alert says: ${alertText}`)
// Accept alert (click OK)
await alert.accept()
}Confirm Dialogs
async function handleConfirm(driver: WebDriver) {
const confirmButton = await driver.findElement(By.id('confirm-btn'))
await confirmButton.click()
await driver.wait(until.alertIsPresent(), 5000)
const confirmDialog = await driver.switchTo().alert()
const text = await confirmDialog.getText()
console.log(`Confirm dialog: ${text}`)
// Accept (OK)
await confirmDialog.accept()
// Or dismiss (Cancel)
// await confirmDialog.dismiss()
}Prompt Dialogs
async function handlePrompt(driver: WebDriver) {
const promptButton = await driver.findElement(By.id('prompt-btn'))
await promptButton.click()
await driver.wait(until.alertIsPresent(), 5000)
const promptDialog = await driver.switchTo().alert()
// Type into prompt
await promptDialog.sendKeys('John Doe')
// Submit
await promptDialog.accept()
// Or cancel
// await promptDialog.dismiss()
}Window Handles and Popups
async function handlePopupWindows(driver: WebDriver) {
// Get current window handle
const mainWindow = await driver.getWindowHandle()
// Click link that opens new window
const popupLink = await driver.findElement(By.id('open-popup'))
await popupLink.click()
// Get all window handles
const windows = await driver.getAllWindowHandles()
// Switch to new window
for (const handle of windows) {
if (handle !== mainWindow) {
await driver.switchTo().window(handle)
break
}
}
// Do something in popup
const heading = await driver.findElement(By.tagName('h1'))
const text = await heading.getText()
console.log(`Popup heading: ${text}`)
// Close popup
await driver.close()
// Switch back to main window
await driver.switchTo().window(mainWindow)
}iFrames
async function handleIframes(driver: WebDriver) {
// Switch to iframe by index
await driver.switchTo().frame(0)
// Switch to iframe by name or ID
await driver.switchTo().frame('iframe-name')
// Switch to iframe by WebElement
const iframeElement = await driver.findElement(By.id('my-iframe'))
await driver.switchTo().frame(iframeElement)
// Interact with elements inside iframe
const button = await driver.findElement(By.id('iframe-button'))
await button.click()
// Switch back to main content
await driver.switchTo().defaultContent()
// Switch to parent frame
await driver.switchTo().parentFrame()
}Waiting Strategies
Implicit Waits
Tells WebDriver to poll the DOM for a certain amount of time when trying to find elements.
async function implicitWaitExample(driver: WebDriver) {
// Set implicit wait (applies to all findElement calls)
await driver.manage().setTimeouts({ implicit: 10000 }) // 10 seconds
// This will wait up to 10 seconds for element to appear
const element = await driver.findElement(By.id('dynamic-element'))
await element.click()
}Don’t mix implicit and explicit waits! It can lead to unpredictable wait times. Choose one strategy and stick with it.
Explicit Waits
Wait for a specific condition to be true before proceeding.
import { until, WebDriver } from 'selenium-webdriver'
async function explicitWaitExamples(driver: WebDriver) {
// Wait for element to be located
await driver.wait(
until.elementLocated(By.id('submit-btn')),
10000,
'Element not found'
)
// Wait for element to be visible
const element = await driver.findElement(By.id('message'))
await driver.wait(
until.elementIsVisible(element),
5000
)
// Wait for element to be enabled
const button = await driver.findElement(By.id('submit'))
await driver.wait(
until.elementIsEnabled(button),
5000
)
// Wait for element to be selected
const checkbox = await driver.findElement(By.id('terms'))
await driver.wait(
until.elementIsSelected(checkbox),
5000
)
// Wait for element to contain text
await driver.wait(
until.elementTextContains(element, 'Success'),
5000
)
// Wait for element to be clickable (visible and enabled)
await driver.wait(
until.elementIsEnabled(button),
5000
)
await driver.wait(
until.elementIsVisible(button),
5000
)
// Wait for title to contain text
await driver.wait(
until.titleContains('Dashboard'),
10000
)
// Wait for URL to contain text
await driver.wait(
until.urlContains('/dashboard'),
10000
)
}Fluent Waits
More flexible waiting with custom polling intervals and ignore exceptions.
import { WebDriver, until, WebElement } from 'selenium-webdriver'
async function fluentWaitExample(driver: WebDriver) {
// Custom wait function
const waitForElementText = async (
locator: any,
expectedText: string,
timeout: number = 10000
): Promise<WebElement> => {
const endTime = Date.now() + timeout
while (Date.now() < endTime) {
try {
const element = await driver.findElement(locator)
const text = await element.getText()
if (text.includes(expectedText)) {
return element
}
} catch (error) {
// Ignore NoSuchElementException
}
// Poll every 500ms
await driver.sleep(500)
}
throw new Error(`Element with text "${expectedText}" not found within ${timeout}ms`)
}
// Usage
const element = await waitForElementText(
By.id('status'),
'Complete',
15000
)
}Custom Wait Conditions
import { WebDriver, Condition } from 'selenium-webdriver'
async function customWaitConditions(driver: WebDriver) {
// Wait for element to have specific class
const hasClass = new Condition(
'for element to have class',
async () => {
const element = await driver.findElement(By.id('button'))
const className = await element.getAttribute('class')
return className?.includes('active') || false
}
)
await driver.wait(hasClass, 10000)
// Wait for element count
const elementCountIs = (locator: any, count: number) => {
return new Condition(
`for element count to be ${count}`,
async () => {
const elements = await driver.findElements(locator)
return elements.length === count
}
)
}
await driver.wait(elementCountIs(By.className('item'), 5), 10000)
// Wait for AJAX request to complete
const ajaxComplete = new Condition(
'for AJAX to complete',
async () => {
const result = await driver.executeScript(
'return jQuery.active === 0'
)
return result as boolean
}
)
await driver.wait(ajaxComplete, 10000)
}Page Object Model (POM) Pattern
The Page Object Model is a design pattern that creates an object repository for web elements, improving test maintenance and reducing code duplication.
Benefits
- Maintainability: Changes to UI require updates in one place
- Reusability: Page objects can be used across multiple tests
- Readability: Tests are more readable and easier to understand
- Separation of Concerns: Separates test logic from page interactions
Basic Page Object
// pages/LoginPage.ts
import { WebDriver, By, until } from 'selenium-webdriver'
export class LoginPage {
private driver: WebDriver
private url = 'https://example.com/login'
// Locators
private locators = {
emailInput: By.id('email'),
passwordInput: By.id('password'),
submitButton: By.css('button[type="submit"]'),
errorMessage: By.className('error-message'),
successMessage: By.className('success-message'),
}
constructor(driver: WebDriver) {
this.driver = driver
}
// Page actions
async navigate(): Promise<void> {
await this.driver.get(this.url)
}
async enterEmail(email: string): Promise<void> {
const emailField = await this.driver.findElement(this.locators.emailInput)
await emailField.clear()
await emailField.sendKeys(email)
}
async enterPassword(password: string): Promise<void> {
const passwordField = await this.driver.findElement(this.locators.passwordInput)
await passwordField.clear()
await passwordField.sendKeys(password)
}
async clickSubmit(): Promise<void> {
const submitBtn = await this.driver.findElement(this.locators.submitButton)
await submitBtn.click()
}
async login(email: string, password: string): Promise<void> {
await this.enterEmail(email)
await this.enterPassword(password)
await this.clickSubmit()
}
// Assertions/Verifications
async getErrorMessage(): Promise<string> {
await this.driver.wait(
until.elementLocated(this.locators.errorMessage),
5000
)
const errorElement = await this.driver.findElement(this.locators.errorMessage)
return await errorElement.getText()
}
async isLoginSuccessful(): Promise<boolean> {
try {
await this.driver.wait(
until.elementLocated(this.locators.successMessage),
5000
)
return true
} catch {
return false
}
}
async getCurrentUrl(): Promise<string> {
return await this.driver.getCurrentUrl()
}
}Advanced Page Object with Base Page
// pages/BasePage.ts
import { WebDriver, By, until, WebElement } from 'selenium-webdriver'
export abstract class BasePage {
protected driver: WebDriver
protected url: string = ''
constructor(driver: WebDriver) {
this.driver = driver
}
async navigate(): Promise<void> {
await this.driver.get(this.url)
}
async getCurrentUrl(): Promise<string> {
return await this.driver.getCurrentUrl()
}
async getTitle(): Promise<string> {
return await this.driver.getTitle()
}
protected async findElement(locator: By): Promise<WebElement> {
await this.driver.wait(until.elementLocated(locator), 10000)
return await this.driver.findElement(locator)
}
protected async findElements(locator: By): Promise<WebElement[]> {
return await this.driver.findElements(locator)
}
protected async click(locator: By): Promise<void> {
const element = await this.findElement(locator)
await this.driver.wait(until.elementIsVisible(element), 5000)
await element.click()
}
protected async type(locator: By, text: string): Promise<void> {
const element = await this.findElement(locator)
await element.clear()
await element.sendKeys(text)
}
protected async getText(locator: By): Promise<string> {
const element = await this.findElement(locator)
return await element.getText()
}
protected async isDisplayed(locator: By): Promise<boolean> {
try {
const element = await this.findElement(locator)
return await element.isDisplayed()
} catch {
return false
}
}
protected async waitForElement(locator: By, timeout: number = 10000): Promise<void> {
await this.driver.wait(until.elementLocated(locator), timeout)
}
protected async waitForElementVisible(locator: By, timeout: number = 10000): Promise<void> {
const element = await this.findElement(locator)
await this.driver.wait(until.elementIsVisible(element), timeout)
}
}// pages/DashboardPage.ts
import { By } from 'selenium-webdriver'
import { BasePage } from './BasePage'
export class DashboardPage extends BasePage {
protected url = 'https://example.com/dashboard'
private locators = {
welcomeMessage: By.id('welcome'),
logoutButton: By.id('logout'),
userMenu: By.className('user-menu'),
notificationBadge: By.className('notification-badge'),
}
async getWelcomeMessage(): Promise<string> {
return await this.getText(this.locators.welcomeMessage)
}
async logout(): Promise<void> {
await this.click(this.locators.logoutButton)
}
async openUserMenu(): Promise<void> {
await this.click(this.locators.userMenu)
}
async getNotificationCount(): Promise<number> {
const badge = await this.findElement(this.locators.notificationBadge)
const text = await badge.getText()
return parseInt(text, 10)
}
}Full POM Test Example
// tests/login.test.ts
import { describe, it, beforeEach, afterEach } from 'vitest'
import { Builder, Browser, WebDriver } from 'selenium-webdriver'
import { LoginPage } from '../pages/LoginPage'
import { DashboardPage } from '../pages/DashboardPage'
describe('Login Flow', () => {
let driver: WebDriver
let loginPage: LoginPage
let dashboardPage: DashboardPage
beforeEach(async () => {
driver = await new Builder().forBrowser(Browser.CHROME).build()
loginPage = new LoginPage(driver)
dashboardPage = new DashboardPage(driver)
})
afterEach(async () => {
await driver.quit()
})
it('should login successfully with valid credentials', async () => {
await loginPage.navigate()
await loginPage.login('user@example.com', 'password123')
// Verify redirect to dashboard
await dashboardPage.waitForElement(By.id('welcome'), 10000)
const welcomeMessage = await dashboardPage.getWelcomeMessage()
expect(welcomeMessage).toContain('Welcome')
const currentUrl = await dashboardPage.getCurrentUrl()
expect(currentUrl).toContain('/dashboard')
})
it('should show error message with invalid credentials', async () => {
await loginPage.navigate()
await loginPage.login('invalid@example.com', 'wrongpassword')
const errorMessage = await loginPage.getErrorMessage()
expect(errorMessage).toBe('Invalid email or password')
})
it('should logout successfully', async () => {
// First login
await loginPage.navigate()
await loginPage.login('user@example.com', 'password123')
// Then logout
await dashboardPage.logout()
// Verify redirect to login
const currentUrl = await driver.getCurrentUrl()
expect(currentUrl).toContain('/login')
})
})Selenium Grid
Selenium Grid allows you to run tests in parallel across multiple machines and browsers.
Grid Architecture
┌──────────────────┐
│ Grid Hub │
│ (Coordinator) │
└────────┬─────────┘
│
┌────┴────┬─────────┬─────────┐
│ │ │ │
┌───▼───┐ ┌──▼───┐ ┌───▼───┐ ┌───▼───┐
│ Node 1│ │Node 2│ │ Node 3│ │ Node 4│
│Chrome │ │Firefox│ │ Edge │ │Safari │
│Linux │ │Windows│ │Windows│ │ macOS │
└───────┘ └──────┘ └───────┘ └───────┘Starting Grid with Docker
# docker-compose.yml
version: '3'
services:
selenium-hub:
image: selenium/hub:latest
ports:
- "4444:4444"
environment:
- GRID_MAX_SESSION=10
- GRID_BROWSER_TIMEOUT=300
- GRID_TIMEOUT=300
chrome:
image: selenium/node-chrome:latest
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
shm_size: 2gb
firefox:
image: selenium/node-firefox:latest
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
shm_size: 2gb
edge:
image: selenium/node-edge:latest
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
shm_size: 2gb# Start grid
docker-compose up -d
# View grid console
# Navigate to http://localhost:4444Connecting to Grid
import { Builder, Browser } from 'selenium-webdriver'
async function runOnGrid() {
const driver = await new Builder()
.forBrowser(Browser.CHROME)
.usingServer('http://localhost:4444/wd/hub')
.build()
try {
await driver.get('https://example.com')
// Test code...
} finally {
await driver.quit()
}
}Parallel Testing with Grid
import { describe, it } from 'vitest'
import { Builder, Browser, WebDriver } from 'selenium-webdriver'
const testData = [
{ browser: Browser.CHROME, url: 'https://example1.com' },
{ browser: Browser.FIREFOX, url: 'https://example2.com' },
{ browser: Browser.EDGE, url: 'https://example3.com' },
]
describe('Parallel Grid Tests', () => {
testData.forEach(({ browser, url }) => {
it(`should work on ${browser}`, async () => {
const driver = await new Builder()
.forBrowser(browser)
.usingServer('http://localhost:4444/wd/hub')
.build()
try {
await driver.get(url)
const title = await driver.getTitle()
expect(title).toBeTruthy()
} finally {
await driver.quit()
}
})
})
})Selenium Best Practices
1. Use Explicit Waits
// ❌ Bad - Thread.sleep (avoid in Selenium)
await driver.sleep(5000)
// ✅ Good - Explicit wait
await driver.wait(until.elementLocated(By.id('element')), 10000)2. Create Reusable Methods
// ✅ Good - Reusable helper methods
async function waitAndClick(driver: WebDriver, locator: By): Promise<void> {
const element = await driver.wait(until.elementLocated(locator), 10000)
await driver.wait(until.elementIsVisible(element), 5000)
await element.click()
}3. Use Page Object Model
// ✅ Good - Separation of concerns
const loginPage = new LoginPage(driver)
await loginPage.login(email, password)
// ❌ Bad - Direct WebDriver calls in tests
await driver.findElement(By.id('email')).sendKeys(email)
await driver.findElement(By.id('password')).sendKeys(password)4. Close Resources Properly
// ✅ Good - Always quit driver
afterEach(async () => {
if (driver) {
await driver.quit()
}
})5. Take Screenshots on Failure
import fs from 'fs'
async function takeScreenshot(driver: WebDriver, filename: string) {
const screenshot = await driver.takeScreenshot()
fs.writeFileSync(filename, screenshot, 'base64')
}
// In test
it('test case', async () => {
try {
// Test code
} catch (error) {
await takeScreenshot(driver, `failure-${Date.now()}.png`)
throw error
}
})6. Avoid Absolute XPaths
// ❌ Bad
By.xpath('/html/body/div[1]/div[2]/form/input[3]')
// ✅ Good
By.xpath('//input[@id="email"]')
By.css('#email')7. Use Data-Driven Testing
const testCases = [
{ email: 'user1@test.com', password: 'pass1', shouldSucceed: true },
{ email: 'user2@test.com', password: 'pass2', shouldSucceed: false },
]
testCases.forEach(({ email, password, shouldSucceed }) => {
it(`should ${shouldSucceed ? 'login' : 'fail'} with ${email}`, async () => {
await loginPage.login(email, password)
if (shouldSucceed) {
expect(await loginPage.isLoginSuccessful()).toBe(true)
} else {
expect(await loginPage.getErrorMessage()).toBeTruthy()
}
})
})Common Pitfalls
1. StaleElementReferenceException
// ❌ Problem - Element reference becomes stale after page refresh
const element = await driver.findElement(By.id('button'))
await driver.navigate().refresh()
await element.click() // Throws StaleElementReferenceException
// ✅ Solution - Re-find element
await driver.navigate().refresh()
const element = await driver.findElement(By.id('button'))
await element.click()2. NoSuchElementException
// ❌ Problem - Element not found
const element = await driver.findElement(By.id('nonexistent'))
// ✅ Solution - Use waits
await driver.wait(until.elementLocated(By.id('element')), 10000)
const element = await driver.findElement(By.id('element'))3. ElementNotInteractableException
// ❌ Problem - Element not visible/enabled
const button = await driver.findElement(By.id('button'))
await button.click() // Fails if not visible
// ✅ Solution - Wait for element to be interactable
const button = await driver.findElement(By.id('button'))
await driver.wait(until.elementIsVisible(button), 5000)
await driver.wait(until.elementIsEnabled(button), 5000)
await button.click()Playwright
Playwright is a modern, fast, and reliable browser automation framework with auto-waiting and powerful debugging tools.
Installation and Setup
# Install Playwright
npm install -D @playwright/test
# Install browsers
npx playwright install
# Install specific browser
npx playwright install chromiumConfiguration
playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results.json' }],
['junit', { outputFile: 'junit.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})Browser Automation
Basic Test
import { test, expect } from '@playwright/test'
test('basic navigation', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example Domain/)
await expect(page).toHaveURL(/example.com/)
})
test('clicking and navigation', async ({ page }) => {
await page.goto('https://example.com')
await page.click('text=More information')
await expect(page).toHaveURL(/iana.org/)
})Locators
test('playwright locators', async ({ page }) => {
await page.goto('https://example.com')
// By role (preferred)
await page.getByRole('button', { name: 'Submit' }).click()
await page.getByRole('link', { name: /learn more/i }).click()
// By label
await page.getByLabel('Email').fill('user@example.com')
// By placeholder
await page.getByPlaceholder('Enter your name').fill('John')
// By text
await page.getByText('Welcome back').click()
// By test ID
await page.getByTestId('submit-button').click()
// CSS selector
await page.locator('#email').fill('test@example.com')
await page.locator('.btn-primary').click()
// XPath
await page.locator('xpath=//button[@type="submit"]').click()
})Auto-Waiting Mechanism
Playwright automatically waits for elements to be ready before performing actions.
test('auto-waiting', async ({ page }) => {
await page.goto('https://example.com')
// Playwright automatically waits for:
// - Element to be attached to DOM
// - Element to be visible
// - Element to be stable (not animating)
// - Element to receive events (not obscured)
// - Element to be enabled
await page.click('button') // Waits automatically
await page.fill('input', 'text') // Waits automatically
// Custom wait if needed
await page.waitForSelector('#dynamic-element')
await page.waitForLoadState('networkidle')
})Multiple Browser Support
import { test, chromium, firefox, webkit } from '@playwright/test'
test('cross-browser testing', async () => {
// Chrome
const chrome = await chromium.launch()
const chromePage = await chrome.newPage()
await chromePage.goto('https://example.com')
await chrome.close()
// Firefox
const ff = await firefox.launch()
const ffPage = await ff.newPage()
await ffPage.goto('https://example.com')
await ff.close()
// WebKit (Safari)
const wk = await webkit.launch()
const wkPage = await wk.newPage()
await wkPage.goto('https://example.com')
await wk.close()
})Mobile Emulation
import { test, devices } from '@playwright/test'
test('mobile emulation', async ({ browser }) => {
const iphone = devices['iPhone 13']
const context = await browser.newContext({
...iphone,
})
const page = await context.newPage()
await page.goto('https://example.com')
// Interactions on mobile viewport
await page.getByRole('button', { name: 'Menu' }).click()
await context.close()
})
test('custom viewport', async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 375, height: 667 },
userAgent: 'Custom User Agent',
geolocation: { latitude: 37.7749, longitude: -122.4194 },
permissions: ['geolocation'],
})
const page = await context.newPage()
await page.goto('https://example.com')
await context.close()
})Network Interception
test('network mocking', async ({ page }) => {
// Mock API response
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]),
})
})
await page.goto('https://example.com/users')
// Verify mocked data is displayed
await expect(page.getByText('John Doe')).toBeVisible()
})
test('abort requests', async ({ page }) => {
// Block images and stylesheets
await page.route('**/*.{png,jpg,jpeg,css}', route => route.abort())
await page.goto('https://example.com')
})
test('modify requests', async ({ page }) => {
await page.route('**/api/**', route => {
const headers = {
...route.request().headers(),
'Authorization': 'Bearer mock-token',
}
route.continue({ headers })
})
await page.goto('https://example.com')
})Screenshots and Videos
test('screenshots', async ({ page }) => {
await page.goto('https://example.com')
// Full page screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true })
// Element screenshot
const element = page.getByRole('heading')
await element.screenshot({ path: 'heading.png' })
// Screenshot to buffer
const buffer = await page.screenshot()
})
test('videos', async ({ browser }) => {
const context = await browser.newContext({
recordVideo: {
dir: 'videos/',
size: { width: 1280, height: 720 },
},
})
const page = await context.newPage()
await page.goto('https://example.com')
// Test actions...
await context.close()
// Video is saved after context closes
})Parallel Testing
import { test } from '@playwright/test'
test.describe.configure({ mode: 'parallel' })
test.describe('Parallel tests', () => {
test('test 1', async ({ page }) => {
// Runs in parallel
})
test('test 2', async ({ page }) => {
// Runs in parallel
})
test('test 3', async ({ page }) => {
// Runs in parallel
})
})
// Serial tests
test.describe.configure({ mode: 'serial' })
test.describe('Serial tests', () => {
test('test 1', async ({ page }) => {
// Runs first
})
test('test 2', async ({ page }) => {
// Runs second
})
})Debugging Tools
Playwright Inspector
# Run with inspector
PWDEBUG=1 npx playwright test
# Headed mode with slow mo
npx playwright test --headed --slow-mo=1000Trace Viewer
test('with tracing', async ({ page }) => {
await page.context().tracing.start({ screenshots: true, snapshots: true })
await page.goto('https://example.com')
// Test actions...
await page.context().tracing.stop({ path: 'trace.zip' })
})# View trace
npx playwright show-trace trace.zipCypress
Cypress is a developer-friendly E2E testing framework with excellent DX and time-travel debugging.
Setup and Configuration
# Install Cypress
npm install -D cypress
# Open Cypress
npx cypress openConfiguration
cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
},
})Writing E2E Tests
describe('Login Flow', () => {
beforeEach(() => {
cy.visit('/login')
})
it('should login successfully', () => {
cy.get('[data-testid="email-input"]').type('user@example.com')
cy.get('[data-testid="password-input"]').type('password123')
cy.get('button[type="submit"]').click()
cy.url().should('include', '/dashboard')
cy.contains('Welcome').should('be.visible')
})
it('should show error for invalid credentials', () => {
cy.get('[data-testid="email-input"]').type('invalid@example.com')
cy.get('[data-testid="password-input"]').type('wrongpassword')
cy.get('button[type="submit"]').click()
cy.contains('Invalid credentials').should('be.visible')
cy.url().should('include', '/login')
})
})Component Testing
import { mount } from 'cypress/react18'
import Counter from './Counter'
describe('Counter Component', () => {
it('renders and increments', () => {
mount(<Counter initialValue={0} />)
cy.contains('Count: 0').should('be.visible')
cy.get('button').contains('Increment').click()
cy.contains('Count: 1').should('be.visible')
})
})API Testing
describe('API Tests', () => {
it('should fetch users', () => {
cy.request('GET', '/api/users')
.its('status')
.should('equal', 200)
cy.request('/api/users').then(response => {
expect(response.body).to.have.length.greaterThan(0)
expect(response.body[0]).to.have.property('name')
})
})
it('should create user', () => {
cy.request({
method: 'POST',
url: '/api/users',
body: {
name: 'John Doe',
email: 'john@example.com',
},
}).then(response => {
expect(response.status).to.equal(201)
expect(response.body).to.have.property('id')
})
})
})Custom Commands
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>
logout(): Chainable<void>
}
}
}
Cypress.Commands.add('login', (email: string, password: string) => {
cy.visit('/login')
cy.get('[data-testid="email-input"]').type(email)
cy.get('[data-testid="password-input"]').type(password)
cy.get('button[type="submit"]').click()
cy.url().should('include', '/dashboard')
})
Cypress.Commands.add('logout', () => {
cy.get('[data-testid="user-menu"]').click()
cy.get('[data-testid="logout-button"]').click()
cy.url().should('include', '/login')
})
// Usage in tests
it('should work with custom commands', () => {
cy.login('user@example.com', 'password123')
cy.logout()
})Best Practices
1. Use data-testid for Selectors
// ✅ Good
cy.get('[data-testid="submit-button"]')
// ❌ Bad (fragile)
cy.get('.btn.btn-primary.submit')2. Avoid Hard-Coded Waits
// ❌ Bad
cy.wait(5000)
// ✅ Good
cy.get('[data-testid="loading"]').should('not.exist')
cy.get('[data-testid="data"]').should('be.visible')3. Use Aliases
cy.get('[data-testid="user-list"]').as('userList')
cy.get('@userList').find('li').should('have.length', 5)For more E2E testing strategies and best practices, see Best Practices and CI/CD Integration.