The k6 Browser module doesn’t just simulate requests; it orchestrates actual browser instances to perform load tests.
Let’s see it in action. Imagine you want to test the login flow of a web application.
import { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'per-vu',
vus: 10,
duration: '1m',
},
},
thresholds: {
// http_req_failed: ['rate==0'], // Error rate
// browser_action_duration: ['p(95)<3000'], // 95% of actions should complete within 3 seconds
},
};
export default function () {
const page = browser.newPage();
try {
// Navigate to the login page
page.goto('https://your-app.com/login');
// Fill in username and password
page.locator('input[name="username"]').fill('testuser');
page.locator('input[name="password"]').fill('password123');
// Click the login button
page.locator('button[type="submit"]').click();
// Wait for the dashboard or a success indicator to load
page.waitForNavigation({ waitUntil: 'domcontentloaded' });
page.screenshot({ path: `screenshot-${__VU}-${__ITER}.png` }); // Take a screenshot for debugging
console.log(`VU ${__VU} successfully logged in.`);
} catch (e) {
console.error(`VU ${__VU} failed: ${e}`);
page.screenshot({ path: `error-screenshot-${__VU}-${__ITER}.png` }); // Screenshot on error
} finally {
page.close();
}
}
This script opens a new browser context for each virtual user (vu), navigates to a login page, types credentials into specific input fields, and clicks a submit button. It then waits for the page to load and optionally takes a screenshot. The browser.newPage() call is key here; it’s not just a network request, but the instantiation of a headless Chromium browser.
The problem k6 Browser solves is the gap between protocol-level testing (like with the standard k6 HTTP module) and end-to-end user experience. Protocol tests are fast and efficient, but they don’t account for JavaScript execution, rendering time, or the nuances of how a real user interacts with a page. The Browser module bridges this by running tests in actual browsers, giving you metrics on what your users actually experience: page load times, interaction speeds, and visual rendering.
Internally, k6 uses Playwright under the hood. When you run browser.newPage(), k6 is launching a Playwright browser instance. The page object you interact with is a Playwright Page object. This means you have access to a rich set of browser automation capabilities: locating elements by CSS selectors, XPath, or text, filling forms, clicking buttons, waiting for specific network requests or DOM states, taking screenshots, and even recording network activity. The browser.close() call ensures that the browser instance is properly shut down, freeing up resources.
The waitForNavigation({ waitUntil: 'domcontentloaded' }) is a critical control point. domcontentloaded means k6 will wait until the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. If you need to ensure all assets are loaded before considering a step complete, you might use 'load', or even 'networkidle' which waits until there are no more than 0 network connections for at least 500 ms. The choice here directly impacts how you measure "page load complete."
When you run k6 run your_script.js, k6 doesn’t just send HTTP requests; it launches multiple instances of a headless browser. Each instance navigates, interacts, and renders the page just like a real user would. The metrics collected — browser_action_duration, browser_page_load_time, browser_dom_content_loaded — are derived from the browser’s internal timings. This allows you to test complex client-side logic, single-page applications (SPAs), and user journeys that involve multiple steps and dynamic content.
The page.locator() method, combined with .fill(), .click(), and .waitForNavigation(), forms the core of interaction scripting. You can chain these methods to build complex user flows. For instance, to simulate typing into a search box and then hitting Enter: page.locator('#search-input').fill('k6 browser testing').press('Enter');. The press() method allows simulating keyboard actions, which is crucial for testing keyboard-driven interfaces or shortcuts.
The most surprising thing about k6 Browser is how it handles resource management and parallelization. While it launches full browser instances, it’s still designed for load testing. The per-vu executor, for example, ensures that each virtual user gets its own isolated browser environment. k6 manages the lifecycle of these browser instances, starting them up, performing the script’s actions, and tearing them down efficiently. This is a significant engineering feat, as managing hundreds or thousands of browser instances concurrently requires careful handling of memory, CPU, and inter-process communication, all while collecting accurate performance metrics.
The next step is to explore how to handle dynamic content and waiting for specific elements to appear before proceeding with an action, which is crucial for testing modern, JavaScript-heavy applications.