Skip to main content
k6 browser enables end-to-end testing of web applications using real browser automation. Built on the Chrome DevTools Protocol, it provides a Playwright-compatible API for interacting with web pages, perfect for testing user journeys and frontend performance.

Getting Started

Browser tests use the k6/browser module and require async functions:
import { browser } from 'k6/browser';
import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
};

export default async function() {
  const context = await browser.newContext();
  const page = await context.newPage();

  try {
    await page.goto('https://quickpizza.grafana.com/test.k6.io/', {
      waitUntil: 'networkidle'
    });
    
    console.log('Page title:', page.title());
  } finally {
    await page.close();
  }
}
Browser tests require the browser scenario option and must use async/await syntax. The browser module is currently available for Chromium-based browsers.

Browser Configuration

Configure browser behavior in scenario options:
export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
  thresholds: {
    checks: ['rate==1.0'],
    'browser_web_vital_fcp': ['p(95)<1000'],
    'browser_web_vital_lcp': ['p(95)<2500'],
  },
};

Browser Options

OptionTypeDescription
typeStringBrowser type (currently only ‘chromium’)
headlessBooleanRun in headless mode (default: true)
timeoutStringDefault timeout for operations
Navigate to pages and wait for different load states:
const page = await context.newPage();

// Navigate and wait for load
await page.goto('https://example.com');

// Navigate with options
await page.goto('https://example.com', {
  waitUntil: 'networkidle',  // Wait until network is idle
  timeout: '30s',
});

Element Interaction

Locators

Locators provide a robust way to find and interact with elements:
import { browser } from 'k6/browser';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
};

export default async function() {
  const context = await browser.newContext();
  const page = await context.newPage();

  try {
    await page.goto("https://quickpizza.grafana.com/flip_coin.php", {
      waitUntil: "networkidle",
    });

    // Create locators for elements
    const heads = page.getByRole("button", { name: "Bet on heads!" });
    const tails = page.getByRole("button", { name: "Bet on tails!" });
    const currentBet = page.getByText(/Your bet\: .*/);

    // Use locators across navigations
    await Promise.all([
      page.waitForNavigation(),
      tails.click(),
    ]);
    console.log(await currentBet.innerText());

    await Promise.all([
      page.waitForNavigation(),
      heads.click(),
    ]);
    console.log(await currentBet.innerText());
  } finally {
    await page.close();
  }
}

Locator Methods

// Recommended: Use ARIA roles
const button = page.getByRole('button', { name: 'Submit' });
const link = page.getByRole('link', { name: 'Login' });
const heading = page.getByRole('heading', { name: 'Welcome' });

Form Interaction

Fill forms and submit data:
import { browser } from 'k6/browser';
import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
};

export default async function() {
  const context = await browser.newContext();
  const page = await context.newPage();

  try {
    await page.goto('https://quickpizza.grafana.com/test.k6.io/', {
      waitUntil: 'networkidle'
    });

    // Navigate to login page
    await Promise.all([
      page.waitForNavigation(),
      page.getByRole('link', {name: '/my_messages.php'}).click(),
    ]);

    // Fill form fields
    await page.locator('input[name="login"]').type('admin');
    await page.locator('input[name="password"]').type("123");

    // Submit form
    await Promise.all([
      page.waitForNavigation(),
      page.getByText('Go!').click(),
    ]);

    // Verify login success
    await check(page.locator('h2'), {
      'header': async lo => {
        return await lo.textContent() == 'Welcome, admin!'
      }
    });

    // Check cookies
    await check(context, {
      'session cookie is set': async ctx => {
        const cookies = await ctx.cookies();
        return cookies.find(c => c.name == 'AWSALB') !== undefined;
      }
    });
  } finally {
    await page.close();
  }
}

Screenshots and Debugging

Capture screenshots for visual verification:
import { browser } from 'k6/browser';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
};

export default async function() {
  const context = await browser.newContext();
  const page = await context.newPage();
  
  try {
    await page.goto('https://quickpizza.grafana.com/test.k6.io/');
    
    // Take full page screenshot
    await page.screenshot({ path: 'screenshot.png' });
    
    // Screenshot specific element
    const element = page.locator('#main-content');
    await element.screenshot({ path: 'element.png' });
  } finally {
    await page.close();
  }
}

Waiting Strategies

Different ways to wait for elements and conditions:
// Wait for element to appear
await page.waitForSelector('.dynamic-content');

// Wait with timeout
await page.waitForSelector('.slow-loader', { timeout: '10s' });

Advanced Interactions

Mouse and Keyboard

