Generating k6 Browser Tests
Generate browser-based E2E performance tests using the k6/browser module. Tests real browser interactions with Chromium, measures Web Vitals, and supports hybrid protocol+browser testing.
Quick Start
import { browser } from 'k6/browser';
import { check } from 'k6';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
iterations: 5,
vus: 1,
options: {
browser: {
type: 'chromium',
},
},
},
},
thresholds: {
browser_web_vital_lcp: ['p(90)<2500'],
browser_web_vital_fcp: ['p(90)<1800'],
browser_web_vital_cls: ['p(95)<0.1'],
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');
const title = await page.title();
check(null, { 'page title loaded': () => title !== '' });
await page.screenshot({ path: 'screenshot.png' });
} finally {
await page.close(); // Always close pages
}
}
Key Concepts
- All browser functions are
async— useawaitandasync function - Always close pages — use
try/finallyto ensurepage.close()is called - Browser type — only
'chromium'is supported - Browser scenario — requires
options.browser.typein scenario config - Avoid
networkidle—waitForLoadState('networkidle')may never fire on chatty pages; prefer'load'orlocator.waitFor()when possible - Resource intensive — browser VUs consume more resources; start with few VUs
Element Selection
Locator API (Recommended)
// CSS selector
const btn = page.locator('button.submit');
// Semantic selectors (preferred for resilience)
const submitBtn = page.getByRole('button', { name: 'Submit' });
const email = page.getByLabel('Email');
const search = page.getByPlaceholder('Search...');
const heading = page.getByText('Welcome');
const card = page.getByTestId('user-card');
Common Interactions
// Click
await page.locator('button').click();
// Fill text input
await page.getByLabel('Username').fill('testuser');
// Select dropdown option
await page.locator('select#country').selectOption('US');
// Checkbox
await page.getByRole('checkbox', { name: 'Terms' }).check();
// Type character by character (with key events)
await page.locator('#search').type('search query', { delay: 50 });
// Press key
await page.locator('#search').press('Enter');
// Hover
await page.locator('.menu-item').hover();
Query Element State
const isVisible = await page.locator('.modal').isVisible();
const isEnabled = await page.locator('button').isEnabled();
const text = await page.locator('.message').textContent();
const value = await page.locator('input').inputValue();
const count = await page.locator('li.item').count();
Web Vitals
k6/browser automatically collects Core Web Vitals:
| Metric | Name | Good Threshold |
|---|---|---|
browser_web_vital_lcp |
Largest Contentful Paint | < 2500ms |
browser_web_vital_fcp |
First Contentful Paint | < 1800ms |
browser_web_vital_cls |
Cumulative Layout Shift | < 0.1 |
browser_web_vital_inp |
Interaction to Next Paint | < 200ms |
browser_web_vital_ttfb |
Time to First Byte | < 600ms |
export const options = {
thresholds: {
'browser_web_vital_lcp': ['p(90)<2500'],
'browser_web_vital_fcp': ['p(90)<1800'],
'browser_web_vital_cls': ['p(95)<0.1'],
'browser_web_vital_inp': ['p(90)<200'],
},
};
Common Test Patterns
Login Flow
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForNavigation();
check(null, {
'redirected to dashboard': () => page.url().includes('/dashboard'),
});
} finally {
await page.close();
}
}
Form Submission
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://app.example.com/contact');
await page.getByLabel('Name').fill('Load Test User');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Message').fill('Performance test submission');
await page.locator('select#department').selectOption('support');
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: 'Submit' }).click();
await page.waitForLoadState('networkidle');
const confirmation = await page.locator('.success-message').textContent();
check(null, {
'form submitted': () => confirmation.includes('Thank you'),
});
} finally {
await page.close();
}
}
Page Navigation with Screenshots
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://app.example.com');
await page.screenshot({ path: 'home.png', fullPage: true });
await page.locator('a[href="/products"]').click();
await page.waitForNavigation();
await page.screenshot({ path: 'products.png' });
await page.locator('.product-card').first().click();
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'product-detail.png' });
} finally {
await page.close();
}
}
Hybrid Testing (Browser + Protocol)
Combine browser tests with protocol-level load tests:
import { browser } from 'k6/browser';
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
// High-volume API load
api_load: {
exec: 'apiTest',
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
},
// Low-volume browser tests (resource intensive)
browser_test: {
exec: 'browserTest',
executor: 'constant-vus',
vus: 2,
duration: '5m',
options: { browser: { type: 'chromium' } },
},
},
};
export function apiTest() {
const res = http.get('https://api.example.com/products');
check(res, { 'API 200': (r) => r.status === 200 });
}
export async function browserTest() {
const page = await browser.newPage();
try {
await page.goto('https://app.example.com');
// Browser interactions...
} finally {
await page.close();
}
}
Advanced Features
For detailed API reference including BrowserContext, Keyboard, Mouse, Touchscreen, and advanced patterns:
See reference/browser-api.md — Complete Page, Locator, BrowserContext API with all methods and parameters
See reference/web-vitals.md — Web Vitals metrics explanation, threshold guidance, and custom performance measurement
Related Skills
- For protocol-level load tests (HTTP, gRPC, WebSocket):
/k6:generating-api-load-tests - For scenario and executor configuration:
/k6:designing-test-scenarios - For interpreting test results:
/k6:analyzing-test-results