browser

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2022 License: AGPL-3.0 Imports: 8 Imported by: 0

README

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
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).

https://user-images.githubusercontent.com/10811379/188889687-660ce8f9-264d-4dd0-905c-41a909102be8.mp4

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.

Troubleshooting

If you're having issues installing or running xk6-browser, please see the troubleshooting document.

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();
  })
}

Running examples in a Docker container

The examples above are also available as standalone files in the examples directory. You can run them in a Docker container using Docker Compose with:

docker-compose run -T xk6-browser run - <examples/browser_on.js

Status

Currently only Chromium is supported, and the Playwright API coverage is as follows:

Class Support Missing APIs
Accessibility snapshot()
Browser startTracing(), stopTracing()
BrowserContext addCookies(), backgroundPages(), cookies(), exposeBinding(), exposeFunction(), newCDPSession(), on(), route(), serviceWorkers(), storageState(), unroute(), waitForEvent(), 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 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 All
Mouse -
Page $eval(), $$eval(), addInitScript(), addScriptTag(), addStyleTag(), dragAndDrop(), exposeBinding(), exposeFunction(), frame(), goBack(), goForward(), on(), pause(), pdf(), route(), unroute(), video(), waitForEvent(), waitForResponse(), waitForURL(), workers()
Request failure(), postDataJSON(), redirectFrom(), redirectTo()
Response finished()
Route All
Selectors All
Touchscreen -
Tracing All
Video All
WebSocket All
Worker All

Support

To get help about usage, report bugs, suggest features and discuss xk6-browser with other users see SUPPORT.md.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type JSModule

type JSModule struct {
	Chromium api.BrowserType
	Devices  map[string]common.Device
	Version  string
}

JSModule exposes the properties available to the JS script.

type ModuleInstance

type ModuleInstance struct {
	// contains filtered or unexported fields
}

ModuleInstance represents an instance of the JS module.

func (*ModuleInstance) Exports added in v0.1.1

func (mi *ModuleInstance) Exports() k6modules.Exports

Exports returns the exports of the JS module so that it can be used in test scripts.

type RootModule

type RootModule struct{}

RootModule is the global module instance that will create module instances for each VU.

func New

func New() *RootModule

New returns a pointer to a new RootModule instance.

func (*RootModule) NewModuleInstance

func (*RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance

NewModuleInstance implements the k6modules.Module interface to return a new instance for each VU.

Directories

Path Synopsis
js
Package k6ext acts as an encapsulation layer between the k6 core and xk6-browser.
Package k6ext acts as an encapsulation layer between the k6 core and xk6-browser.
ws
Package ws provides a test WebSocket server.
Package ws provides a test WebSocket server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL