xk6-browser icon indicating copy to clipboard operation
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.

xk6-browser

Browser automation and end-to-end web testing for k6

An extension for k6 adding browser-level APIs with rough Playwright compatibility.

Github release Build status Go Report Card
@k6_io on Twitter Slack channel

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 a Promise, 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:

Then:

  1. Install xk6:
go install go.k6.io/xk6/cmd/xk6@latest
  1. 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.

  1. 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 { 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:

Class Support Missing APIs
Accessibility :warning: snapshot()
Browser :white_check_mark: startTracing(), stopTracing()
BrowserContext :white_check_mark: addCookies(), backgroundPages(), cookies(), exposeBinding(), exposeFunction(), newCDPSession(), on(), route(), serviceWorkers(), storageState(), unroute(), waitForEvent(), tracing
BrowserServer :warning: All
BrowserType :white_check_mark: connect(), connectOverCDP(), launchPersistentContext(), launchServer()
CDPSession :warning: All
ConsoleMessage :warning: All
Coverage :warning: All
Dialog :warning: All
Download :warning: All
ElementHandle :white_check_mark: $eval(), $$eval(), setInputFiles()
FetchRequest :warning: All
FetchResponse :warning: All
FileChooser :warning: All
Frame :white_check_mark: $eval(), $$eval(), addScriptTag(), addStyleTag(), dragAndDrop(), locator(), setInputFiles()
JSHandle :white_check_mark: -
Keyboard :white_check_mark: -
Locator :white_check_mark: allInnerTexts(), allTextContents(), boundingBox([options]), count(), dragTo(target[, options]), elementHandle([options]) (state: attached), elementHandles(), evaluate(pageFunction[, arg, options]), evaluateAll(pageFunction[, arg]), evaluateHandle(pageFunction[, arg, options]), first(), frameLocator(selector), frameLocator(selector), highlight(), last(), nth(index), page(), screenshot([options]), scrollIntoViewIfNeeded([options]), selectText([options]), setChecked(checked[, options]), setInputFiles(files[, options])
Logger :warning: All
Mouse :white_check_mark: -
Page :white_check_mark: $eval(), $$eval(), addInitScript(), addScriptTag(), addStyleTag(), dragAndDrop(), exposeBinding(), exposeFunction(), frame(), goBack(), goForward(), on(), pause(), pdf(), route(), unroute(), video(), waitForEvent(), waitForResponse(), waitForURL(), workers()
Request :white_check_mark: failure(), postDataJSON(), redirectFrom(), redirectTo()
Response :white_check_mark: finished()
Route :warning: All
Selectors :warning: All
Touchscreen :white_check_mark: -
Tracing :warning: All
Video :warning: All
WebSocket :warning: All
Worker :warning: All