xk6-browser
xk6-browser copied to clipboard
The browser module adds support for browser automation and end-to-end web testing via the Chrome Devtools Protocol to k6.
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.
See our project roadmap for more details.
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 large and some of the functionality only makes sense if it's implemented using async operations: event listening, request interception, waiting for events, etc. This requires the existence of an event loop per VU in k6, which was only recently added. Most of the current xk6-browser API is synchronous and thus lacks some of the functionality that requires asynchronicity, but we're gradually migrating existing methods to return aPromise
, and adding new ones that will follow the same API.Expect many breaking changes during this transition, which we'll point out in the release notes.
Note that
async
/await
is still not natively supported in k6 scripts, because of the outdated Babel version it uses. If you wish to use this syntax you'll have to transform your script beforehand with an updated Babel version. See the k6-template-es6 project and this comment for details.
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:
- Make sure that you're running the latest Go version
- Go toolchain
- Git
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 newxk6-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 { chromium } from 'k6/x/browser';
export default function() {
const browser = chromium.launch({
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 { chromium } from 'k6/x/browser';
export default function() {
const browser = chromium.launch();
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 { chromium } from 'k6/x/browser';
export default function() {
const browser = chromium.launch({ 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 { chromium } from 'k6/x/browser';
export default function() {
const browser = chromium.launch({ headless: false });
const context = browser.newContext();
const page = context.newPage();
page.goto('http://whatsmyuseragent.org/');
// Find element using CSS selector
let 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 { chromium } from 'k6/x/browser';
export default function() {
const browser = chromium.launch({ 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 { chromium } from 'k6/x/browser';
import { sleep } from "k6";
export default function() {
const browser = chromium.launch({
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();
}
Fill out a form
import { chromium } from 'k6/x/browser';
export default function() {
const browser = chromium.launch({
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().then(() => {
// Enter login credentials and login
page.$('input[name="login"]').type('admin');
page.$('input[name="password"]').type('123');
return page.$('input[type="submit"]').click();
}).then(() => {
// Wait for next page to load
page.waitForLoadState('networkidle');
}).finally(() => {
// Release the page and browser.
page.close();
browser.close();
});
}
Check element state
import { chromium } from 'k6/x/browser';
import { check } from "k6";
export default function() {
const browser = chromium.launch({
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();
}
Locator API
We suggest using the Locator API instead of the low-level
ElementHandle
methods. An element handle can go stale if
the element's underlying frame is navigated. However,
with the Locator API, even if the underlying frame
navigates, locators will continue to work.
The Locator API can also help you abstract a page to simplify testing. To do that, you can use a pattern called the Page Object Model. You can see an example here.
import { chromium } from 'k6/x/browser';
export default function () {
const browser = chromium.launch({
headless: false,
});
const context = browser.newContext();
const page = context.newPage();
page.goto("https://test.k6.io/flip_coin.php", {
waitUntil: "networkidle",
});
/*
In this example, we will use two locators, matching a
different betting button on the page. If you were to query
the buttons once and save them as below, you would see an
error after the initial navigation. Try it!
const heads = page.$("input[value='Bet on heads!']");
const tails = page.$("input[value='Bet on tails!']");
The Locator API allows you to get a fresh element handle each
time you use one of the locator methods. And, you can carry a
locator across frame navigations. Let's create two locators;
each locates a button on the page.
*/
const heads = page.locator("input[value='Bet on heads!']");
const tails = page.locator("input[value='Bet on tails!']");
const currentBet = page.locator("//p[starts-with(text(),'Your bet: ')]");
// In the following Promise.all the tails locator clicks
// on the tails button by using the locator's selector.
// Since clicking on each button causes page navigation,
// waitForNavigation is needed -- this is because the page
// won't be ready until the navigation completes.
// Setting up the waitForNavigation first before the click
// is important to avoid race conditions.
Promise.all([
page.waitForNavigation(),
tails.click(),
]).then(() => {
console.log(currentBet.innerText());
// the heads locator clicks on the heads button
// by using the locator's selector.
return Promise.all([
page.waitForNavigation(),
heads.click(),
]);
}).then(() => {
console.log(currentBet.innerText());
return Promise.all([
page.waitForNavigation(),
tails.click(),
]);
}).finally(() => {
console.log(currentBet.innerText());
page.close();
browser.close();
})
}
Status
Currently only Chromium is supported, and the Playwright API coverage is as follows: