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://test.k6.io/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 Adds multi-source secret example for better documentation of secret management patterns.
Bug fixes
- #5374 Fixes
getBy*selectors when using quotes inside element names. - #5477 Fixes retry mechanism when frame has been detached.
- #5461 Fixes panic when using nil
page.onhandlers. - #5401 Fixes panic when assigning to nil headers and cookies when the host is not found. Thanks, @chojs23, for the contribution π
- #5379 Fixes browsers not being stopped in tests due to
EndIteration. - #5439 Fixes loading files with spaces.
- #5406 Fixes error messages after Sobek/goja changes.
- #5381 Fixes version command JSON output for output extensions.
- #5358 Fixes sending correct data when using
ArrayViewsin experimental/websockets. - #5488 Fixes a goroutine leak when performing CDP requests.
Maintenance and internal improvements
- #5464 Adds
ExecutionStatusMarkedAsFailedstatus for improved test execution tracking. - #5467 Refactors span error recording to avoid boilerplate code.
- #5438 Unblocks the release workflow for package publishing.
- #5436 Optimizes browser module allocations and improves CI stability.
- #5411 Extends base config and stops updating go.mod toolchain.
- #5408 Removes redundant GitHub Actions rule.
- #5415 Updates Sobek dependency to latest version.
- #5392 Updates OpenTelemetry proto module to v1.9.0.
- #5357 Refactors browser module task queue usage.
- #5378 Fixes
TestNavigationSpanCreationtest in the browser module. - #5482 Fixes tests and subcommand handling in the version command.
- #5255 Updates gRPC module to v1.77.0.
- #5506 Updates xk6-redis to v0.3.6.
- #5473 Updates compression library to v1.18.2.
- #5505 Removes
closecall in integration tests. - #5517 Removes
SECURITY.mdto sync with Grafana's org-wide security policy documentation. - #5528 Resolves CVE-2025-61729. Thanks, @SimKev2, for the contribution π
Roadmap
Deprecation of First Input Delay (FID) Web Vital
Following the official web vitals guidance, First Input Delay (FID) is no longer a Core Web Vital as of September 9, 2024, having been replaced by Interaction to Next Paint (INP). The k6 browser module already emits INP metrics, and we're planning to deprecate FID support to align with industry standards.
FID only measures the delay before the browser runs your event handler, so it ignores the time your code takes and the delay to paint the UIβoften underestimating how slow an interaction feels. INP captures the full interaction latency (input delay + processing + next paint) across a pageβs interactions, so it better reflects real user-perceived responsiveness and is replacing FID.
Action required
If you're currently using FID in your test scripts for thresholds or relying on it in external integrations, you should migrate to using INP as soon as possible.
// Instead of relying on FID
export const options = {
thresholds: {
// 'browser_web_vital_fid': ['p(95)<100'], // Deprecated
'browser_web_vital_inp': ['p(95)<200'], // Use INP instead
},
};This change ensures k6 browser testing stays aligned with modern web performance best practices and Core Web Vitals standards.