Browser automation and end-to-end web testing for k6
An extension for k6 adding browser-level APIs with rough Playwright compatibility.
Download ·
Install ·
Documentation ·
Community Forum
xk6-browser is a k6 extension adding support for automation of browsers via the Chrome Devtools Protocol (CDP).
Special acknowledgment to the authors of Playwright and Puppeteer for their trailblazing work in this area. This project is heavily influenced and in some regards based on the code of those projects.
Goals
- Bring browser automation to the k6 testing platform while supporting core k6 features like VU executors, scenarios, metrics, checks, thresholds, logging, DNS remapping, IP blocklists, etc.
- Test stability as the top priority by supporting non-flaky selectors combined with auto-waiting for actions just like Playwright.
- Aim for rough API compatibility with Playwright. The reason for this is two-fold; for one we don't want users to have to learn a completely new API just to use xk6-browser, and secondly, it opens up for using the Playwright RPC server as an optional backend for xk6-browser should we decide to support that in the future.
- Support for Chromium compatible browsers first, and eventually Firefox and WebKit-based browsers.
FAQ
-
Is this production ready?
No, not yet. We're focused on making the extension stable and reliable, as that's our top priority, before adding more features.
-
Is this extension supported in k6 Cloud?
No, not yet. Once the codebase is deemed production ready we'll add support for browser-based testing in k6 Cloud.
-
It doesn't work with my Chromium/Chrome version, why?
CDP evolves and there are differences between different versions of Chromium, sometimes quite subtle. The codebase is continuously tested with the two latest major releases of Google Chrome.
-
Are Firefox or WebKit-based browsers supported?
Not yet. There are differences in CDP coverage between Chromium, Firefox, and WebKit-based browsers. xk6-browser is initially only targetting Chromium-based browsers.
-
Are all features of Playwright supported?
No. Playwright's API is pretty big and some of the functionality only makes sense if they're implemented as async operations: event listening, request interception, waiting for events, etc. As k6 doesn't have a VU event-loop yet, the xk6-browser API is synchronous right now and thus lacks some of the functionality that requires asynchronicity.
Install
Pre-built binaries
The easiest way to install xk6-browser is to grab a pre-built binary from the GitHub Releases page. Once you download and unpack the release, you can optionally copy the xk6-browser binary it contains somewhere in your PATH
, so you are able to run xk6-browser from any location on your system.
Note that you cannot use the plain k6 binary released by the k6 project and must run any scripts that import k6/x/browser
with this separate binary.
Build from source
To build a k6
binary with this extension, first ensure you have the prerequisites:
Then:
- Install
xk6
:
go install go.k6.io/xk6/cmd/xk6@latest
- Build the binary:
xk6 build --output xk6-browser --with github.com/grafana/xk6-browser
This will create a xk6-browser
binary file in the current working directory. This file can be used exactly the same as the main k6
binary, with the addition of being able to run xk6-browser scripts.
-
Run scripts that import k6/x/browser
with the new xk6-browser
binary. On Linux and macOS make sure this is done by referencing the file in the current directory:
./xk6-browser run <script>
Note: You can place it somewhere in your PATH
so that it can be run from anywhere on your system.
Examples
Launch options
import launcher from "k6/x/browser";
export default function() {
const browser = launcher.launch('chromium', {
args: [], // Extra commandline arguments to include when launching browser process
debug: true, // Log all CDP messages to k6 logging subsystem
devtools: true, // Open up developer tools in the browser by default
env: {}, // Environment variables to set before launching browser process
executablePath: null, // Override search for browser executable in favor of specified absolute path
headless: false, // Show browser UI or not
ignoreDefaultArgs: [], // Ignore any of the default arguments included when launching browser process
proxy: {}, // Specify to set browser's proxy config
slowMo: '500ms', // Slow down input actions and navigations by specified time
timeout: '30s', // Default timeout to use for various actions and navigations
});
browser.close();
}
New browser context options
import launcher from "k6/x/browser";
export default function() {
const browser = launcher.launch('chromium');
const context = browser.newContext({
acceptDownloads: false, // Whether to accept downloading of files by default
bypassCSP: false, // Whether to bypass content-security-policy rules
colorScheme: 'light', // Preferred color scheme of browser ('light', 'dark' or 'no-preference')
deviceScaleFactor: 1.0, // Device scaling factor
extraHTTPHeaders: {name: "value"}, // HTTP headers to always include in HTTP requests
geolocation: {latitude: 0.0, longitude: 0.0}, // Geolocation to use
hasTouch: false, // Simulate device with touch or not
httpCredentials: {username: null, password: null}, // Credentials to use if encountering HTTP authentication
ignoreHTTPSErrors: false, // Ignore HTTPS certificate issues
isMobile: false, // Simulate mobile device or not
javaScriptEnabled: true, // Should JavaScript be enabled or not
locale: 'en-US', // The locale to set
offline: false, // Whether to put browser in offline mode or not
permissions: ['midi'], // Permisions to grant by default
reducedMotion: 'no-preference', // Indicate to browser whether it should try to reduce motion/animations
screen: {width: 800, height: 600}, // Set default screen size
timezoneID: '', // Set default timezone to use
userAgent: '', // Set default user-agent string to use
viewport: {width: 800, height: 600},// Set default viewport to use
});
browser.close();
}
Page screenshot
import launcher from "k6/x/browser";
export default function() {
const browser = launcher.launch('chromium', { headless: false });
const context = browser.newContext();
const page = context.newPage();
page.goto('http://whatsmyuseragent.org/');
page.screenshot({ path: `example-chromium.png` });
page.close();
browser.close();
}
Query DOM for element using CSS, XPath or Text based selectors
import launcher from "k6/x/browser";
export default function() {
const browser = launcher.launch('chromium', { headless: false });
const context = browser.newContext();
const page = context.newPage();
page.goto('http://whatsmyuseragent.org/');
// Find element using CSS selector
const ip = page.$('.ip-address p').textContent();
console.log("CSS selector: ", ip);
// Find element using XPath expression
ip = page.$("//div[@class='ip-address']/p").textContent();
console.log("Xpath expression: ", ip);
// Find element using Text search (TODO: support coming soon!)
//ip = page.$("My IP Address").textContent();
//console.log("Text search: ", ip);
page.close();
browser.close();
}
Evaluate JS in browser
import launcher from "k6/x/browser";
export default function() {
const browser = launcher.launch('chromium', { headless: false });
const context = browser.newContext();
const page = context.newPage();
page.goto('http://whatsmyuseragent.org/', { waitUntil: 'load' });
const dimensions = page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio
};
});
console.log(JSON.stringify(dimensions));
page.close();
browser.close();
}
Set preferred color scheme of browser
import launcher from "k6/x/browser";
import { sleep } from "k6";
export default function() {
const browser = launcher.launch('chromium', {
headless: false
});
const context = browser.newContext({
colorScheme: 'dark', // Valid values are "light", "dark" or "no-preference"
});
const page = context.newPage();
page.goto('http://whatsmyuseragent.org/');
sleep(5);
page.close();
browser.close();
}
import launcher from "k6/x/browser";
export default function() {
const browser = launcher.launch('chromium', {
headless: false,
slowMo: '500ms' // slow down by 500ms
});
const context = browser.newContext();
const page = context.newPage();
// Goto front page, find login link and click it
page.goto('https://test.k6.io/', { waitUntil: 'networkidle' });
const elem = page.$('a[href="/my_messages.php"]');
elem.click();
// Enter login credentials and login
page.$('input[name="login"]').type('admin');
page.$('input[name="password"]').type('123');
page.$('input[type="submit"]').click();
// Wait for next page to load
page.waitForLoadState('networkdidle');
page.close();
browser.close();
}
Check element state
import launcher from "k6/x/browser";
import { check } from "k6";
export default function() {
const browser = launcher.launch('chromium', {
headless: false
});
const context = browser.newContext();
const page = context.newPage();
// Inject page content
page.setContent(`
<div class="visible">Hello world</div>
<div style="display:none" class="hidden"></div>
<div class="editable" editable>Edit me</div>
<input type="checkbox" enabled class="enabled">
<input type="checkbox" disabled class="disabled">
<input type="checkbox" checked class="checked">
<input type="checkbox" class="unchecked">
`);
// Check state
check(page, {
'visible': p => p.$('.visible').isVisible(),
'hidden': p => p.$('.hidden').isHidden(),
'editable': p => p.$('.editable').isEditable(),
'enabled': p => p.$('.enabled').isEnabled(),
'disabled': p => p.$('.disabled').isDisabled(),
'checked': p => p.$('.checked').isChecked(),
'unchecked': p => p.$('.unchecked').isChecked() === false,
});
page.close();
browser.close();
}
Status
Currently only Chromium is supported, and the Playwright API coverage is as follows:
Class |
Support |
Missing APIs |
Accessibility |
⚠ |
snapshot() |
Browser |
✅ |
on() (dependent on event-loop support in k6), startTracing() , stopTracing() |
BrowserContext |
✅ |
addCookies() , backgroundPages() , cookies() , exposeBinding() , exposeFunction() , newCDPSession() , on() (dependent on event-loop support in k6), route() (dependent on event-loop support in k6), serviceWorkers() , storageState() , unroute() (dependent on event-loop support in k6), waitForEvent() (dependent on event-loop support in k6), tracing |
BrowserServer |
⚠ |
All |
BrowserType |
✅ |
connect() , connectOverCDP() , launchPersistentContext() , launchServer() |
CDPSession |
⚠ |
All |
ConsoleMessage |
⚠ |
All |
Coverage |
⚠ |
All |
Dialog |
⚠ |
All |
Download |
⚠ |
All |
ElementHandle |
✅ |
$eval() , $$eval() , setInputFiles() |
FetchRequest |
⚠ |
All |
FetchResponse |
⚠ |
All |
FileChooser |
⚠ |
All |
Frame |
✅ |
$eval() , $$eval() , addScriptTag() , addStyleTag() , dragAndDrop() , locator() , setInputFiles() |
JSHandle |
✅ |
- |
Keyboard |
✅ |
- |
Locator |
⚠ |
All |
Logger |
⚠ |
All |
Mouse |
✅ |
- |
Page |
✅ |
$eval() , $$eval() , addInitScript() , addScriptTag() , addStyleTag() , dragAndDrop() , exposeBinding() , exposeFunction() , frame() , goBack() , goForward() , locator() , on() (dependent on event-loop support in k6), pause() , pdf() , route() (dependent on event-loop support in k6), unroute() (dependent on event-loop support in k6), video() , waitForEvent() (dependent on event-loop support in k6), waitForResponse() (dependent on event-loop support in k6), waitForURL() (dependent on event-loop support in k6), workers() |
Request |
✅ |
failure() (dependent on event-loop support in k6), postDataJSON() , redirectFrom() , redirectTo() |
Response |
✅ |
finished() (dependent on event-loop support in k6) |
Route |
⚠ |
All |
Selectors |
⚠ |
All |
Touchscreen |
✅ |
- |
Tracing |
⚠ |
All |
Video |
⚠ |
All |
WebSocket |
⚠ |
All |
Worker |
⚠ |
All |