Skip to main content

UI Testing Web

Comprehensive strategies for testing web user interfaces to ensure quality and reliability.

Overview

UI testing for web applications verifies that the user interface works correctly and provides a good user experience. This includes functional testing, visual regression testing, accessibility testing, and performance testing.

Types of UI Testing

1. Unit Testing

Testing individual components in isolation.

import { render, screen } from '@testing-library/react';
import Button from './Button';

test('button renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

2. Integration Testing

Testing how components work together.

import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('login form submission', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);

fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'user@example.com' },
});

fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'password123' },
});

fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
});

3. End-to-End Testing

Testing complete user flows through the application.

E2E Testing Tools

Cypress

Modern end-to-end testing framework.

Installation:

npm install --save-dev cypress

Basic Test:

describe('Login Flow', () => {
beforeEach(() => {
cy.visit('/login');
});

it('should login successfully with valid credentials', () => {
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();

cy.url().should('include', '/dashboard');
cy.contains('Welcome back').should('be.visible');
});

it('should show error with invalid credentials', () => {
cy.get('input[name="email"]').type('wrong@example.com');
cy.get('input[name="password"]').type('wrongpass');
cy.get('button[type="submit"]').click();

cy.contains('Invalid credentials').should('be.visible');
cy.url().should('include', '/login');
});

it('should validate required fields', () => {
cy.get('button[type="submit"]').click();

cy.contains('Email is required').should('be.visible');
cy.contains('Password is required').should('be.visible');
});
});

Custom Commands:

// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
});

// Usage
cy.login('user@example.com', 'password123');

Playwright

Cross-browser automation framework.

Installation:

npm install --save-dev @playwright/test

Basic Test:

import { test, expect } from '@playwright/test';

test.describe('Shopping Cart', () => {
test('add item to cart', async ({ page }) => {
await page.goto('/products');

await page.click('text=Add to Cart');

await page.click('[aria-label="View cart"]');

await expect(page.locator('.cart-item')).toHaveCount(1);
await expect(page.locator('.cart-total')).toContainText('$29.99');
});

test('remove item from cart', async ({ page }) => {
await page.goto('/cart');

await page.click('[aria-label="Remove item"]');

await expect(page.locator('.cart-empty')).toBeVisible();
await expect(page.locator('.cart-item')).toHaveCount(0);
});
});

Multiple Browser Testing:

import { test, devices } from '@playwright/test';

test.use({
...devices['iPhone 12'],
});

test('mobile responsive layout', async ({ page }) => {
await page.goto('/');
await expect(page.locator('.mobile-menu')).toBeVisible();
});

Puppeteer

Headless Chrome automation.

Installation:

npm install --save-dev puppeteer

Basic Test:

const puppeteer = require('puppeteer');

describe('Search functionality', () => {
let browser;
let page;

beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
});

afterAll(async () => {
await browser.close();
});

test('performs search', async () => {
await page.goto('http://localhost:3000');

await page.type('#search-input', 'test query');
await page.click('#search-button');

await page.waitForSelector('.search-results');

const results = await page.$$('.result-item');
expect(results.length).toBeGreaterThan(0);
});
});

Visual Regression Testing

Percy

Visual testing and review platform.

Installation:

npm install --save-dev @percy/cli @percy/cypress

Cypress Integration:

import '@percy/cypress';

describe('Visual Tests', () => {
it('homepage looks correct', () => {
cy.visit('/');
cy.percySnapshot('Homepage');
});

it('product page looks correct', () => {
cy.visit('/products/1');
cy.percySnapshot('Product Page');
});
});

Chromatic

Visual testing for Storybook.

Installation:

npm install --save-dev chromatic

Usage:

npx chromatic --project-token=<token>

BackstopJS

Visual regression testing tool.

Installation:

npm install --save-dev backstopjs

Configuration:

// backstop.json
{
"id": "my_app_test",
"viewports": [
{
"label": "phone",
"width": 320,
"height": 480
},
{
"label": "tablet",
"width": 1024,
"height": 768
}
],
"scenarios": [
{
"label": "Homepage",
"url": "http://localhost:3000",
"selectors": ["document"],
"delay": 500
}
]
}

Accessibility Testing

axe-core

Automated accessibility testing.

Installation:

npm install --save-dev @axe-core/react

Integration:

import React from 'react';

if (process.env.NODE_ENV !== 'production') {
const axe = require('@axe-core/react');
axe(React, ReactDOM, 1000);
}

Cypress Plugin:

import 'cypress-axe';

describe('Accessibility tests', () => {
it('has no accessibility violations', () => {
cy.visit('/');
cy.injectAxe();
cy.checkA11y();
});

it('checks specific component', () => {
cy.visit('/form');
cy.injectAxe();
cy.checkA11y('.contact-form');
});
});

Pa11y

Automated accessibility testing tool.

