| name | generating-browser-tests |
| description | Use when creating k6 browser-based performance tests, testing frontend performance, measuring Web Vitals (LCP, FCP, CLS, INP, TTFB), or automating browser interactions for load testing. Use when the user mentions browser testing, page load testing, E2E performance, or Chromium-based testing with k6. |
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();
}
}
Key Concepts
- All browser functions are
async — use await and async function
- Always close pages — use
try/finally to ensure page.close() is called
- Browser type — only
'chromium' is supported
- Browser scenario — requires
options.browser.type in scenario config
- Avoid
networkidle — waitForLoadState('networkidle') may never fire on chatty pages; prefer 'load' or locator.waitFor() when possible
- Resource intensive — browser VUs consume more resources; start with few VUs
Element Selection
Locator API (Recommended)
const btn = page.locator('button.submit');
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
await page.locator('button').click();
await page.getByLabel('Username').fill('testuser');
await page.locator('select#country').selectOption('US');
await page.getByRole('checkbox', { name: 'Terms' }).check();
await page.locator('#search').type('search query', { delay: 50 });
await page.locator('#search').press('Enter');
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: {
api_load: {
exec: 'apiTest',
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
},
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');
} 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