// Mouse actions
await page.mouse.click(100, 200);
await page.mouse.move(150, 250);

// Keyboard input
await page.keyboard.type('Hello World');
await page.keyboard.press('Enter');
await page.keyboard.down('Shift');
await page.keyboard.up('Shift');

Cookies

// Get cookies
const cookies = await context.cookies();
console.log('Cookies:', cookies);

// Set cookies
await context.addCookies([
  {
    name: 'session',
    value: 'abc123',
    domain: 'example.com',
    path: '/',
  },
]);

// Clear cookies
await context.clearCookies();

Local Storage and Session Storage

// Evaluate JavaScript to access storage
const value = await page.evaluate(() => {
  return localStorage.getItem('key');
});

await page.evaluate(() => {
  localStorage.setItem('key', 'value');
  sessionStorage.setItem('session-key', 'session-value');
});

Network Control

Request Interception

// Intercept and modify requests
await page.route('**/*.{png,jpg,jpeg}', route => {
  route.abort();
});

// Continue with modifications
await page.route('**/api/**', route => {
  route.continue({
    headers: {
      ...route.request().headers(),
      'X-Custom-Header': 'value',
    },
  });
});

Network Throttling

// Emulate slow network
await context.throttleNetwork({
  download: 1000,  // 1 Mbps
  upload: 500,     // 0.5 Mbps
  latency: 100,    // 100ms
});

Device Emulation

import { browser } from 'k6/browser';

export default async function() {
  const context = await browser.newContext({
    viewport: { width: 375, height: 667 },
    userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
    deviceScaleFactor: 2,
    isMobile: true,
    hasTouch: true,
  });

  const page = await context.newPage();
  
  try {
    await page.goto('https://example.com');
    // Mobile test logic
  } finally {
    await page.close();
  }
}

Multiple Pages and Contexts

import { browser } from 'k6/browser';

export default async function() {
  // Create isolated contexts (separate sessions)
  const context1 = await browser.newContext();
  const context2 = await browser.newContext();

  // Open multiple pages in same context
  const page1 = await context1.newPage();
  const page2 = await context1.newPage();

  try {
    // Pages in same context share cookies/storage
    await page1.goto('https://example.com/login');
    await page2.goto('https://example.com/dashboard');

    // Different contexts are isolated
    await context2.newPage();
  } finally {
    await page1.close();
    await page2.close();
  }
}

Web Vitals

k6 browser automatically collects Web Vitals metrics:
export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
  thresholds: {
    'browser_web_vital_fcp': ['p(95)<1000'],      // First Contentful Paint
    'browser_web_vital_lcp': ['p(95)<2500'],      // Largest Contentful Paint
    'browser_web_vital_fid': ['p(95)<100'],       // First Input Delay
    'browser_web_vital_cls': ['p(95)<0.1'],       // Cumulative Layout Shift
    'browser_web_vital_ttfb': ['p(95)<800'],      // Time to First Byte
    'browser_web_vital_inp': ['p(95)<200'],       // Interaction to Next Paint
  },
};

Page Object Model

Organize tests using the Page Object pattern:
import { browser } from 'k6/browser';

class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.locator('input[name="username"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.submitButton = page.getByRole('button', { name: 'Login' });
  }

  async login(username, password) {
    await this.usernameInput.type(username);
    await this.passwordInput.type(password);
    await Promise.all([
      this.page.waitForNavigation(),
      this.submitButton.click(),
    ]);
  }
}

export default async function() {
  const context = await browser.newContext();
  const page = await context.newPage();

  try {
    await page.goto('https://example.com/login');
    
    const loginPage = new LoginPage(page);
    await loginPage.login('user', 'password');
  } finally {
    await page.close();
  }
}
Browser tests consume more resources than protocol-level tests. Use them strategically for critical user journeys and complement with HTTP tests for higher load levels.

Best Practices

  1. Always close pages and contexts to free up resources
  2. Use locators over selectors for resilience across page changes
  3. Wait for navigation when actions trigger page loads
  4. Set up waiters before actions to avoid race conditions
  5. Use Page Object Model for maintainable test code
  6. Combine with HTTP tests for comprehensive coverage
  7. Monitor Web Vitals to ensure performance standards
  8. Use headless mode in CI/CD environments

Metrics

k6 browser collects comprehensive metrics:
  • browser_web_vital_* - Core Web Vitals (FCP, LCP, CLS, etc.)
  • browser_http_req_duration - HTTP request durations
  • browser_http_req_failed - Failed HTTP requests
  • browser_data_sent / browser_data_received - Network traffic

Build docs developers (and LLMs) love