Installation:

npm install --save-dev pa11y

Usage:

const pa11y = require('pa11y');

test('page is accessible', async () => {
const results = await pa11y('http://localhost:3000');

expect(results.issues).toHaveLength(0);
});

Performance Testing

Lighthouse CI

Automated performance testing.

Installation:

npm install --save-dev @lhci/cli

Configuration:

// lighthouserc.js
module.exports = {
ci: {
collect: {
startServerCommand: 'npm run serve',
url: ['http://localhost:3000'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.9 }],
},
},
},
};

Web Vitals Testing

import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

describe('Performance metrics', () => {
test('LCP is under 2.5s', async () => {
const lcp = await new Promise(resolve => {
getLCP(resolve);
});

expect(lcp.value).toBeLessThan(2500);
});

test('FID is under 100ms', async () => {
const fid = await new Promise(resolve => {
getFID(resolve);
});

expect(fid.value).toBeLessThan(100);
});
});

Testing Best Practices

1. Use Page Object Model

// pages/LoginPage.js
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error-message');
}

async goto() {
await this.page.goto('/login');
}

async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}

async getErrorMessage() {
return await this.errorMessage.textContent();
}
}

// Usage in test
import { LoginPage } from './pages/LoginPage';

test('login with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);

await loginPage.goto();
await loginPage.login('invalid@example.com', 'wrongpass');

const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});

2. Use Test Data Factories

// factories/userFactory.js
export const createUser = (overrides = {}) => ({
id: Math.random().toString(36).substr(2, 9),
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides,
});

// Usage
test('displays user profile', () => {
const user = createUser({ name: 'John Doe' });
render(<UserProfile user={user} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});

3. Clean Up Between Tests

beforeEach(() => {
cy.clearCookies();
cy.clearLocalStorage();
cy.visit('/');
});

afterEach(() => {
// Clean up any test data
cy.task('cleanDatabase');
});

4. Use Data Attributes for Testing

// Component
<button data-testid="submit-button" onClick={handleSubmit}>
Submit
</button>

// Test
cy.get('[data-testid="submit-button"]').click();

5. Test User Journeys

describe('Complete purchase flow', () => {
it('user can complete a purchase', () => {
// Browse products
cy.visit('/products');
cy.contains('Running Shoes').click();

// Add to cart
cy.get('[data-testid="add-to-cart"]').click();
cy.contains('Added to cart').should('be.visible');

// Go to cart
cy.get('[aria-label="Cart"]').click();
cy.url().should('include', '/cart');

// Proceed to checkout
cy.contains('Proceed to Checkout').click();

// Fill shipping info
cy.get('input[name="name"]').type('John Doe');
cy.get('input[name="address"]').type('123 Main St');
cy.contains('Continue').click();

// Fill payment info
cy.get('input[name="cardNumber"]').type('4242424242424242');
cy.get('input[name="expiry"]').type('12/25');
cy.get('input[name="cvc"]').type('123');

// Complete order
cy.contains('Place Order').click();

// Verify success
cy.contains('Order confirmed').should('be.visible');
cy.url().should('include', '/order-confirmation');
});
});

Cross-Browser Testing

BrowserStack

Cloud-based testing platform.

// wdio.conf.js
exports.config = {
user: process.env.BROWSERSTACK_USERNAME,
key: process.env.BROWSERSTACK_ACCESS_KEY,
services: ['browserstack'],
capabilities: [
{
browserName: 'Chrome',
'bstack:options': {
os: 'Windows',
osVersion: '10',
},
},
{
browserName: 'Safari',
'bstack:options': {
os: 'OS X',
osVersion: 'Big Sur',
},
},
],
};

Sauce Labs

// playwright.config.js
module.exports = {
use: {
connectOptions: {
wsEndpoint: `wss://ondemand.saucelabs.com:443/wd/hub`,
},
},
projects: [
{
name: 'chrome',
use: { browserName: 'chromium' },
},
{
name: 'firefox',
use: { browserName: 'firefox' },
},
{
name: 'safari',
use: { browserName: 'webkit' },
},
],
};

Continuous Integration

# .github/workflows/test.yml
name: E2E Tests

on: [push, pull_request]

jobs:
cypress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Cypress run
uses: cypress-io/github-action@v4
with:
start: npm start
wait-on: 'http://localhost:3000'
browser: chrome

- name: Upload screenshots
uses: actions/upload-artifact@v2
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots

playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Install dependencies
run: npm ci

- name: Install Playwright
run: npx playwright install --with-deps

- name: Run tests
run: npx playwright test

- name: Upload report
uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: playwright-report/

Test Reporting

Mochawesome

HTML test reporter for Mocha.

// cypress.json
{
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/results",
"overwrite": false,
"html": true,
"json": true
}
}

Allure

Test reporting framework.

npm install --save-dev @wdio/allure-reporter