Releases: grafana/k6
v1.6.1
k6 v1.6.1 is here! This patch release includes:
- Bug fix for a race condition in the experimental CSV module
- Bug fix for manifest k6 version override
- Version updates for Go toolchain and Docker images
Bug fixes
- #5632 Fixes a race condition in the
experimental/csvmodule when multiple files with async code usecsv.parsein parallel during initialization. - #5642 Fixes an issue where k6 was not always added as a build dependency, preventing manifests from overriding the k6 version.
Maintenance and security updates
v1.6.0
k6 v1.6.0 is here 🎉! This release includes:
- Cloud commands now support configurable default Grafana Cloud stack.
- New
k6 depscommand for analyzing script dependencies. - Browser APIs enhancements with
frameLocator(),goBack(),goForward()methods. - Crypto module adds PBKDF2 support for password-based key derivation.
jslibgets a new TOTP library for time-based one-time password generation and validation.- New mcp-k6 MCP server for AI-assisted k6 script writing.
Breaking changes
There are no breaking changes in this release.
New features
Configurable default stack for Cloud commands #5420
Cloud commands now support configuring the default Grafana Cloud stack you want to use. The stack slug (or stack id) is used by the Cloud to determine which default project to use when not explicitly provided.
Previously, users had to specify the project id for every test run. With this change, you can configure a default stack during login, and k6 will use it to automatically resolve the appropriate default project. This is particularly useful for organizations with multiple Grafana Cloud stacks or when working across different teams and environments.
Users can also set up a specific stack for every test run, either using the new option stackID or the environment variable K6_CLOUD_STACK_ID.
Please note that, in k6 v2, this stack information will become mandatory to run a test.
# Login interactively and select default stack
k6 cloud login
# Login and set default stack with token
k6 cloud login --token $MY_TOKEN --stack my-stack-slug
# Run test using the configured default stack
k6 cloud run script.js
# Run test using a specific stack
K6_CLOUD_STACK_ID=12345 k6 cloud run script.js
# Stack id can also be set in the options
export const options = {
cloud: {
stackID: 123,
projectID: 789, // If the project does not belong to the stack, this will throw an error
},
};This simplifies the cloud testing workflow and prepares k6 for upcoming changes to the Grafana Cloud k6 authentication process, where the stack will eventually become mandatory.
k6 deps command and manifest support #5410, #5427
A new k6 deps command is now available to analyze and list all dependencies of a given script or archive. This is particularly useful for understanding which extensions are required to run a script, especially when using auto extension resolution.
The command identifies all imports in your script and lists dependencies that might be needed for building a new binary with auto extension resolution. Like auto extension resolution itself, this only accounts for imports, not dynamic require() calls.
# Analyze script dependencies
k6 deps script.js
# Output in JSON format for programmatic consumption
k6 deps --json script.js
# Analyze archived test dependencies
k6 deps archive.tarThis makes it easier to understand extension requirements, share scripts with clear dependency information, and integrate k6 into automated build pipelines.
In addition, k6 now supports a manifest that specifies default version constraints for dependencies when no version is defined in the script using pragmas. If a dependency is imported without an explicit version, it defaults to "*", and the manifest can be used to replace that with a concrete version constraint.
The manifest is set through an environment variable as JSON with keys being a dependency and values being constraints:
K6_DEPENDENCIES_MANIFEST='{"k6/x/faker": ">=v0.4.4"}' k6 run scripts.js
In this example, if the script only imports k6/x/faker and does not use a use k6 with k6/x/faker ... directive, it will set the version constraint to >=v0.4.4. It will not make any changes if k6/x/faker is not a dependency of the script at all.
Browser module: frameLocator() method #5487
The browser module now supports frameLocator() on Page, Frame, Locator, and FrameLocator objects. This method creates a locator for working with iframe elements without the need to explicitly switch contexts, making it much easier to interact with embedded content.
Frame locators are particularly valuable when testing applications with nested iframes, as they allow you to chain locators naturally while maintaining readability:
Click to expand example code
import { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
},
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://example.com');
// Locate an iframe and interact with elements inside it
const frame = page.frameLocator('#payment-iframe');
await frame.locator('#card-number').fill('4242424242424242');
await frame.locator('#submit-button').click();
// Chain frame locators for nested iframes
const nestedFrame = page
.frameLocator('#outer-frame')
.frameLocator('#inner-frame');
await nestedFrame.locator('#nested-content').click();
} finally {
await page.close();
}
}This complements existing frame handling methods and provides a more intuitive API for working with iframe-heavy applications.
Browser module: goBack() and goForward() navigation methods #5494
The browser module now supports page.goBack() and page.goForward() methods for browser history navigation. These methods allow you to navigate the page's history, similar to clicking the browser's back/forward buttons.
Click to expand example code
import { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
},
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://example.com');
await page.goto('https://example.com/page2');
// Navigate back to the previous page
await page.goBack();
// Navigate forward again
await page.goForward();
// Both methods support optional timeout and waitUntil parameters
await page.goBack({ waitUntil: 'networkidle' });
} finally {
await page.close();
}
}Browser module: Request event handlers #5481, #5486
The browser module now supports page.on('requestfailed') and page.on('requestfinished') event handlers, enabling better monitoring and debugging of network activity during browser tests.
The requestfailed event fires when a request fails (network errors, aborts, etc.), while requestfinished fires when a request completes successfully.
Click to expand example code
import { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
},
},
};
export default async function () {
const page = await browser.newPage();
// Monitor failed requests
page.on('requestfailed', (request) => {
console.log(`Request failed: ${request.url()}`);
});
// Monitor successful requests
page.on('requestfinished', (request) => {
console.log(`Request finished: ${request.url()}`);
});
await page.goto('https://example.com');
await page.close();
}These event handlers provide deeper insights into network behavior during browser testing and help identify issues that might not be immediately visible.
Crypto module: PBKDF2 support #5380
The crypto module now supports PBKDF2 for deriving cryptographic keys from passwords. PBKDF2 is widely used for password hashing and key derivation in security-sensitive applications, and this addition enables testing of systems that use PBKDF2 for authentication or encryption.
For usage examples, check out the one provided in the repository or refer to the documentation.
WebSockets module is now stable #5586
The websockets module has been promoted to stable status and is now available via the k6/websockets path.
The experimental k6/experimental/websockets module will be removed in a future release. Users should migrate to the stable k6/websockets module.
To migrate, simply update your import statement:
// Old (experimental)
import ws from 'k6/experimental/websockets';
// New (stable)
import ws from 'k6/websockets';No other changes are required because the API is the same.
Console logging: ArrayBuffer and TypedArray support #5496
console.log() now properly displays ArrayBuffer and TypedArray objects, making it easier to debug binary data handling in your test scripts. Previously, these types would not display useful information, making debugging difficult when working with binary protocols, file uploads, or WebS...
v1.5.0
k6 1.5.0 is here 🎉! This release includes:
- Changes in the browser module:
page.waitForEvent()for event-based synchronization with page events.locator.pressSequentially()for character-by-character typing simulation.
- Improved debugging with deep object logging in
console.log(). - Extended WebSocket support with close code and reason information.
- Enhanced extension ecosystem with custom subcommands and DNS resolver access.
- URL-based secret management for external secret services.
- New machine-readable summary format for test results.
Breaking changes
As per our stability guarantees, breaking changes across minor releases are allowed only for experimental features.
- #5237 Deprecates the
experimental/redismodule. The module will be removed in a future release. Users should migrate to alternative solutions, such as the official k6 Redis extension.
New features
page.waitForEvent() #5478
The browser module now supports page.waitForEvent(), which blocks the caller until a specified event is captured.
If a predicate is provided, it waits for an event that satisfies the predicate. This method is particularly valuable for testing scenarios where you need to synchronize your test flow with specific browser or page events before proceeding with the next action.
import { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
},
},
};
export default async function () {
const page = await browser.newPage();
// Wait for a console message containing specific text
const msgPromise = page.waitForEvent('console', msg => msg.text().includes('hello'));
await page.evaluate(() => console.log('hello world'));
const msg = await msgPromise;
console.log(msg.text());
// Output: hello world
// Wait for a response from a specific URL with timeout
const resPromise = page.waitForEvent('response', {
predicate: res => res.url().includes('/api/data'),
timeout: 5000,
});
await page.click('button#fetch-data');
const res = await resPromise;
await page.close();
}Event-driven synchronization is vital for test reliability, especially when dealing with asynchronous operations where timing is unpredictable. This is more robust than using fixed delays and helps avoid flaky tests.
locator.pressSequentially() #5457
The browser module now supports locator.pressSequentially(), which types text character by character, firing keyboard events (keydown, keypress, keyup) for each character. This method is essential for testing features that depend on gradual typing to trigger specific behaviors, such as autocomplete suggestions, real-time input validation per character, or dynamic character counters.
The method supports a configurable delay between keystrokes, enabling you to simulate realistic typing speeds and test time-dependent input handlers:
import { browser } from 'k6/browser';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
},
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://quickpizza.grafana.com/browser.php');
// Type text character by character
const searchInput = page.locator('#text1');
await searchInput.pressSequentially('Hello World');
// Type with delay to simulate realistic typing speed
await searchInput.clear();
await searchInput.pressSequentially('test query', { delay: 100 });
} finally {
await page.close();
}
}This complements existing text input methods: locator.fill() for simple form filling, locator.type() for gradual typing without keyboard events, and now pressSequentially for character-by-character typing with full keyboard event firing.
Thank you, @rajan2345, for the contribution 🎉
console.log() Deep Object Logging #5460
console.log() now properly traverses and displays complex JavaScript structures, including functions, classes, and circular references. Previously, Sobek's JSON marshaling would lose nested functions, classes, and other non-serializable types, making debugging painful.
Objects with mixed function and class properties are now properly displayed:
console.log({
one: class {},
two: function() {}
});
// Before: {}
// After: {"one":"[object Function]","two":"[object Function]"}Nested arrays and objects with functions are now fully traversed:
console.log([
{ handler: class {} },
{ data: [1, 2, class {}] }
]);
// Before: [{},{"data":[1,2,null]}]
// After: [{"handler":"[object Function]"},{"data":[1,2,"[object Function]"]}]Complex objects with multiple property types are properly preserved:
console.log({
a: [1, 2, 3],
b: class {},
c: () => {},
d: function() {},
e: [1, () => {}, function() {}, class {}, 2]
});
// Before: {"a":[1,2,3],"e":[1,null,null,null,2]}
// After: {
// "a":[1,2,3],
// "b":"[object Function]",
// "c":"[object Function]",
// "d":"[object Function]",
// "e":[1,"[object Function]","[object Function]","[object Function]",2]
// }Circular references are now properly detected and marked:
const obj = {
fn: function() {},
foo: {}
};
obj.foo = obj;
console.log(obj);
// Before: [object Object]
// After: {"fn":"[object Function]","foo":"[Circular]"}This improvement makes debugging k6 test scripts significantly easier when working with API responses, event handlers, and complex state objects.
experimental/websockets - Close Code and Reason Support #5376
The experimental WebSockets module now supports sending close codes and reasons when closing connections, and properly captures close event information. This is essential for testing WebSocket
implementations that rely on specific close codes to determine whether a connection was closed normally or due to an error.
import ws from 'k6/experimental/websockets';
export default function () {
const socket = ws.connect('ws://example.com', (socket) => {
socket.on('close', (data) => {
console.log(`Connection closed with code: ${data.code}, reason: ${data.reason}`);
// Output: Connection closed with code: 1000, reason: Normal closure
});
});
// Close with code and reason
socket.close(1000, 'Normal closure');
}Thanks, @etodanik, for the contribution 🎉
Subcommand Extension Support #5399
Extensions can now register custom subcommands under the k6 x namespace, enabling custom command-line tools that integrate seamlessly with k6. This provides a consistent and discoverable way for extensions to offer specialized CLI utilities while maintaining k6's familiar command structure.
Extensions can now define custom commands like:
k6 x my-tool --help
k6 x debug --inspectThis integration pattern allows extension authors to provide powerful tooling that feels native to the k6 ecosystem.
DNS Resolver Access #5421
Extensions can now access k6's DNS resolver for custom DNS handling and networking extensions. The resolver respects k6's configuration including hosts overrides, custom DNS servers, and DNS caching settings. This enables extensions to use it directly instead of having to reproduce the functionality. Which also makes them work the same way as native modules.
Machine-Readable Summary Format #5338
A new machine-readable summary format for the end-of-test summary is now available, providing structured, programmatic shapes via --summary-export and handleSummary(). This format is designed for easier integration with external systems and analytics pipelines.
The new format is currently opt-in via the --new-machine-readable-summary flag or K6_NEW_MACHINE_READABLE_SUMMARY environment variable, and will become the default in k6 v2:
k6 run script.js --new-machine-readable-summary --summary-export=summary.jsonThis makes it easier to integrate k6 results into CI/CD pipelines, dashboards, and custom analysis tools.
URL-Based Secret Management #5413
The secret management system now supports URL-based secret sources, allowing k6 to fetch secrets from HTTP endpoints. This lets users implement a simple HTTP API to provide secrets to a test. There is a mock implementation, but no particular production-ready implementation is provided at this time. In the future, there is potential for proxies to other systems, including HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault.
UX improvements and enhancements
- #5458 Adds link to k6 extensions list in README for better discoverability.
- [#5366](https://github.com/gra...
v1.4.2
k6 v1.4.2 is here 🎉!
This is a patch release that includes:
- #5439 Fix loading files with spaces in their paths
- #5415 Updates Sobek to fix the unexpected token class issue
v1.4.1
v1.4.0
k6 v1.4.0 is here 🎉! This release includes:
- OpenTelemetry output graduated from experimental to stable status.
- Changes in the Browser module:
page.waitForRequestfor waiting on specific HTTP requests.QueryAllmethods now return elements in DOM order.locator.evaluateandlocator.evaluateHandlefor executing JavaScript code in the page context with access to the matching element.page.unroute(url)andpage.unrouteAllfor removing routes registered withpage.route.
Breaking changes
As per our stability guarantees, breaking changes across minor releases are allowed only for experimental features.
Breaking changes for experimental modules
- #5164 OpenTelemetry output now exports rate metrics as a single counter with
zero/nonzerolabels instead of separate metrics. - #5333 OpenTelemetry output configuration:
K6_OTEL_EXPORTER_TYPEis deprecated in favor ofK6_OTEL_EXPORTER_PROTOCOLto align with OpenTelemetry standards.
Breaking changes for undefined behaviours
- #5320, #5239, #5342 Automatic extension resolution now only inspects ES module
importstatements and no longer supports CommonJSrequire()calls. CommonJSrequire()calls are dynamic, and it is not possible to know for certain if they will be called, or if they will be called with static strings - the only way they were even previously loaded. This functionality was a quirk of the previous implementation and had numerous problems. Additionally,use k6directives are now only recognized when they appear at the beginning of files (after optional shebang and whitespace/comments). This was the original intention, but due to implementation bugs, it did not accurately reflect what was happening.
New features
OpenTelemetry output graduation #5334
The OpenTelemetry output has graduated from experimental status and is now available as a stable output using the name opentelemetry. This change makes OpenTelemetry the recommended vendor-agnostic solution for exporting k6 telemetry data.
You can now use the stable output name in your k6 commands:
# Previous experimental usage (still supported for backward compatibility)
k6 run --out experimental-opentelemetry script.js
# New stable usage
k6 run --out opentelemetry script.jsThe experimental-opentelemetry name will continue to work for backward compatibility for now but it's deprecated and we might remove it in future versions. We recommend migrating to use the new opentelemetry name.
page.waitForRequest #5330
The browser module now supports page.waitForRequest(), which allows you to wait for HTTP requests that match specific URL patterns during browser automation. This method is particularly valuable for testing scenarios where you need to ensure specific network requests are initiated before proceeding with test actions.
The method supports multiple URL pattern matching strategies:
// Wait for exact URL match
const requestPromise = page.waitForRequest('https://api.example.com/data');
await page.click('button[data-testid="load-data"]');
const request = await requestPromise;
// Wait for regex pattern match
await page.waitForRequest(/\/api\/.*\.json$/);
// Use with Promise.all for coordinated actions
await Promise.all([
page.waitForRequest('https://api.example.com/user-data'),
page.click('button[data-testid="load-user-data"]')
]);This complements the existing page.waitForResponse() method by focusing on HTTP requests rather than responses, providing more granular control over network-dependent test scenarios.
page.unroute(url) and page.unrouteAll() #5223
The browser module now supports page.unroute(url) and page.unrouteAll(), allowing you to remove routes previously registered with page.route.
Example usage:
await page.route(/.*\/api\/pizza/, function (route) {
console.log('Modifying request to /api/pizza');
route.continue({
postData: JSON.stringify({
customName: 'My Pizza',
}),
});
});
...
await page.unroute(/.*\/api\/pizza/); // The URL needs to be exactly the same as the one used in the call to the `route` functionawait page.route(/.*\/api\/pizza/, function (route) {
console.log('Modifying request to /api/pizza');
route.continue({
postData: JSON.stringify({
customName: 'My Pizza',
}),
});
});
...
await page.unrouteAll();locator.evaluate and locator.evaluateHandle #5306
The browser module now supports locator.evaluate and locator.evaluateHandle, allowing you to execute JavaScript code in the page context with access to the matching element. The only difference between evaluate and evaluateHandle is that evaluateHandle returns a JSHandle.
Example usage:
await check(page, {
'calling evaluate': async p => {
const n = await p.locator('#pizza-name').evaluate(pizzaName => pizzaName.textContent);
return n == 'Our recommendation:';
}
});
await check(page, {
'calling evaluate with arguments': async p => {
const n = await p.locator('#pizza-name').evaluate((pizzaName, extra) => pizzaName.textContent + extra, ' Super pizza!');
return n == 'Our recommendation: Super pizza!';
}
});const jsHandle = await page.locator('#pizza-name').evaluateHandle((pizzaName) => pizzaName);
const obj = await jsHandle.evaluateHandle((handle) => {
return { innerText: handle.innerText };
});
console.log(await obj.jsonValue()); // {"innerText":"Our recommendation:"}New officially supported k6 DNS extension
The xk6-dns extension is now officially supported in k6 OSS and k6 Cloud. You can import k6/x/dns directly in your scripts thanks to automatic extension resolution, with no custom build required.
Use it to perform DNS resolution testing as part of your tests: resolve names via custom or system DNS, measure resolution latency and errors, validate records before HTTP steps, compare resolvers, and even load test DNS servers in end‑to‑end scenarios.
For example:
import dns from 'k6/x/dns';
export default function () {
const answer = dns.resolve('grafana.com', { recordType: 'A' });
console.log(answer.records.map(({ address }) => address).join(', '));
}The extension currently supports A and AAAA record lookups. If you would like to see additional record types supported, please consider contributing to the extension.
Automatic extension resolution improvements #5320, #5239, #5342, #5332, #5240
Automatic extension resolution has been completely reimplemented to use k6's internal module loader instead of the external k6deps/esbuild pipeline. This change brings significant improvements in reliability and maintainability.
As part of the rewrite, a few issues and unintended features were found, namely:
- Trying to follow
requirecalls, which, due to their dynamic nature, don't work particularly stably. That is, depending on where and how therequirecall was used, k6 might decide whether it is needed or not. And it definitely doesn't work when using actual string variables. Support for CommonJS is primarily for backward compatibility, so after an internal discussion, we opted not to support it at all. We could bring this back until v2, if there is enough interest. However, in the long term, it is intended that this not be part of k6. - "use k6 with ..." directives were parsed from the whole file instead of just the beginning, which leads to numerous problems, and was not the intended case. As such, they are now only parsed at the beginning of files (not just the main one) with potential empty lines and comments preceding them.
Example:
// main.js
"use k6 with k6/x/faker"
import { faker } from 'k6/x/faker';
import { helper } from './utils.js';
export default function() {
console.log(faker.name());
helper();
}Or, an example using the directive with CommonJS
// utils.js
"use k6 with k6/x/redis"
const redis = require('k6/x/redis');
exports.helper = function() {
// Use redis extension
}In this example, k6 will detect both k6/x/faker and k6/x/redis extensions from the use k6 directives in both files and provision a binary that includes both extensions if needed.
Other fixes this brings are:
- Fixes for path related issues (irregardless of usage of the feature) on windows, especially between drives. It is possible there were problems on other OSes that were just not reported. [#5...
v1.3.0
k6 v1.3.0 is here 🎉! This release includes:
- Browser module gets:
locator.locator,locator.contentFrame, andFrameLocator.locatorfor powerful locator chaining and iframe handling.locator|frame|FrameLocator.getBy*for targeting elements without relying on brittle CSS selectors.locator.filterfor filtering locators for more precise element targeting.locator.boundingBoxfor retrieving element geometry.page.waitForResponsefor waiting on specific HTTP responses.
Deprecations
A new summary mode disabled has been introduced to replace the "no summary" option #5118
The --no-summary flag and its corresponding environment variable K6_NO_SUMMARY have been deprecated in favor of
the new disabled summary mode. This change unifies the configuration experience for controlling the end-of-test summary.
You can now disable the end-of-test summary with either --summary-mode=disabled or K6_SUMMARY_MODE=disabled.
The legacy summary mode has been deprecated #5138
The legacy summary mode was introduced in k6 v1.0, when the end-of-test summary was revamped with the addition of two
new modes: compact and full.
Its purpose was to ease the transition for users who relied heavily on the old summary format.
However, we’ve now reached the point where it’s time to deprecate it.
The plan is to fully remove it in k6 v2.0, so please migrate to either compact or full to ensure readiness for the
next major release.
New features
locator.locator #5073
The locator.locator method allows you to define locators relative to a parent locator, enabling powerful locator chaining and nesting. This feature lets you create more precise element targeting by combining multiple selectors in a hierarchical manner.
await page
.locator('[data-testid="inventory"]')
.locator('[data-item="apples"]')
.locator('button.add')
.click();This nesting capability provides a more intuitive way to navigate complex DOM structures and serves as the foundation for other locator APIs in this release that require such hierarchical targeting.
locator.contentFrame #5075
The browser module now supports locator.contentFrame(), which returns a new type frameLocator. This method is essential for switching context from the parent page to iframe contents.
frameLocator types target iframe elements on the page and provide a gateway to interact with their contents. Unlike regular locators that work within the current frame context, frameLocators specifically target iframe elements and prepare them for content interaction.
This approach is essential for iframe interaction because:
- Iframes create separate DOM contexts that require special handling.
- Browsers enforce security boundaries between frames.
- Iframe content may load asynchronously and needs proper waiting.
- Using
elementHandlefor iframe interactions is error-prone and can lead to stale references, whileframeLocatorprovide reliable, auto-retrying approaches.
Example usage:
// Get iframe element and switch to its content frame
const iframeLocator = page.locator('iframe[name="payment-form"]');
const frame = await iframeLocator.contentFrame();frameLocator.locator #5075
We've also added frameLocator.locator which allows you to create locators for elements inside an iframe. Once you've targeted an iframe with page.contentFrame(), you can use .locator() to find and interact with elements within that iframe's content with the frameLocator type.
Example usage:
// Target an iframe and interact with elements inside it
const iframe = page.locator('iframe[name="checkout-frame"]').contentFrame();
await iframe.locator('input[name="card-number"]').fill('4111111111111111');
await iframe.locator('button[type="submit"]').click();This functionality enables testing of complex web applications that use iframes for embedded content, payment processing, authentication widgets, and third-party integrations.
locator.boundingBox #5076
The browser module now supports locator.boundingBox(), which returns the bounding box of an element as a rectangle with position and size information. This method provides essential geometric data about elements on the page, making it valuable for visual testing, and layout verification.
Using locator.boundingBox() is recommended over elementHandle.boundingBox() because locators have built-in auto-waiting and retry logic, making them more resilient to dynamic content and DOM changes. While element handles can become stale if the page updates, locators represent a live query that gets re-evaluated, ensuring more reliable test execution.
The method returns a rectangle object with x, y, width, and height properties, or null if the element is not visible:
// Get bounding box of an element
const submitButton = page.locator('button[type="submit"]');
const rect = await submitButton.boundingBox();Locator filtering #5114, #5150
The browser module now supports filtering options for locators, allowing you to create more precise and reliable element selections. This enhancement improves the robustness of your tests by enabling you to target elements that contain or exclude specific text, reducing reliance on brittle CSS selectors.
locator.filter() creates a new locator that matches only elements containing or excluding specified text.
// Filter list items that contain specific text
const product2Item = page
.locator('li')
.filter({ hasText: 'Product 2' });
// Filter items that do NOT contain specific text using regex
const otherProducts = page
.locator('li')
.filter({ hasNotText: /Product 2/ });It's also possible to filter locators during their creation with options.
page.locator(selector, options) creates page locators with optional text filtering:
// Create locators with text filtering during creation
const submitButton = page.locator('button', { hasText: 'Submit Order' });
await submitButton.click();frame.locator(selector, options) creates frame locators with optional text filtering:
// Filter elements within frame context
const frame = page.mainFrame();
const input = frame.locator('input', { hasNotText: 'Disabled' });locator.locator(selector, options) chains locators with optional text filtering:
// Chain locators with filtering options
await page
.locator('[data-testid="inventory"]')
.locator('[data-item="apples"]', { hasText: 'Green' })
.click();frameLocator.locator(selector, options) create locators within iframe content with optional text filtering:
// Filter elements within iframe content
const iframe = page.locator('iframe').contentFrame();
await iframe.locator('button', { hasText: 'Submit Payment' }).click();frame.getBy*, locator.getBy*, frameLocator.getBy* #5105, #5106, #5135
The browser module now supports all getBy* methods on frame, locator, and frameLocator types, expanding on the page.getBy* APIs introduced in v1.2.1. This enhancement provides consistent element targeting across all browser automation contexts, improving Playwright compatibility and offering more flexible testing workflows. The available methods on all types are:
getByRole()- Find elements by ARIA rolegetByText()- Find elements by text contentgetByLabel()- Find elements by associated label textgetByPlaceholder()- Find elements by placeholder textgetByAltText()- Find elements by alt textgetByTitle()- Find elements by title attributegetByTestId()- Find elements by data-testid attribute
Examples across different types
// Frame context
const frame = page.mainFrame();
await frame.getByRole('button', { name: 'Submit' }).click();
await frame.getByLabel('Email').fill('user@example.com');
// Locator context (for scoped searches)
const form = page.locator('form.checkout');
await form.getByRole('textbox', { name: 'Card number' }).fill('4111111111111111');
await form.getByTestId('submit-button').click();
// FrameLocator context (for iframe content)
const paymentFrame = page.locator('iframe').contentFrame();
await paymentFrame.getByLabel('Cardholder name').fill('John Doe');
await paymentFrame.getByRole('button', { name: 'Pay now' }).click();
// Chaining for precise targeting
await page
.locator('.product-list')
.getByText('Premium Plan')
.getByRole('button', { name: 'Select' })
.click();This expansion makes k6 browser automation more versatile and aligns with modern testing practices where element targeting by semantic attributes (roles, labels, text) is preferred over fragile CSS and XPath selectors.
`pag...
v1.2.3
v1.2.2
k6 1.2.2 is a small patch release fixing a panic and two other smaller bugfixes.
Bug fixes
- #5067 fixes a panic on the deprecated
k6 login cloudcommand. Thanks @indygriffiths for reporting it! - #5069 Fixes group order in end of test summary when scenarios are used.
- #5070 Adds nullish check to the new getByRole and add tests for other getBy* APIs nullish checks.
v1.2.1
k6 v1.2.1 is here 🎉! This release includes:
- Automatic extension resolution (previously Binary Provisioning) enabled for everyone
- gRPC gets better handling of
NaNandInfinityfloat values and easier health check - Browser module gets
page.route, all thepage.getBy*APIs,locator.all(), andpage.waitForURL
Note: An old xk6-browser repo v1.2.0 tag was pushed by mistake. It was left over on the machine since the merging of the two repos. As such it can not be used as a go module or installed with go install. For this reason v1.2.1 is released.
Breaking changes
As per our stability guarantees,
breaking changes across minor releases are allowed only for experimental features.
Breaking changes for experimental modules
- The experimental Open Telemetry and Prometheus outputs now default to TLSv1.3. This should've been the default to begin with. It is not expected that anyone should be affected, apart from making it more secure for the metrics output to send messages.
New features
Automatic extension resolution
k6 extensions allow you to add custom functionality to your tests, such as connecting to databases, message queues, or specialized networking protocols. Previously, using extensions required manual building of a custom k6 binary with the extensions compiled in. This new version introduces the Automatic Extension Resolution functionality, previously named Binary Provisioning, which is enabled by default and automatically detects when your script imports extensions and handles the complexity of provisioning the right k6 binary for you.
import faker from "k6/x/faker";
export default function () {
console.log(faker.person.firstName());
}The previous experimental versions only supported official extensions. #4922 added the support to use any extension listed in the community list by setting the K6_ENABLE_COMMUNITY_EXTENSIONS environment variable.
K6_ENABLE_COMMUNITY_EXTENSIONS=true k6 run script.js
Note, Community extensions are only supported for local test executions (using k6 run or k6 cloud run --local-execution). When running tests on Grafana Cloud k6, only official extensions are allowed.
Check out the new extensions documentation for additional details.
Handling of NaN and Infinity float values in gRPC #4631
Previously, float values of NaN or Infinity were marshalled as null. This has now changed to use their string representation, aligning with other gRPC APIs.
There are no changes required in the scripts.
This is also the first contribution by @ariasmn. Thank you @ariasmn for taking the time to make the PR and answer all our questions.
Health check for gRPC APIs #4853
The k6 gRPC module now has a client.healthCheck() method that simplifies checking the status of a gRPC service. This method eliminates the need for manual invoke calls, making it particularly useful for readiness checks and service discovery.
Before, you had to write boilerplate code to perform a health check:
import grpc from 'k6/grpc';
const client = new grpc.Client();
// ...
const response = client.invoke('grpc.health.v1.Health/Check', { service: 'my-service' });Now, you can simplify this with the healthCheck() method:
import grpc from 'k6/grpc';
const client = new grpc.Client();
client.connect('grpc.test.k6.io:443');
// Check the health of a specific service
const response = client.healthCheck('my-service');
// Check the health of the overall gRPC server
const overallResponse = client.healthCheck();
client.close();Check out the client.healthCheck documentation for additional details.
Thank you, @tbourrely, for contributing this feature.
Assertions Library (Preview) #4067
k6 now provides an assertions library to help you verify your application behaves as expected during testing.
The library introduces the expect function with a set of expressive matchers. Pass a value to expect() and chain it with a matcher that defines the expected outcome. The library caters to both protocol testing HTTP/API and browser testing scenarios.
The API is inspired by Playwright's assertion syntax, offering a fluent interface for more readable and reliable tests.
import { expect } from 'https://jslib.k6.io/k6-testing/0.5.0/index.js';
import { browser } from 'k6/browser';
import http from 'k6/http';
export function protocolTest() {
// Get the home page of k6's Quick Pizza app
const response = http.get('https://quickpizza.grafana.com/');
// Simple assertions
expect(response.status).toBe(200);
expect(response.error).toEqual('');
expect(response.body).toBeDefined();
}
export async function browserTest() {
const page = await browser.newPage();
try {
await page.goto('https://quickpizza.grafana.com/');
// Assert the "Pizza Please" button is visible
await expect(page.locator('button[name=pizza-please]')).toBeVisible();
} finally {
await page.close();
}
}
export const options = {
scenarios: {
// Protocol tests
protocol: {
executor: 'shared-iterations',
vus: 1,
iterations: 1,
exec: 'protocolTest',
},
// Browser tests
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
exec: 'browserTest',
},
},
};Preview feature
This feature is ready to use, but still in preview:
- No breaking changes are neither planned, nor expected.
- Some functionality may be missing or rough around the edges.
- We expect to keep adding matchers and improving coverage.
We welcome your feedback, and invite you to share your suggestions and contributions on GitHub.
Add page.getByRole API #4843
The browser module now supports page.getByRole(), which allows you to locate elements based on their ARIA roles. This provides a more semantic and accessible way to find elements, making your tests more robust and aligned with how users actually interact with web applications.
ARIA roles represent the purpose or function of an element (like button, link, textbox, etc.), making them excellent selectors for testing since they're less likely to change when the UI is refactored compared to CSS classes or IDs.
Example usage:
// Find elements by role
await page.getByRole('button').click();
// Find elements by role and accessible name
await page.getByRole('button', { name: 'Submit' }).click();
// `name` works with regex too
await page.getByRole('textbox', { name: /^Username$/ }).fill('admin');
// Work with specific states
await page.getByRole('checkbox', { name: 'Accept terms', checked: true }).click();
// Find headings by level
await page.getByRole('heading', { level: 2, name: 'Section Title' }).textContent();
### Add `page.getByAltText` [#4881](https://github.com/grafana/k6/pull/4881)
The browser module now includes `page.getByAltText()`, which provides a convenient way to select elements that have an `alt` text attribute. This is particularly useful for locating images or other elements that rely on alternative text for accessibility.
Previously, you would have to use CSS or XPath selectors to find these elements:
```javascript
// Using CSS selector
const locator = page.locator('img[alt="World Map"]');
// Using XPath selector
const locator = page.locator('//img[@alt="World Map"]');Now, you can simplify this by using getByAltText():
const locator = page.getByAltText('World Map');
// Find an image with alt text that starts with 'World'
const locator = page.getByAltText(/^World/);Add page.getByLabel #4890
The browser module now includes page.getByLabel(), which provides a convenient way to locate form elements and other interactive components by their associated label text. This method works with both explicit <label> elements and elements that have an aria-label attribute, making it particularly useful for finding form inputs, buttons, and other interactive elements.
Previously, you would need to use XPath selectors to find elements by their label text, since CSS selectors cannot easily handle the relationship between labels and form elements:
// Using XPath to find input by label text
const locator = page.locator('//label[text()="Password"]');
// Or using aria-label with CSS
const locator = page.locator('[aria-label="Username"]');Now, you can simplify this with getByLabel():
// Works with both <label> elements and aria-label attributes
const passwordInput = page.getByLabel('Password');
// Works with regex too
const usernameInput = page.getByLabel(/^Username$/);