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
Option Type Description typeString Browser type (currently only ‘chromium’) headlessBoolean Run in headless mode (default: true) timeoutString Default timeout for operations
Page Navigation
Navigate to pages and wait for different load states:
Basic Navigation
Wait Conditions
Concurrent Navigation
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' ,
});
// Wait for navigation to complete
await Promise . all ([
page . waitForNavigation (),
page . click ( 'a.link' ),
]);
// Wait for network idle
await page . goto ( 'https://example.com' , {
waitUntil: 'networkidle'
});
// Wait for DOM content loaded
await page . goto ( 'https://example.com' , {
waitUntil: 'domcontentloaded'
});
// Set up waiter before triggering navigation
await Promise . all ([
page . waitForNavigation (),
page . click ( 'button[type="submit"]' ),
]);
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
By Role
By Text
By Label
By Selector
// 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' });
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 Selector
Wait for Function
Wait for Event
// 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
Always close pages and contexts to free up resources
Use locators over selectors for resilience across page changes
Wait for navigation when actions trigger page loads
Set up waiters before actions to avoid race conditions
Use Page Object Model for maintainable test code
Combine with HTTP tests for comprehensive coverage
Monitor Web Vitals to ensure performance standards
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