From k6
Generates k6 browser-based E2E performance tests using Chromium to measure Web Vitals (LCP, FCP, CLS, INP, TTFB) and automate frontend interactions for load testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/k6:generating-browser-testsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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
}
}
async — use await and async functiontry/finally to ensure page.close() is called'chromium' is supportedoptions.browser.type in scenario confignetworkidle — waitForLoadState('networkidle') may never fire on chatty pages; prefer 'load' or locator.waitFor() when possible// 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');
// 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();
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();
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'],
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill('[email protected]');
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();
}
}
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('[email protected]');
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();
}
}
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();
}
}
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();
}
}
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
/k6:generating-api-load-tests/k6:designing-test-scenarios/k6:analyzing-test-resultsnpx claudepluginhub kimdoubleb/grafana-k6-skills --plugin k6Writes and executes k6 load tests for HTTP APIs, WebSocket endpoints, and browser scenarios. Configures smoke, load, stress, spike, and soak tests with thresholds, stages, and CI/CD integration.
End-to-end performance, load, and stress testing of public websites with k6. Produces hybrid protocol+browser test suites, SLO-backed thresholds, and monitoring.
Guides creation of Browser Library tests using Playwright-powered automation for web testing, covering locators, auto-waiting, assertions, iframes, Shadow DOM, and multi-tabs.