k6 v1.2.0 is here 🎉! This release includes:

- Automatic extension resolution (previously Binary Provisioning) enabled for everyone
- gRPC gets better handling of `NaN` and `Infinity` float values and easier health check
- Browser module gets `page.route`, all the `page.getBy*` APIs, `locator.all()`, and `page.waitForURL`

## Breaking changes

As per our [stability guarantees](https://grafana.com/docs/k6/latest/reference/versioning-and-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](https://grafana.com/docs/k6/latest/extensions/run/build-k6-binary-using-go) 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.

```javascript
import faker from "k6/x/faker";

export default function () {
  console.log(faker.person.firstName());
}
```

The previous experimental versions only supported official extensions. [#4922](https://github.com/grafana/k6/pull/4922) added the support to use any extension listed in the [community list](https://grafana.com/docs/k6/latest/extensions/explore/) 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](https://grafana.com/docs/k6/latest/extensions/) for additional details.

### Handling of NaN and Infinity float values in gRPC [#4631](https://github.com/grafana/k6/pull/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](https://github.com/grafana/k6/pull/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:
```javascript
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:
```javascript
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](https://grafana.com/docs/k6/latest/using-k6/protocols/grpc/#health-checking-protocol) documentation for additional details.
Thank you, @tbourrely, for contributing this feature.

### Assertions Library (Preview) [#4067](https://github.com/grafana/k6/issues/4067)

k6 now provides an [assertions](https://grafana.com/docs/k6/latest/using-k6/assertions) library to help you verify your application behaves as expected during testing.

The library introduces the [`expect`](https://grafana.com/docs/k6/latest/javascript-api/jslib/testing/expect) function with a set of expressive matchers. Pass a value to [`expect()`](https://grafana.com/docs/k6/latest/javascript-api/jslib/testing/expect) and chain it with a matcher that defines the expected outcome. The library caters to both protocol testing [HTTP/API](https://grafana.com/docs/k6/latest/using-k6/protocols) and [browser](https://grafana.com/docs/k6/latest/using-k6-browser) testing scenarios.

The API is inspired by Playwright's assertion syntax, offering a fluent interface for more readable and reliable tests.

```javascript
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](https://github.com/grafana/k6-jslib-testing).

### Add `page.getByRole` API [#4843](https://github.com/grafana/k6/pull/4843)

The browser module now supports `page.getByRole()`, which allows you to locate elements based on their [ARIA roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/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:
```javascript
// 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()`:
```javascript
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](https://github.com/grafana/k6/pull/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:

```javascript
// 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()`:

```javascript
// Works with both <label> elements and aria-label attributes
const passwordInput = page.getByLabel('Password');

// Works with regex too
const usernameInput = page.getByLabel(/^Username$/);
```

### Add `page.getByPlaceholder` [#4904](https://github.com/grafana/k6/pull/4904)

The browser module now includes `page.getByPlaceholder()`, which provides a convenient way to locate form elements by their placeholder text. This is particularly useful for finding input fields, textareas, and other form controls that use placeholder text to guide user input.

Previously, you would need to use CSS or XPath selectors to find elements by their placeholder attribute:

```javascript
// Using CSS selector
const locator = page.locator('input[placeholder="Enter your name"]');

// Using XPath selector  
const locator = page.locator('//input[@placeholder="Enter your name"]');
```

Now, you can simplify this with `getByPlaceholder()`:

```javascript
const nameInput = page.getByPlaceholder('Enter your name');

// Works with regex too
const emailInput = page.getByPlaceholder(/^Email/);
```

### Add `page.getByTitle` [#4910](https://github.com/grafana/k6/pull/4910)

The browser module now includes `page.getByTitle()`, which provides a convenient way to locate elements by their `title` attribute. This is particularly useful for finding tooltips, buttons, or any other elements that use the `title` attribute to provide extra information.

Previously, you would need to use CSS or XPath selectors to find these elements:

```javascript
// Using CSS selector
const locator = page.locator('div[title="Information box"]');

// Using XPath selector
const locator = page.locator('//div[@title="Information box"]');
```

Now, you can simplify this with `getByTitle()`:

```javascript
const infoBox = page.getByTitle('Information box');

// Works with regex too
const saveButton = page.getByTitle(/^Save/);
```

### Add `page.getByTestId` [#4911](https://github.com/grafana/k6/pull/4911)

The browser module now includes `page.getByTestId()`, which provides a convenient way to locate elements by their `data-testid` attribute. This is particularly useful for creating resilient tests that are not affected by changes to the UI, since `data-testid` attributes are specifically added for testing purposes and are not expected to change.

Previously, you would need to use CSS or XPath selectors to find these elements:

```javascript
// Using CSS selector
const locator = page.locator('button[data-testid="submit-button"]');

// Using XPath selector
const locator = page.locator('//button[@data-testid="submit-button"]');
```

Now, you can simplify this with `getByTestId()`:

```javascript
const submitButton = page.getByTestId('submit-button');

// Works with regex too
const usernameInput = page.getByTestId(/^username/);
```

### Add `page.getByText` [#4912](https://github.com/grafana/k6/pull/4912)

The browser module now includes `page.getByText()`, which allows you to locate elements by their text content. This provides a convenient way to find elements like buttons, links, and other interactive components that are identified by their visible text.

Previously, you would need to use XPath selectors to find elements by their text content, since CSS selectors cannot directly query the text of an element:

```javascript
// Using XPath selector
const locator = page.locator('//div[text()="Hello World"]');
```

Now, you can simplify this with `getByText()`:

```javascript
const helloWorldElement = page.getByText('Hello World');

// Works with regex too
const submitButton = page.getByText(/^Submit/);
```

### Add `page.route` [#4953](https://github.com/grafana/k6/pull/4953) [#4961](https://github.com/grafana/k6/pull/4961), [#4971](https://github.com/grafana/k6/pull/4971), [#4985](https://github.com/grafana/k6/pull/4985)

The browser module now supports `page.route()`, which allows you to intercept and handle network requests before they are sent. This is particularly useful for testing scenarios where you need to mock API responses, block certain resources, or modify request behavior.

The route handler receives a `route` object that provides methods to `abort()`, `continue()`, or `fulfill()` the request.

You can use `page.route()` to:
- **Block requests**: Prevent certain resources from loading (e.g., images, ads, analytics) with `abort()`.
    ```javascript
    // Block all image requests
    await page.route(/(\.png$)|(\.jpg$)|(\.jpeg$)/, async (route) => {
        await route.abort();
    });
    ```
- **Mock responses**: Return custom responses without hitting real endpoints with `fulfill()`.
    ```javascript
    // Mock API responses
    await page.route('**/api/users', async (route) => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify([{ id: 1, name: 'Mock User' }])
        });
    });
    ```
- **Modify requests**: Change headers, URL, or request body before they're sent with `continue()`.
    ```javascript
    // Continue with modified headers
    await page.route('**/api/**', async (route) => {
        await route.continue({
            headers: {
            ...route.request().headers(),
            'Authorization': 'Bearer mock-token'
            }
        });
    });
    ```

### Add `locator.all()` [#4899](https://github.com/grafana/k6/pull/4899)

The browser module now supports the `locator.all()` method, which returns an array of locators for all elements matching the selector. This is particularly useful when you need to interact with multiple similar elements on a page, such as items in a list or multiple buttons with the same styling.

Example usage:
```javascript
// Get all list items and iterate through them
const items = await page.locator('li').all();
for (const item of items) {
  console.log(await item.textContent());
}
```

### Add `waitForURL` in `frame` and `page` [#4917](https://github.com/grafana/k6/pull/4917), [#4920](https://github.com/grafana/k6/pull/4920)

The browser module now includes the `waitForURL` method for both `page` and `frame` objects.

As a prerequiste to this enhancement, `waitForNavigation` now accepts a `url` option. This also allows you to wait for a specific URL during navigation. **It is advised that you work with `waitForURL` instead**.

The `waitForURL` method first checks if the current page URL already matches the expected pattern. If it does, it waits for the load state to complete. Otherwise, it waits for a navigation to the specified URL. This approach prevents race conditions where a page might complete navigation before the wait condition is set up, which is particularly useful when dealing with pages that perform multiple redirects. It supports both string patterns and regular expressions:

```javascript
// Wait for navigation to a specific URL
await Promise.all([
  page.waitForURL('https://quickpizza.grafana.com/my_messages.php'),
  page.locator('a[href="/my_messages.php"]').click(),
]);

// Using regex pattern
await Promise.all([
  page.waitForURL(/.*\/contacts\.php.*/),
  page.locator('a[href^="/contacts.php"]').click()
]);
```

While `waitForURL` provides a convenient way to wait for specific URLs, we still recommend using element-based waiting strategies or the locator API with its built-in auto-waiting capabilities for more reliable tests.

## UX improvements and enhancements

- [#4878](https://github.com/grafana/k6/pull/4878) Do not report NaN percentages when there are no checks in the end of test summary. Thank you @Fernando-hub527 for the fix.
- [#4897](https://github.com/grafana/k6/pull/4897) Support string-labels in `locator.selectOption` in the browser module.
- [#4898](https://github.com/grafana/k6/pull/4898) Add support for `authority` pseudo header to the gRPC module. Thank you @Oursin for the changes.
- [#4916](https://github.com/grafana/k6/pull/4916) Print errors more consistently and correctly.
- [#4918](https://github.com/grafana/k6/pull/4918) Surface navigation errors from navigation events in the browser module.
- [#4919](https://github.com/grafana/k6/pull/4919) `page.url()` now doesn't make a call to the browser but instead uses a cached version. Making it a lot faster and aligned with playwright.
- [#4932](https://github.com/grafana/k6/pull/4932) Making it more clear that the requests were aborted in the browser module.
- [#4944](https://github.com/grafana/k6/pull/4944) Add Prometheus metrics endpoint. Thank you @gouthamve.
- [#5040](https://github.com/grafana/k6/pull/5040) Use new `expect()` syntax in script templates.
- [#4976](https://github.com/grafana/k6/pull/4976) Align metrics in end of test summary with using less dots and less horizontal space.

## Bug fixes

- [#4850](https://github.com/grafana/k6/pull/4850) Fixes incorrect conversions between integer types in the browser module.
- [#4973](https://github.com/grafana/k6/pull/4973) Fixes panic when `BrowserContext` is requested before creating a `Page` in the browser module.
- [#4975](https://github.com/grafana/k6/pull/4975) Fixes potential race conditions in the browser module.
- [#5015](https://github.com/grafana/k6/pull/5015) Fixes `waitForNavigation` now blocking the iteration from ending if `page.close` is not called.
- [#5017](https://github.com/grafana/k6/pull/5017) Fixes potential race conditions in gRPC module, when it gets protobuf defintions from remote server.

## Maintenance and internal improvements

- [#4666](https://github.com/grafana/k6/pull/4666) Lowers the log level on some cloud output logs that were too noisy.
- [#4879](https://github.com/grafana/k6/pull/4879), [#4945](https://github.com/grafana/k6/pull/4945) Updates of software used by the internal k6packager that packages k6.
- [#4886](https://github.com/grafana/k6/pull/4886) Prevents running Winsign for public forks to avoid failing and wasting CI time.
- [#4892](https://github.com/grafana/k6/pull/4892), [#4901](https://github.com/grafana/k6/pull/4901), [#4909](https://github.com/grafana/k6/pull/4909), [#4927](https://github.com/grafana/k6/pull/4927), [#4928](https://github.com/grafana/k6/pull/4928), [#4929](https://github.com/grafana/k6/pull/4929), [#4939](https://github.com/grafana/k6/pull/4939), [#4954](https://github.com/grafana/k6/pull/4954), [#4965](https://github.com/grafana/k6/pull/4965), [#4977](https://github.com/grafana/k6/pull/4977), [#5000](https://github.com/grafana/k6/pull/5000), [#5001](https://github.com/grafana/k6/pull/5001), [#5041](https://github.com/grafana/k6/pull/5041), [#5043](https://github.com/grafana/k6/pull/5043) Updates dependencies.
- [#4925](https://github.com/grafana/k6/pull/4925) Configures Dependabot to update all OTel dependencies.
- [#4914](https://github.com/grafana/k6/pull/4914), [#4967](https://github.com/grafana/k6/pull/4967), [#4974](https://github.com/grafana/k6/pull/4974), [#4990](https://github.com/grafana/k6/pull/4990), [#4991](https://github.com/grafana/k6/pull/4991), [#5007](https://github.com/grafana/k6/pull/5007), [#5008](https://github.com/grafana/k6/pull/5008) PRs trying to get the CI tests to be more stable.
- [#4923](https://github.com/grafana/k6/pull/4923) Mention k6 studio in the readme for more visibility.
- [#4931](https://github.com/grafana/k6/pull/4931) Lower the debug log on internal implementation detail error.
- [#4943](https://github.com/grafana/k6/pull/4943), [#4946](https://github.com/grafana/k6/pull/4946), [#4948](https://github.com/grafana/k6/pull/4948) Fix the CI not checking generated files and pinning versions.
- [#4949](https://github.com/grafana/k6/pull/4949) Updates to Linux Alpine 3.22 for building the k6 docker image.
- [#4950](https://github.com/grafana/k6/pull/4950) Adds Dependabot rule to update the main Dockerfile dependencies.
- [#4958](https://github.com/grafana/k6/pull/4958) Passes k6's file system to k6deps (for automatic extension resolution).
- [#4959](https://github.com/grafana/k6/pull/4959) Copies Zizmor configuration in the repository to make it work for forks.
- [#4960](https://github.com/grafana/k6/pull/4960) Reduces `waitForNavigation` complexity.
- [#4969](https://github.com/grafana/k6/pull/4969), [#4986](https://github.com/grafana/k6/pull/4986) Updates around semgrep scanning for security issues.
- [#4994](https://github.com/grafana/k6/pull/4994) Configures Dependabot to update dependencies in Github actions.
- [#4996](https://github.com/grafana/k6/pull/4996), [#4997](https://github.com/grafana/k6/pull/4997), [#4998](https://github.com/grafana/k6/pull/4998) Updates to github actions dependancies.
- [#5014](https://github.com/grafana/k6/pull/5014) Adds a test for running tests with the automatic extension resolution disabled.
- [#5016](https://github.com/grafana/k6/pull/5016) Fix internal/cmd/tests with automatic extension resolution enabled.
- [#5020](https://github.com/grafana/k6/pull/5020) Fix examples with browser using the wrong endpoints.
