diff --git a/docs/sources/k6/next/using-k6-browser/_index.md b/docs/sources/k6/next/using-k6-browser/_index.md index 20584a8812..d394e0958f 100644 --- a/docs/sources/k6/next/using-k6-browser/_index.md +++ b/docs/sources/k6/next/using-k6-browser/_index.md @@ -1,6 +1,6 @@ --- aliases: - - ./examples/crawl-webpage # docs/k6//examples/crawl-webpage + - ./using-k6-browser # docs/k6//using-k6-browser title: Using k6 browser description: 'The browser module brings browser automation and end-to-end testing to k6 while supporting core k6 features. Interact with real browsers and collect frontend metrics as part of your k6 tests.' weight: 300 @@ -31,58 +31,26 @@ The main use case for the browser module is to test performance on the browser l - Are all my elements interactive on the frontend? - Are there any loading spinners that take a long time to disappear? -## A simple browser test - -{{< code >}} - -```javascript -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', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const context = await browser.newContext(); - const page = await context.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - await Promise.all([page.waitForNavigation(), page.locator('input[type="submit"]').click()]); - - await check(page.locator('h2'), { - header: async (h2) => (await h2.textContent()) == 'Welcome, admin!', - }); - } finally { - await page.close(); - } -} -``` +## Create a browser test + +To create a browser test, you first need to: -{{< /code >}} +- [Install k6](https://grafana.com/docs/k6//set-up/install-k6/) in your machine. +- Install a Chromium-based browser, such as Google Chrome, in your machine. -The preceding code launches a Chromium-based browser, visits the application and mimics a user logging in to the application. Once submitted, it checks if the text of the header matches what is expected. +After, run the `k6 new` command with the `--template` option set to `browser`: -After running the test, the following [browser metrics](https://grafana.com/docs/k6//using-k6-browser/metrics) will be reported. +```bash +k6 new --template browser browser-script.js +``` -{{< code >}} +The command creates a test script you can run right away with the `k6 run` command: + +```bash +k6 run browser-script.js +``` + +After running the test, you can see the [end of test results](https://grafana.com/docs/k6//results-output/end-of-test/). It contains metrics that show the performance of the website on the script. ```bash /\ Grafana /‾‾/ @@ -91,38 +59,69 @@ After running the test, the following [browser metrics](https://grafana.com/docs / \ | ( | (‾) | / __________ \ |_|\_\ \_____/ - execution: local - script: test.js - output: - - - scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): - * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) - - -running (00m01.3s), 0/1 VUs, 1 complete and 0 interrupted iterations -ui ✓ [======================================] 1 VUs 00m01.3s/10m0s 1/1 shared iters - - ✓ header - - browser_data_received.......: 2.6 kB 2.0 kB/s - browser_data_sent...........: 1.9 kB 1.5 kB/s - browser_http_req_duration...: avg=215.4ms min=124.9ms med=126.65ms max=394.64ms p(90)=341.04ms p(95)=367.84ms - browser_http_req_failed.....: 0.00% ✓ 0 ✗ 3 - browser_web_vital_cls.......: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 - browser_web_vital_fcp.......: avg=344.15ms min=269.2ms med=344.15ms max=419.1ms p(90)=404.11ms p(95)=411.6ms - browser_web_vital_fid.......: avg=200µs min=200µs med=200µs max=200µs p(90)=200µs p(95)=200µs - browser_web_vital_inp.......: avg=8ms min=8ms med=8ms max=8ms p(90)=8ms p(95)=8ms - browser_web_vital_lcp.......: avg=419.1ms min=419.1ms med=419.1ms max=419.1ms p(90)=419.1ms p(95)=419.1ms - browser_web_vital_ttfb......: avg=322.4ms min=251ms med=322.4ms max=393.8ms p(90)=379.52ms p(95)=386.66ms - ✓ checks......................: 100.00% ✓ 1 ✗ 0 - data_received...............: 0 B 0 B/s - data_sent...................: 0 B 0 B/s - iteration_duration..........: avg=1.28s min=1.28s med=1.28s max=1.28s p(90)=1.28s p(95)=1.28s - iterations..................: 1 0.777541/s - vus.........................: 1 min=1 max=1 - vus_max.....................: 1 min=1 max=1 + execution: local + script: script.js + output: - + + scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): + * ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) + + + + █ TOTAL RESULTS + + checks_total.......................: 2 0.300669/s + checks_succeeded...................: 100.00% 2 out of 2 + checks_failed......................: 0.00% 0 out of 2 + + ✓ header + ✓ recommendation + + HTTP + http_req_duration.......................................................: avg=122ms min=122ms med=122ms max=122ms p(90)=122ms p(95)=122ms + { expected_response:true }............................................: avg=122ms min=122ms med=122ms max=122ms p(90)=122ms p(95)=122ms + http_req_failed.........................................................: 0.00% 0 out of 1 + http_reqs...............................................................: 1 0.150334/s + + EXECUTION + iteration_duration......................................................: avg=4.39s min=4.39s med=4.39s max=4.39s p(90)=4.39s p(95)=4.39s + iterations..............................................................: 1 0.150334/s + vus.....................................................................: 1 min=0 max=1 + vus_max.................................................................: 1 min=1 max=1 + + NETWORK + data_received...........................................................: 6.9 kB 1.0 kB/s + data_sent...............................................................: 543 B 82 B/s + + BROWSER + browser_data_received...................................................: 357 kB 54 kB/s + browser_data_sent.......................................................: 4.9 kB 738 B/s + browser_http_req_duration...............................................: avg=355.28ms min=124.04ms med=314.4ms max=1.45s p(90)=542.75ms p(95)=753.09ms + browser_http_req_failed.................................................: 0.00% 0 out of 18 + + WEB_VITALS + browser_web_vital_cls...................................................: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 + browser_web_vital_fcp...................................................: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s + browser_web_vital_fid...................................................: avg=300µs min=300µs med=300µs max=300µs p(90)=300µs p(95)=300µs + browser_web_vital_inp...................................................: avg=56ms min=56ms med=56ms max=56ms p(90)=56ms p(95)=56ms + browser_web_vital_lcp...................................................: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s + browser_web_vital_ttfb..................................................: avg=1.45s min=1.45s med=1.45s max=1.45s p(90)=1.45s p(95)=1.45s ``` -{{< /code >}} +You can also see at the end of the output the browser and Web Vital metrics that report performance specific to browser testing. -This gives you a representation of browser performance, via the web vitals, as well as the HTTP requests that came from the browser. +```bash +BROWSER +browser_data_received.........: 357 kB 54 kB/s +browser_data_sent.............: 4.9 kB 738 B/s +browser_http_req_duration.....: avg=355.28ms min=124.04ms med=314.4ms max=1.45s p(90)=542.75ms p(95)=753.09ms +browser_http_req_failed.......: 0.00% 0 out of 18 + +WEB_VITALS +browser_web_vital_cls.........: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 +browser_web_vital_fcp.........: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s +browser_web_vital_fid.........: avg=300µs min=300µs med=300µs max=300µs p(90)=300µs p(95)=300µs +browser_web_vital_inp.........: avg=56ms min=56ms med=56ms max=56ms p(90)=56ms p(95)=56ms +browser_web_vital_lcp.........: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s +browser_web_vital_ttfb........: avg=1.45s min=1.45s med=1.45s max=1.45s p(90)=1.45s p(95)=1.45s +``` diff --git a/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/_index.md b/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/_index.md new file mode 100644 index 0000000000..7fb3c5c1a5 --- /dev/null +++ b/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/_index.md @@ -0,0 +1,9 @@ +--- +title: How to write browser tests +description: 'Learn how to write k6 browser tests.' +weight: 300 +--- + +# How to write browser tests + +{{< section >}} diff --git a/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md b/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md new file mode 100644 index 0000000000..92aa82c3a0 --- /dev/null +++ b/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md @@ -0,0 +1,150 @@ +--- +title: Asynchronous operations +description: 'Learn how the k6 browser module uses asynchronous operations.' +weight: 100 +--- + +# Asynchronous operations + +Most methods in the browser module return [JavaScript promises](#why-the-browser-module-uses-asynchronous-apis), and k6 scripts must be written to handle this properly. This usually means using the `await` keyword to wait for the async operation to complete. + +For example: + + + +```js +const page = await browser.newPage(); + +await page.goto('https://quickpizza.grafana.com/'); + +const locator = page.locator('button[name="pizza-please"]'); + +await locator.click(); +``` + +In addition to using `await`, another important part of writing k6 browser tests is handling page navigations. There are two recommended methods for doing that: using `Promise.all` or using the `waitFor` method. + +## Promise.all + +To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. + +{{< code >}} + +```javascript +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', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = await browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + await page.locator('input[name="login"]').type('admin'); + await page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + + await check(page.locator('h2'), { + header: async (lo) => (await lo.textContent()) == 'Welcome, admin!', + }); + } finally { + await page.close(); + } +} +``` + +{{< /code >}} + +The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. + +Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. + +## Wait for specific elements + +We also encourage the use of `locator.waitFor` where possible. When you navigate to a website, there are usually one or more elements that are important for your test. Once those elements load, you can safely proceed to the next step. For example, in a search scenario: + +1. Navigate to the search site +1. Wait for the search bar and submit button to appear +1. Fill in the search bar with the query +1. Click the submit button +1. Wait for the search results + +We should be able to do these actions like so: + + + +```js +await page.goto('https://my-search-engine.com'); + +const searchBar = page.locator('.search-bar'); +const submitButton = page.locator('.submit-button'); + +await searchBar.waitFor(); +await submitButton.waitFor(); + +await searchBar.fill('k6'); +await submitButton.click(); + +const searchResults = page.locator('.search-results-table'); + +await searchResults.waitFor(); +``` + +This avoids the use of `Promise.all` which can be confusing to work with, and instead makes the script easier to follow. + +## Why the browser module uses asynchronous APIs + +The browser module uses asynchronous APIs that require `await` for several reasons: + +1. JavaScript is single-threaded with a single event loop. Asynchronous APIs prevent blocking the thread and event loop with long-running or I/O-based tasks. +1. Consistency with [Playwright](https://playwright.dev/), a popular browser automation library. +1. Alignment with how developers expect to work with modern JavaScript APIs. + +For example: + + + +```js +const page = await browser.newPage(); + +await page.goto('https://quickpizza.grafana.com/'); + +const locator = page.locator('button[name="pizza-please"]'); + +await locator.click(); +``` + +API calls that interact with Chromium are asynchronous and require `await` to ensure completion before proceeding. Synchronous APIs, such as `page.locator`, do not require `await`, but using it does not cause issues since the JavaScript runtime will simply return the value immediately. + +If you don't add `await` on asynchronous APIs, it can cause the script to finish before the test completes, resulting in errors like `"Uncaught (in promise) TypeError: Object has no member 'goto'"`. That can happen because the page object from `browser.newPage()` without an `await` is actually a JavaScript promise. You can try and see the error using the following code snippet: + + + +```js +const page = browser.newPage(); + +page.goto('https://quickpizza.grafana.com/'); // An error should occur since we're not using await in the line above. + +const locator = page.locator('button[name="pizza-please"]'); + +locator.click(); +``` diff --git a/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md b/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md new file mode 100644 index 0000000000..f671d96b06 --- /dev/null +++ b/docs/sources/k6/next/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md @@ -0,0 +1,61 @@ +--- +title: Interact with elements on your webpage +description: 'Learn how to interact with elements on a webpage using the k6 browser module.' +weight: 200 +--- + +# Interact with elements on your webpage + +You can use `page.locator()` and pass in the element's selector you want to find on the page. `page.locator()` will create and return a [Locator](https://grafana.com/docs/k6//javascript-api/k6-browser/locator) object, which you can later use to interact with the element. + +To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). + +{{< admonition type="note" >}} + +You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-browser/locator). + +{{< /admonition >}} + +{{< code >}} + +```javascript +import { browser } from 'k6/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = await browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + // Enter login credentials + await page.locator('input[name="login"]').type('admin'); + await page.locator('input[name="password"]').type('123'); + + await page.screenshot({ path: 'screenshots/screenshot.png' }); + } finally { + await page.close(); + } +} +``` + +{{< /code >}} + +The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. + +Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. diff --git a/docs/sources/k6/next/using-k6-browser/metrics.md b/docs/sources/k6/next/using-k6-browser/metrics.md index d1b36d76bf..c78cb6a4ef 100644 --- a/docs/sources/k6/next/using-k6-browser/metrics.md +++ b/docs/sources/k6/next/using-k6-browser/metrics.md @@ -1,7 +1,7 @@ --- title: 'Browser metrics' description: 'An overview of the different browser performance metrics that the browser module tracks.' -weight: 03 +weight: 400 --- # Browser metrics @@ -86,7 +86,7 @@ Currently, you can only use URLs to specify thresholds for different pages. If y {{< /admonition >}} -{{< code >}} + ```javascript export const options = { @@ -98,8 +98,6 @@ export const options = { }; ``` -{{< /code >}} - When the test is run, you should see a similar output as the one below. ```bash diff --git a/docs/sources/k6/next/using-k6-browser/migrating-to-k6-v0-52.md b/docs/sources/k6/next/using-k6-browser/migrating-to-k6-v0-52.md index 3210f79587..44c18eea87 100644 --- a/docs/sources/k6/next/using-k6-browser/migrating-to-k6-v0-52.md +++ b/docs/sources/k6/next/using-k6-browser/migrating-to-k6-v0-52.md @@ -3,7 +3,7 @@ aliases: - ./migrating-to-k6-v0-46/ # docs/k6//using-k6-browser/migrating-to-k6-v0-46 title: 'Migrating browser scripts to k6 v0.52' description: 'A migration guide to ease the process of transitioning to the new k6 browser module version bundled with k6 v0.52' -weight: 05 +weight: 600 --- # Migrating browser scripts to k6 v0.52 @@ -35,8 +35,7 @@ To ensure your scripts work with the latest release of the k6 browser module, yo For example, before: -{{< code >}} - + ```javascript @@ -50,12 +49,9 @@ export default async function () { } ``` -{{< /code >}} - And now: -{{< code >}} - + ```javascript @@ -69,8 +65,6 @@ export default async function () { } ``` -{{< /code >}} - You might have already encountered async APIs when working with the browser module, such as [page.click](http://grafana.com/docs/k6//javascript-api/k6-browser/page/click/), so the use of `async` and `await` keywords might be familiar to you. Below is a screenshot of a comparison between a generic browser test in `v0.51` and `v0.52` to help visualize the change: @@ -279,22 +273,18 @@ The k6 `check` API will not `await` promises, so calling a function that returns For example, before: -{{< code >}} - + ```javascript check(page.locator('h2'), { - header: lo => lo.textContent() == 'Welcome, admin!', + header: (lo) => lo.textContent() == 'Welcome, admin!', }); ``` -{{< /code >}} - And now: -{{< code >}} - + ```javascript @@ -303,12 +293,10 @@ import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; // ... await check(page.locator('h2'), { - 'header': async lo => await lo.textContent() === 'Welcome, admin!' + header: async (lo) => (await lo.textContent()) === 'Welcome, admin!', }); ``` -{{< /code >}} - ## Groups A note on [groups](https://grafana.com/docs/k6//using-k6/tags-and-groups/#groups), they don't work with async APIs either, there is no workaround as of yet. Here's the [GitHub issue](https://github.com/grafana/k6/issues/2728) that you can follow to keep up-to-date with relevant news on a group API that works with async APIs. diff --git a/docs/sources/k6/next/using-k6-browser/options.md b/docs/sources/k6/next/using-k6-browser/options.md index 7aff806738..ff8d50b922 100644 --- a/docs/sources/k6/next/using-k6-browser/options.md +++ b/docs/sources/k6/next/using-k6-browser/options.md @@ -1,13 +1,15 @@ --- title: 'Browser options' description: 'An overview of the different options you can use to customize the browser module behavior when running browser tests.' -weight: 04 +weight: 500 --- # Browser options To enable browser testing, add the `browser` configuration within the `options` property of the [Scenario options](https://grafana.com/docs/k6//using-k6/scenarios/#options). + + ```javascript export const options = { scenarios: { diff --git a/docs/sources/k6/next/using-k6-browser/recommended-practices/_index.md b/docs/sources/k6/next/using-k6-browser/recommended-practices/_index.md index 2f59344031..60caa9aca7 100644 --- a/docs/sources/k6/next/using-k6-browser/recommended-practices/_index.md +++ b/docs/sources/k6/next/using-k6-browser/recommended-practices/_index.md @@ -1,7 +1,7 @@ --- title: 'Recommended practices' description: 'A list of different examples and recommended practices when working with the k6 browser module' -weight: 100 +weight: 350 --- # Recommended practices diff --git a/docs/sources/k6/next/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md b/docs/sources/k6/next/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md index f046d7b260..c629c598b4 100644 --- a/docs/sources/k6/next/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md +++ b/docs/sources/k6/next/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md @@ -10,10 +10,24 @@ weight: 01 An alternative approach to [browser-based load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites/#browser-based-load-testing) that's much less resource-intensive is combining a small number of virtual users for a browser test with a large number of virtual users for a protocol-level test. -You can achieve hybrid performance in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. +You can achieve [hybrid performance](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. + +Some of the advantages of running a hybrid performance test are: + +- Testing real user flows on the frontend while generating a higher load in the backend. +- Measuring backend and frontend performance in the same test execution +- Increased collaboration between backend and frontend teams since the same tool can be used. + +{{< admonition type="note" >}} + +Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. + +{{< /admonition >}} ## Browser and HTTP test +To run a browser-level and protocol-level test concurrently in k6, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). + The code below shows an example of combining a browser and HTTP test in a single script. While the script exposes the backend to the typical load, it also checks the frontend for any unexpected issues. It also defines thresholds to check both HTTP and browser metrics against pre-defined SLOs. {{< code >}} @@ -71,7 +85,7 @@ export function getPizza() { const res = http.post(`${BASE_URL}/api/pizza`, JSON.stringify(restrictions), { headers: { 'Content-Type': 'application/json', - 'X-User-ID': randomIntBetween(1, 30000), + 'Authorization': 'token abcdef0123456789', }, }); @@ -87,7 +101,8 @@ export async function checkFrontend() { await page.goto(BASE_URL); await check(page.locator('h1'), { - 'header': async lo => await lo.textContent() == 'Looking to break out of your pizza routine?' + header: async (lo) => + (await lo.textContent()) == 'Looking to break out of your pizza routine?', }); await Promise.all([ @@ -97,7 +112,7 @@ export async function checkFrontend() { await page.screenshot({ path: `screenshots/${__ITER}.png` }); await check(page.locator('div#recommendations'), { - 'recommendation': async lo => await lo.textContent() != '', + recommendation: async (lo) => (await lo.textContent()) != '', }); } finally { await page.close(); @@ -127,7 +142,7 @@ The following code shows an example of how you could use the xk6-disruptor exten To find out more information about injecting faults to your service, check out the [Get started with xk6-disruptor guide](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/first-steps/). -{{< code >}} + ```javascript import { browser } from 'k6/browser'; @@ -186,7 +201,8 @@ export async function checkFrontend() { try { await page.goto(BASE_URL); await check(page.locator('h1'), { - 'header': async lo => await lo.textContent() == 'Looking to break out of your pizza routine?' + header: async (lo) => + (await lo.textContent()) == 'Looking to break out of your pizza routine?', }); await Promise.all([ @@ -196,7 +212,7 @@ export async function checkFrontend() { await page.screenshot({ path: `screenshots/${__ITER}.png` }); await check(page.locator('div#recommendations'), { - recommendation: async lo => await lo.textContent() != '', + recommendation: async (lo) => (await lo.textContent()) != '', }); } finally { await page.close(); @@ -204,8 +220,6 @@ export async function checkFrontend() { } ``` -{{< /code >}} - ## Recommended practices - **Start small**. Start with a small number of browser-based virtual users. A good starting point is to have 10% virtual users or less to monitor the user experience for your end-users, while the script emulates around 90% of traffic from the protocol level. diff --git a/docs/sources/k6/next/using-k6-browser/running-browser-tests.md b/docs/sources/k6/next/using-k6-browser/running-browser-tests.md index 8cb6544aa6..cb2bece525 100644 --- a/docs/sources/k6/next/using-k6-browser/running-browser-tests.md +++ b/docs/sources/k6/next/using-k6-browser/running-browser-tests.md @@ -1,7 +1,7 @@ --- title: 'Running browser tests' description: 'Follow along to learn how to run a browser test, interact with elements on the page, wait for page navigation, write assertions and run both browser-level and protocol-level tests in a single script.' -weight: 02 +weight: 200 --- # Running browser tests @@ -160,194 +160,3 @@ To run a simple local script: ```bash docker run --rm -i --platform linux/amd64 -v $(pwd):/home/k6/screenshots -e K6_BROWSER_HEADLESS=false grafana/k6:master-with-browser run - /javascript-api/k6-browser/locator) object, which you can later use to interact with the element. - -To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). - -{{< admonition type="note" >}} - -You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-browser/locator). - -{{< /admonition >}} - -{{< code >}} - -```javascript -import { browser } from 'k6/browser'; - -export const options = { - scenarios: { - ui: { - executor: 'shared-iterations', - options: { - browser: { - type: 'chromium', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - // Enter login credentials - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - await page.screenshot({ path: 'screenshots/screenshot.png' }); - } finally { - await page.close(); - } -} -``` - -{{< /code >}} - -The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. - -Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. - -## Asynchronous operations - -Since many browser operations happen asynchronously, and to follow the Playwright API more closely, we are working on migrating most of the browser module methods to be asynchronous as well. - -At the moment, methods such as `page.goto()`, `page.waitForNavigation()` and `Element.click()` return [JavaScript promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), and scripts must be written to handle this properly. - -To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. - -{{< code >}} - -```javascript -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', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - const submitButton = page.locator('input[type="submit"]'); - - await Promise.all([page.waitForNavigation(), submitButton.click()]); - - await check(page.locator('h2'), { - header: async (lo) => (await lo.textContent()) == 'Welcome, admin!', - }); - } finally { - await page.close(); - } -} -``` - -{{< /code >}} - -The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. - -Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. - -## Run both browser-level and protocol-level tests in a single script - -The real power of the browser module shines when it’s combined with the existing features of k6. A common scenario that you can try is to mix a smaller subset of browser-level tests with a larger protocol-level test which can simulate how your website responds to various performance events. This approach is what we refer to as [hybrid load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) and provides advantages such as: - -- testing real user flows on the frontend while generating a higher load in the backend -- measuring backend and frontend performance in the same test execution -- increased collaboration between backend and frontend teams since the same tool can be used - -To run a browser-level and protocol-level test concurrently, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). - -{{< admonition type="note" >}} - -Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. - -{{< /admonition >}} - -{{< code >}} - -```javascript -import { browser } from 'k6/browser'; -import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; -import http from 'k6/http'; - -export const options = { - scenarios: { - browser: { - executor: 'constant-vus', - exec: 'browserTest', - vus: 1, - duration: '10s', - options: { - browser: { - type: 'chromium', - }, - }, - }, - news: { - executor: 'constant-vus', - exec: 'news', - vus: 20, - duration: '1m', - }, - }, -}; - -export async function browserTest() { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/browser.php'); - - await page.locator('#checkbox1').check(); - - await check(page.locator('#checkbox-info-display'), { - 'checkbox is checked': async (lo) => - (await lo.textContent()) === 'Thanks for checking the box', - }); - } finally { - await page.close(); - } -} - -export function news() { - const res = http.get('https://test.k6.io/news.php'); - - check(res, { - 'status is 200': (r) => r.status === 200, - }); -} -``` - -{{< /code >}} - -The preceding code contains two scenarios. One for the browser-level test called `browser` and one for the protocol-level test called `news`. Both scenarios are using the [constant-vus executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) which introduces a constant number of virtual users to execute as many iterations as possible for a specified amount of time. - -Since it's all in one script, this allows for greater collaboration amongst teams. diff --git a/docs/sources/k6/next/using-k6-browser/write-your-first-browser-test.md b/docs/sources/k6/next/using-k6-browser/write-your-first-browser-test.md new file mode 100644 index 0000000000..e4c36167e4 --- /dev/null +++ b/docs/sources/k6/next/using-k6-browser/write-your-first-browser-test.md @@ -0,0 +1,223 @@ +--- +title: 'Write your first browser test' +description: 'Learn how to write your first k6 browser test script.' +weight: 100 +--- + +# Write your first browser test + +k6 browser tests allow you to simulate real user interactions with web applications, such as clicking buttons, filling out forms, and verifying page content. This helps you test not only backend performance, but also frontend reliability and user experience. + +In this guide, you'll learn how to write your first k6 browser test script using the browser module. + +## Before you begin + +To write and run k6 browser tests, you'll need: + +- A basic knowledge of JavaScript or TypeScript. +- A code editor to write your scripts, such as [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains editors](https://www.jetbrains.com/). +- A Chromium-based browser (such as Google Chrome) installed locally. +- [Install k6](https://grafana.com/docs/k6//set-up/install-k6/) on your machine. + +## Basic structure of a k6 browser test + +For k6 to be able to interpret and execute your test, every k6 script follows a common structure, revolving around a few core components: + +1. **Default function**: This is where the test logic resides. It defines what your test will do and how it will behave during execution. It should be exported as the default function in your script. For browser scripts, it's written as an `async` function to handle browser actions. +1. **Imports**: You can import additional [k6 modules](https://grafana.com/docs/k6//javascript-api/) or [JavaScript libraries (jslibs)](https://grafana.com/docs/k6//javascript-api/jslib/) to extend your script’s functionality, such as making HTTP requests. Note that k6 is not built upon Node.js, and instead uses its own JavaScript runtime. Compatibility with some npm modules may vary. +1. **Options**: Enable you to configure the execution of the test, such as defining the number of virtual users, the test duration, or setting performance thresholds. Refer to [Options](https://grafana.com/docs/k6//using-k6/k6-options/) for more details. +1. **Lifecycle operations (optional)**: Because your test might need to run code before and/or after the execution of the test logic, such as parsing data from a file, or download an object from Amazon S3, [lifecycle operations](https://grafana.com/docs/k6//using-k6/test-lifecycle/) allow you to write code, either as predefined functions or within specific code scopes, that will be executed at different stages of the test execution. + +## Key concepts + +To write k6 browser tests, its important to understand a few key concepts: + +1. **Browser type**: A browser script must have the `options.browser.type` field set in the `options` object. For example: + + ```js + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }; + ``` + + If that option isn't set, k6 will throw an error when trying to execute your script. + +1. **Asynchronous operations**: Browser interactions (like navigation and clicks) are asynchronous, so your test must use `async`/`await`. Refer to [Asynchronous operations](https://grafana.com/docs/k6//using-k6-browser/how-to-write-browser-tests/asynchronous-operations/) for more details. +1. **Locators**: The Locator API can be used to find and interact with elements on a page, such as buttons or headers. There are other ways to find elements, such as using the `Page.$()` method, but the Locator API provides several benefits, including finding an element even if the underlying frame navigates, and working with dynamic web pages and SPAs such as React. + +## Write your first browser test script + +Let's walk through creating a browser test that loads a page, clicks a button, takes a screenshot, and checks for recommendations. + +1. **Create a test file**: A test file can be named anything you like, and live wherever you see fit in your project, but it should have a `.js` or `.ts` extension. In this example, create a JavaScript file named `my-first-browser-test.js`. Open your terminal and run the following command: + + ```bash + touch my-first-browser-test.js + ``` + +1. **Import k6 modules**: Import the necessary modules for HTTP requests, browser automation, and utility functions. + + + + ```js + import http from 'k6/http'; + import exec from 'k6/execution'; + import { browser } from 'k6/browser'; + import { sleep, check, fail } from 'k6'; + ``` + +1. **Define options**: Configure the `options` object to use the browser executor and specify the browser type. + + + + ```js + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }; + ``` + +1. **Setup function**: Create a `setup` function to check if the target site is available, before running the main test logic. + + + + ```typescript + export function setup() { + let res = http.get(BASE_URL); + if (res.status !== 200) { + exec.test.abort(`Got unexpected status code ${res.status} when trying to setup. Exiting.`); + } + } + ``` + +1. **Default function**: The default exported function is the entry point for the test script. It will be executed repeatedly the number of times you define with the `iterations` option. In this function, using `async`/`await`, add the functions that open a browser page, interacts with the elements on the page, takes a screenshot, and performs a `check`: + + + + ```typescript + export default async function () { + let checkData; + const page = await browser.newPage(); + + try { + await page.goto(BASE_URL); + + checkData = await page.locator('h1').textContent(); + check(page, { + header: checkData === 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + + await page.screenshot({ path: 'screenshot.png' }); + + checkData = await page.locator('div#recommendations').textContent(); + check(page, { + recommendation: checkData !== '', + }); + } catch (error) { + fail(`Browser iteration failed: ${error.message}`); + } finally { + await page.close(); + } + + sleep(1); + } + ``` + +Your final script should look like this: + +```js +import http from 'k6/http'; +import exec from 'k6/execution'; +import { browser } from 'k6/browser'; +import { sleep, check, fail } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'https://quickpizza.grafana.com'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export function setup() { + const res = http.get(BASE_URL); + if (res.status !== 200) { + exec.test.abort(`Got unexpected status code ${res.status} when trying to setup. Exiting.`); + } +} + +export default async function () { + let checkData; + const page = await browser.newPage(); + + try { + await page.goto(BASE_URL); + + checkData = await page.locator('h1').textContent(); + check(page, { + header: checkData === 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + + await page.screenshot({ path: 'screenshot.png' }); + + checkData = await page.locator('div#recommendations').textContent(); + check(page, { + recommendation: checkData !== '', + }); + } catch (error) { + fail(`Browser iteration failed: ${error.message}`); + } finally { + await page.close(); + } + + sleep(1); +} +``` + +## Extending your browser test + +Once you're comfortable with the basics, you can extend your browser test by: + +- Simulating more complex user flows (multiple pages, form submissions, etc.). +- Adding more checks and assertions to validate UI elements and content. +- Creating hybrid performance tests to test both the frontend and backend simultaneously. + +Refer to the [Using k6 browser](https://grafana.com/docs/k6//using-k6-browser/) section for more details about the browser module and its features. + +## Next steps + +Now that you've written your first k6 browser test script, it's time to run it. Refer to [Running k6 browser tests](https://grafana.com/docs/k6//using-k6-browser/running-browser-tests/) for instructions on executing your script and analyzing results. diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/_index.md b/docs/sources/k6/v1.0.x/using-k6-browser/_index.md index 20584a8812..d394e0958f 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/_index.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/_index.md @@ -1,6 +1,6 @@ --- aliases: - - ./examples/crawl-webpage # docs/k6//examples/crawl-webpage + - ./using-k6-browser # docs/k6//using-k6-browser title: Using k6 browser description: 'The browser module brings browser automation and end-to-end testing to k6 while supporting core k6 features. Interact with real browsers and collect frontend metrics as part of your k6 tests.' weight: 300 @@ -31,58 +31,26 @@ The main use case for the browser module is to test performance on the browser l - Are all my elements interactive on the frontend? - Are there any loading spinners that take a long time to disappear? -## A simple browser test - -{{< code >}} - -```javascript -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', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const context = await browser.newContext(); - const page = await context.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - await Promise.all([page.waitForNavigation(), page.locator('input[type="submit"]').click()]); - - await check(page.locator('h2'), { - header: async (h2) => (await h2.textContent()) == 'Welcome, admin!', - }); - } finally { - await page.close(); - } -} -``` +## Create a browser test + +To create a browser test, you first need to: -{{< /code >}} +- [Install k6](https://grafana.com/docs/k6//set-up/install-k6/) in your machine. +- Install a Chromium-based browser, such as Google Chrome, in your machine. -The preceding code launches a Chromium-based browser, visits the application and mimics a user logging in to the application. Once submitted, it checks if the text of the header matches what is expected. +After, run the `k6 new` command with the `--template` option set to `browser`: -After running the test, the following [browser metrics](https://grafana.com/docs/k6//using-k6-browser/metrics) will be reported. +```bash +k6 new --template browser browser-script.js +``` -{{< code >}} +The command creates a test script you can run right away with the `k6 run` command: + +```bash +k6 run browser-script.js +``` + +After running the test, you can see the [end of test results](https://grafana.com/docs/k6//results-output/end-of-test/). It contains metrics that show the performance of the website on the script. ```bash /\ Grafana /‾‾/ @@ -91,38 +59,69 @@ After running the test, the following [browser metrics](https://grafana.com/docs / \ | ( | (‾) | / __________ \ |_|\_\ \_____/ - execution: local - script: test.js - output: - - - scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): - * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) - - -running (00m01.3s), 0/1 VUs, 1 complete and 0 interrupted iterations -ui ✓ [======================================] 1 VUs 00m01.3s/10m0s 1/1 shared iters - - ✓ header - - browser_data_received.......: 2.6 kB 2.0 kB/s - browser_data_sent...........: 1.9 kB 1.5 kB/s - browser_http_req_duration...: avg=215.4ms min=124.9ms med=126.65ms max=394.64ms p(90)=341.04ms p(95)=367.84ms - browser_http_req_failed.....: 0.00% ✓ 0 ✗ 3 - browser_web_vital_cls.......: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 - browser_web_vital_fcp.......: avg=344.15ms min=269.2ms med=344.15ms max=419.1ms p(90)=404.11ms p(95)=411.6ms - browser_web_vital_fid.......: avg=200µs min=200µs med=200µs max=200µs p(90)=200µs p(95)=200µs - browser_web_vital_inp.......: avg=8ms min=8ms med=8ms max=8ms p(90)=8ms p(95)=8ms - browser_web_vital_lcp.......: avg=419.1ms min=419.1ms med=419.1ms max=419.1ms p(90)=419.1ms p(95)=419.1ms - browser_web_vital_ttfb......: avg=322.4ms min=251ms med=322.4ms max=393.8ms p(90)=379.52ms p(95)=386.66ms - ✓ checks......................: 100.00% ✓ 1 ✗ 0 - data_received...............: 0 B 0 B/s - data_sent...................: 0 B 0 B/s - iteration_duration..........: avg=1.28s min=1.28s med=1.28s max=1.28s p(90)=1.28s p(95)=1.28s - iterations..................: 1 0.777541/s - vus.........................: 1 min=1 max=1 - vus_max.....................: 1 min=1 max=1 + execution: local + script: script.js + output: - + + scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): + * ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) + + + + █ TOTAL RESULTS + + checks_total.......................: 2 0.300669/s + checks_succeeded...................: 100.00% 2 out of 2 + checks_failed......................: 0.00% 0 out of 2 + + ✓ header + ✓ recommendation + + HTTP + http_req_duration.......................................................: avg=122ms min=122ms med=122ms max=122ms p(90)=122ms p(95)=122ms + { expected_response:true }............................................: avg=122ms min=122ms med=122ms max=122ms p(90)=122ms p(95)=122ms + http_req_failed.........................................................: 0.00% 0 out of 1 + http_reqs...............................................................: 1 0.150334/s + + EXECUTION + iteration_duration......................................................: avg=4.39s min=4.39s med=4.39s max=4.39s p(90)=4.39s p(95)=4.39s + iterations..............................................................: 1 0.150334/s + vus.....................................................................: 1 min=0 max=1 + vus_max.................................................................: 1 min=1 max=1 + + NETWORK + data_received...........................................................: 6.9 kB 1.0 kB/s + data_sent...............................................................: 543 B 82 B/s + + BROWSER + browser_data_received...................................................: 357 kB 54 kB/s + browser_data_sent.......................................................: 4.9 kB 738 B/s + browser_http_req_duration...............................................: avg=355.28ms min=124.04ms med=314.4ms max=1.45s p(90)=542.75ms p(95)=753.09ms + browser_http_req_failed.................................................: 0.00% 0 out of 18 + + WEB_VITALS + browser_web_vital_cls...................................................: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 + browser_web_vital_fcp...................................................: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s + browser_web_vital_fid...................................................: avg=300µs min=300µs med=300µs max=300µs p(90)=300µs p(95)=300µs + browser_web_vital_inp...................................................: avg=56ms min=56ms med=56ms max=56ms p(90)=56ms p(95)=56ms + browser_web_vital_lcp...................................................: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s + browser_web_vital_ttfb..................................................: avg=1.45s min=1.45s med=1.45s max=1.45s p(90)=1.45s p(95)=1.45s ``` -{{< /code >}} +You can also see at the end of the output the browser and Web Vital metrics that report performance specific to browser testing. -This gives you a representation of browser performance, via the web vitals, as well as the HTTP requests that came from the browser. +```bash +BROWSER +browser_data_received.........: 357 kB 54 kB/s +browser_data_sent.............: 4.9 kB 738 B/s +browser_http_req_duration.....: avg=355.28ms min=124.04ms med=314.4ms max=1.45s p(90)=542.75ms p(95)=753.09ms +browser_http_req_failed.......: 0.00% 0 out of 18 + +WEB_VITALS +browser_web_vital_cls.........: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 +browser_web_vital_fcp.........: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s +browser_web_vital_fid.........: avg=300µs min=300µs med=300µs max=300µs p(90)=300µs p(95)=300µs +browser_web_vital_inp.........: avg=56ms min=56ms med=56ms max=56ms p(90)=56ms p(95)=56ms +browser_web_vital_lcp.........: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s +browser_web_vital_ttfb........: avg=1.45s min=1.45s med=1.45s max=1.45s p(90)=1.45s p(95)=1.45s +``` diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/_index.md b/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/_index.md new file mode 100644 index 0000000000..7fb3c5c1a5 --- /dev/null +++ b/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/_index.md @@ -0,0 +1,9 @@ +--- +title: How to write browser tests +description: 'Learn how to write k6 browser tests.' +weight: 300 +--- + +# How to write browser tests + +{{< section >}} diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md b/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md new file mode 100644 index 0000000000..92aa82c3a0 --- /dev/null +++ b/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md @@ -0,0 +1,150 @@ +--- +title: Asynchronous operations +description: 'Learn how the k6 browser module uses asynchronous operations.' +weight: 100 +--- + +# Asynchronous operations + +Most methods in the browser module return [JavaScript promises](#why-the-browser-module-uses-asynchronous-apis), and k6 scripts must be written to handle this properly. This usually means using the `await` keyword to wait for the async operation to complete. + +For example: + + + +```js +const page = await browser.newPage(); + +await page.goto('https://quickpizza.grafana.com/'); + +const locator = page.locator('button[name="pizza-please"]'); + +await locator.click(); +``` + +In addition to using `await`, another important part of writing k6 browser tests is handling page navigations. There are two recommended methods for doing that: using `Promise.all` or using the `waitFor` method. + +## Promise.all + +To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. + +{{< code >}} + +```javascript +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', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = await browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + await page.locator('input[name="login"]').type('admin'); + await page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + + await check(page.locator('h2'), { + header: async (lo) => (await lo.textContent()) == 'Welcome, admin!', + }); + } finally { + await page.close(); + } +} +``` + +{{< /code >}} + +The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. + +Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. + +## Wait for specific elements + +We also encourage the use of `locator.waitFor` where possible. When you navigate to a website, there are usually one or more elements that are important for your test. Once those elements load, you can safely proceed to the next step. For example, in a search scenario: + +1. Navigate to the search site +1. Wait for the search bar and submit button to appear +1. Fill in the search bar with the query +1. Click the submit button +1. Wait for the search results + +We should be able to do these actions like so: + + + +```js +await page.goto('https://my-search-engine.com'); + +const searchBar = page.locator('.search-bar'); +const submitButton = page.locator('.submit-button'); + +await searchBar.waitFor(); +await submitButton.waitFor(); + +await searchBar.fill('k6'); +await submitButton.click(); + +const searchResults = page.locator('.search-results-table'); + +await searchResults.waitFor(); +``` + +This avoids the use of `Promise.all` which can be confusing to work with, and instead makes the script easier to follow. + +## Why the browser module uses asynchronous APIs + +The browser module uses asynchronous APIs that require `await` for several reasons: + +1. JavaScript is single-threaded with a single event loop. Asynchronous APIs prevent blocking the thread and event loop with long-running or I/O-based tasks. +1. Consistency with [Playwright](https://playwright.dev/), a popular browser automation library. +1. Alignment with how developers expect to work with modern JavaScript APIs. + +For example: + + + +```js +const page = await browser.newPage(); + +await page.goto('https://quickpizza.grafana.com/'); + +const locator = page.locator('button[name="pizza-please"]'); + +await locator.click(); +``` + +API calls that interact with Chromium are asynchronous and require `await` to ensure completion before proceeding. Synchronous APIs, such as `page.locator`, do not require `await`, but using it does not cause issues since the JavaScript runtime will simply return the value immediately. + +If you don't add `await` on asynchronous APIs, it can cause the script to finish before the test completes, resulting in errors like `"Uncaught (in promise) TypeError: Object has no member 'goto'"`. That can happen because the page object from `browser.newPage()` without an `await` is actually a JavaScript promise. You can try and see the error using the following code snippet: + + + +```js +const page = browser.newPage(); + +page.goto('https://quickpizza.grafana.com/'); // An error should occur since we're not using await in the line above. + +const locator = page.locator('button[name="pizza-please"]'); + +locator.click(); +``` diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md b/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md new file mode 100644 index 0000000000..f671d96b06 --- /dev/null +++ b/docs/sources/k6/v1.0.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md @@ -0,0 +1,61 @@ +--- +title: Interact with elements on your webpage +description: 'Learn how to interact with elements on a webpage using the k6 browser module.' +weight: 200 +--- + +# Interact with elements on your webpage + +You can use `page.locator()` and pass in the element's selector you want to find on the page. `page.locator()` will create and return a [Locator](https://grafana.com/docs/k6//javascript-api/k6-browser/locator) object, which you can later use to interact with the element. + +To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). + +{{< admonition type="note" >}} + +You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-browser/locator). + +{{< /admonition >}} + +{{< code >}} + +```javascript +import { browser } from 'k6/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = await browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + // Enter login credentials + await page.locator('input[name="login"]').type('admin'); + await page.locator('input[name="password"]').type('123'); + + await page.screenshot({ path: 'screenshots/screenshot.png' }); + } finally { + await page.close(); + } +} +``` + +{{< /code >}} + +The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. + +Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/metrics.md b/docs/sources/k6/v1.0.x/using-k6-browser/metrics.md index d1b36d76bf..c78cb6a4ef 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/metrics.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/metrics.md @@ -1,7 +1,7 @@ --- title: 'Browser metrics' description: 'An overview of the different browser performance metrics that the browser module tracks.' -weight: 03 +weight: 400 --- # Browser metrics @@ -86,7 +86,7 @@ Currently, you can only use URLs to specify thresholds for different pages. If y {{< /admonition >}} -{{< code >}} + ```javascript export const options = { @@ -98,8 +98,6 @@ export const options = { }; ``` -{{< /code >}} - When the test is run, you should see a similar output as the one below. ```bash diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/migrating-to-k6-v0-52.md b/docs/sources/k6/v1.0.x/using-k6-browser/migrating-to-k6-v0-52.md index 3210f79587..44c18eea87 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/migrating-to-k6-v0-52.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/migrating-to-k6-v0-52.md @@ -3,7 +3,7 @@ aliases: - ./migrating-to-k6-v0-46/ # docs/k6//using-k6-browser/migrating-to-k6-v0-46 title: 'Migrating browser scripts to k6 v0.52' description: 'A migration guide to ease the process of transitioning to the new k6 browser module version bundled with k6 v0.52' -weight: 05 +weight: 600 --- # Migrating browser scripts to k6 v0.52 @@ -35,8 +35,7 @@ To ensure your scripts work with the latest release of the k6 browser module, yo For example, before: -{{< code >}} - + ```javascript @@ -50,12 +49,9 @@ export default async function () { } ``` -{{< /code >}} - And now: -{{< code >}} - + ```javascript @@ -69,8 +65,6 @@ export default async function () { } ``` -{{< /code >}} - You might have already encountered async APIs when working with the browser module, such as [page.click](http://grafana.com/docs/k6//javascript-api/k6-browser/page/click/), so the use of `async` and `await` keywords might be familiar to you. Below is a screenshot of a comparison between a generic browser test in `v0.51` and `v0.52` to help visualize the change: @@ -279,22 +273,18 @@ The k6 `check` API will not `await` promises, so calling a function that returns For example, before: -{{< code >}} - + ```javascript check(page.locator('h2'), { - header: lo => lo.textContent() == 'Welcome, admin!', + header: (lo) => lo.textContent() == 'Welcome, admin!', }); ``` -{{< /code >}} - And now: -{{< code >}} - + ```javascript @@ -303,12 +293,10 @@ import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; // ... await check(page.locator('h2'), { - 'header': async lo => await lo.textContent() === 'Welcome, admin!' + header: async (lo) => (await lo.textContent()) === 'Welcome, admin!', }); ``` -{{< /code >}} - ## Groups A note on [groups](https://grafana.com/docs/k6//using-k6/tags-and-groups/#groups), they don't work with async APIs either, there is no workaround as of yet. Here's the [GitHub issue](https://github.com/grafana/k6/issues/2728) that you can follow to keep up-to-date with relevant news on a group API that works with async APIs. diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/options.md b/docs/sources/k6/v1.0.x/using-k6-browser/options.md index 7aff806738..ff8d50b922 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/options.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/options.md @@ -1,13 +1,15 @@ --- title: 'Browser options' description: 'An overview of the different options you can use to customize the browser module behavior when running browser tests.' -weight: 04 +weight: 500 --- # Browser options To enable browser testing, add the `browser` configuration within the `options` property of the [Scenario options](https://grafana.com/docs/k6//using-k6/scenarios/#options). + + ```javascript export const options = { scenarios: { diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/_index.md b/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/_index.md index 2f59344031..1591515738 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/_index.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/_index.md @@ -1,11 +1,11 @@ --- title: 'Recommended practices' description: 'A list of different examples and recommended practices when working with the k6 browser module' -weight: 100 +weight: 350 --- # Recommended practices This section presents some examples and recommended practices when working with the `k6 browser` module to leverage browser automation as part of your k6 tests. -{{< section menuTitle="true">}} \ No newline at end of file +{{< section menuTitle="true">}} diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md b/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md index f046d7b260..c629c598b4 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md @@ -10,10 +10,24 @@ weight: 01 An alternative approach to [browser-based load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites/#browser-based-load-testing) that's much less resource-intensive is combining a small number of virtual users for a browser test with a large number of virtual users for a protocol-level test. -You can achieve hybrid performance in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. +You can achieve [hybrid performance](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. + +Some of the advantages of running a hybrid performance test are: + +- Testing real user flows on the frontend while generating a higher load in the backend. +- Measuring backend and frontend performance in the same test execution +- Increased collaboration between backend and frontend teams since the same tool can be used. + +{{< admonition type="note" >}} + +Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. + +{{< /admonition >}} ## Browser and HTTP test +To run a browser-level and protocol-level test concurrently in k6, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). + The code below shows an example of combining a browser and HTTP test in a single script. While the script exposes the backend to the typical load, it also checks the frontend for any unexpected issues. It also defines thresholds to check both HTTP and browser metrics against pre-defined SLOs. {{< code >}} @@ -71,7 +85,7 @@ export function getPizza() { const res = http.post(`${BASE_URL}/api/pizza`, JSON.stringify(restrictions), { headers: { 'Content-Type': 'application/json', - 'X-User-ID': randomIntBetween(1, 30000), + 'Authorization': 'token abcdef0123456789', }, }); @@ -87,7 +101,8 @@ export async function checkFrontend() { await page.goto(BASE_URL); await check(page.locator('h1'), { - 'header': async lo => await lo.textContent() == 'Looking to break out of your pizza routine?' + header: async (lo) => + (await lo.textContent()) == 'Looking to break out of your pizza routine?', }); await Promise.all([ @@ -97,7 +112,7 @@ export async function checkFrontend() { await page.screenshot({ path: `screenshots/${__ITER}.png` }); await check(page.locator('div#recommendations'), { - 'recommendation': async lo => await lo.textContent() != '', + recommendation: async (lo) => (await lo.textContent()) != '', }); } finally { await page.close(); @@ -127,7 +142,7 @@ The following code shows an example of how you could use the xk6-disruptor exten To find out more information about injecting faults to your service, check out the [Get started with xk6-disruptor guide](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/first-steps/). -{{< code >}} + ```javascript import { browser } from 'k6/browser'; @@ -186,7 +201,8 @@ export async function checkFrontend() { try { await page.goto(BASE_URL); await check(page.locator('h1'), { - 'header': async lo => await lo.textContent() == 'Looking to break out of your pizza routine?' + header: async (lo) => + (await lo.textContent()) == 'Looking to break out of your pizza routine?', }); await Promise.all([ @@ -196,7 +212,7 @@ export async function checkFrontend() { await page.screenshot({ path: `screenshots/${__ITER}.png` }); await check(page.locator('div#recommendations'), { - recommendation: async lo => await lo.textContent() != '', + recommendation: async (lo) => (await lo.textContent()) != '', }); } finally { await page.close(); @@ -204,8 +220,6 @@ export async function checkFrontend() { } ``` -{{< /code >}} - ## Recommended practices - **Start small**. Start with a small number of browser-based virtual users. A good starting point is to have 10% virtual users or less to monitor the user experience for your end-users, while the script emulates around 90% of traffic from the protocol level. diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/running-browser-tests.md b/docs/sources/k6/v1.0.x/using-k6-browser/running-browser-tests.md index 8cb6544aa6..cb2bece525 100644 --- a/docs/sources/k6/v1.0.x/using-k6-browser/running-browser-tests.md +++ b/docs/sources/k6/v1.0.x/using-k6-browser/running-browser-tests.md @@ -1,7 +1,7 @@ --- title: 'Running browser tests' description: 'Follow along to learn how to run a browser test, interact with elements on the page, wait for page navigation, write assertions and run both browser-level and protocol-level tests in a single script.' -weight: 02 +weight: 200 --- # Running browser tests @@ -160,194 +160,3 @@ To run a simple local script: ```bash docker run --rm -i --platform linux/amd64 -v $(pwd):/home/k6/screenshots -e K6_BROWSER_HEADLESS=false grafana/k6:master-with-browser run - /javascript-api/k6-browser/locator) object, which you can later use to interact with the element. - -To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). - -{{< admonition type="note" >}} - -You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-browser/locator). - -{{< /admonition >}} - -{{< code >}} - -```javascript -import { browser } from 'k6/browser'; - -export const options = { - scenarios: { - ui: { - executor: 'shared-iterations', - options: { - browser: { - type: 'chromium', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - // Enter login credentials - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - await page.screenshot({ path: 'screenshots/screenshot.png' }); - } finally { - await page.close(); - } -} -``` - -{{< /code >}} - -The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. - -Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. - -## Asynchronous operations - -Since many browser operations happen asynchronously, and to follow the Playwright API more closely, we are working on migrating most of the browser module methods to be asynchronous as well. - -At the moment, methods such as `page.goto()`, `page.waitForNavigation()` and `Element.click()` return [JavaScript promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), and scripts must be written to handle this properly. - -To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. - -{{< code >}} - -```javascript -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', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - const submitButton = page.locator('input[type="submit"]'); - - await Promise.all([page.waitForNavigation(), submitButton.click()]); - - await check(page.locator('h2'), { - header: async (lo) => (await lo.textContent()) == 'Welcome, admin!', - }); - } finally { - await page.close(); - } -} -``` - -{{< /code >}} - -The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. - -Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. - -## Run both browser-level and protocol-level tests in a single script - -The real power of the browser module shines when it’s combined with the existing features of k6. A common scenario that you can try is to mix a smaller subset of browser-level tests with a larger protocol-level test which can simulate how your website responds to various performance events. This approach is what we refer to as [hybrid load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) and provides advantages such as: - -- testing real user flows on the frontend while generating a higher load in the backend -- measuring backend and frontend performance in the same test execution -- increased collaboration between backend and frontend teams since the same tool can be used - -To run a browser-level and protocol-level test concurrently, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). - -{{< admonition type="note" >}} - -Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. - -{{< /admonition >}} - -{{< code >}} - -```javascript -import { browser } from 'k6/browser'; -import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; -import http from 'k6/http'; - -export const options = { - scenarios: { - browser: { - executor: 'constant-vus', - exec: 'browserTest', - vus: 1, - duration: '10s', - options: { - browser: { - type: 'chromium', - }, - }, - }, - news: { - executor: 'constant-vus', - exec: 'news', - vus: 20, - duration: '1m', - }, - }, -}; - -export async function browserTest() { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/browser.php'); - - await page.locator('#checkbox1').check(); - - await check(page.locator('#checkbox-info-display'), { - 'checkbox is checked': async (lo) => - (await lo.textContent()) === 'Thanks for checking the box', - }); - } finally { - await page.close(); - } -} - -export function news() { - const res = http.get('https://test.k6.io/news.php'); - - check(res, { - 'status is 200': (r) => r.status === 200, - }); -} -``` - -{{< /code >}} - -The preceding code contains two scenarios. One for the browser-level test called `browser` and one for the protocol-level test called `news`. Both scenarios are using the [constant-vus executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) which introduces a constant number of virtual users to execute as many iterations as possible for a specified amount of time. - -Since it's all in one script, this allows for greater collaboration amongst teams. diff --git a/docs/sources/k6/v1.0.x/using-k6-browser/write-your-first-browser-test.md b/docs/sources/k6/v1.0.x/using-k6-browser/write-your-first-browser-test.md new file mode 100644 index 0000000000..e4c36167e4 --- /dev/null +++ b/docs/sources/k6/v1.0.x/using-k6-browser/write-your-first-browser-test.md @@ -0,0 +1,223 @@ +--- +title: 'Write your first browser test' +description: 'Learn how to write your first k6 browser test script.' +weight: 100 +--- + +# Write your first browser test + +k6 browser tests allow you to simulate real user interactions with web applications, such as clicking buttons, filling out forms, and verifying page content. This helps you test not only backend performance, but also frontend reliability and user experience. + +In this guide, you'll learn how to write your first k6 browser test script using the browser module. + +## Before you begin + +To write and run k6 browser tests, you'll need: + +- A basic knowledge of JavaScript or TypeScript. +- A code editor to write your scripts, such as [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains editors](https://www.jetbrains.com/). +- A Chromium-based browser (such as Google Chrome) installed locally. +- [Install k6](https://grafana.com/docs/k6//set-up/install-k6/) on your machine. + +## Basic structure of a k6 browser test + +For k6 to be able to interpret and execute your test, every k6 script follows a common structure, revolving around a few core components: + +1. **Default function**: This is where the test logic resides. It defines what your test will do and how it will behave during execution. It should be exported as the default function in your script. For browser scripts, it's written as an `async` function to handle browser actions. +1. **Imports**: You can import additional [k6 modules](https://grafana.com/docs/k6//javascript-api/) or [JavaScript libraries (jslibs)](https://grafana.com/docs/k6//javascript-api/jslib/) to extend your script’s functionality, such as making HTTP requests. Note that k6 is not built upon Node.js, and instead uses its own JavaScript runtime. Compatibility with some npm modules may vary. +1. **Options**: Enable you to configure the execution of the test, such as defining the number of virtual users, the test duration, or setting performance thresholds. Refer to [Options](https://grafana.com/docs/k6//using-k6/k6-options/) for more details. +1. **Lifecycle operations (optional)**: Because your test might need to run code before and/or after the execution of the test logic, such as parsing data from a file, or download an object from Amazon S3, [lifecycle operations](https://grafana.com/docs/k6//using-k6/test-lifecycle/) allow you to write code, either as predefined functions or within specific code scopes, that will be executed at different stages of the test execution. + +## Key concepts + +To write k6 browser tests, its important to understand a few key concepts: + +1. **Browser type**: A browser script must have the `options.browser.type` field set in the `options` object. For example: + + ```js + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }; + ``` + + If that option isn't set, k6 will throw an error when trying to execute your script. + +1. **Asynchronous operations**: Browser interactions (like navigation and clicks) are asynchronous, so your test must use `async`/`await`. Refer to [Asynchronous operations](https://grafana.com/docs/k6//using-k6-browser/how-to-write-browser-tests/asynchronous-operations/) for more details. +1. **Locators**: The Locator API can be used to find and interact with elements on a page, such as buttons or headers. There are other ways to find elements, such as using the `Page.$()` method, but the Locator API provides several benefits, including finding an element even if the underlying frame navigates, and working with dynamic web pages and SPAs such as React. + +## Write your first browser test script + +Let's walk through creating a browser test that loads a page, clicks a button, takes a screenshot, and checks for recommendations. + +1. **Create a test file**: A test file can be named anything you like, and live wherever you see fit in your project, but it should have a `.js` or `.ts` extension. In this example, create a JavaScript file named `my-first-browser-test.js`. Open your terminal and run the following command: + + ```bash + touch my-first-browser-test.js + ``` + +1. **Import k6 modules**: Import the necessary modules for HTTP requests, browser automation, and utility functions. + + + + ```js + import http from 'k6/http'; + import exec from 'k6/execution'; + import { browser } from 'k6/browser'; + import { sleep, check, fail } from 'k6'; + ``` + +1. **Define options**: Configure the `options` object to use the browser executor and specify the browser type. + + + + ```js + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }; + ``` + +1. **Setup function**: Create a `setup` function to check if the target site is available, before running the main test logic. + + + + ```typescript + export function setup() { + let res = http.get(BASE_URL); + if (res.status !== 200) { + exec.test.abort(`Got unexpected status code ${res.status} when trying to setup. Exiting.`); + } + } + ``` + +1. **Default function**: The default exported function is the entry point for the test script. It will be executed repeatedly the number of times you define with the `iterations` option. In this function, using `async`/`await`, add the functions that open a browser page, interacts with the elements on the page, takes a screenshot, and performs a `check`: + + + + ```typescript + export default async function () { + let checkData; + const page = await browser.newPage(); + + try { + await page.goto(BASE_URL); + + checkData = await page.locator('h1').textContent(); + check(page, { + header: checkData === 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + + await page.screenshot({ path: 'screenshot.png' }); + + checkData = await page.locator('div#recommendations').textContent(); + check(page, { + recommendation: checkData !== '', + }); + } catch (error) { + fail(`Browser iteration failed: ${error.message}`); + } finally { + await page.close(); + } + + sleep(1); + } + ``` + +Your final script should look like this: + +```js +import http from 'k6/http'; +import exec from 'k6/execution'; +import { browser } from 'k6/browser'; +import { sleep, check, fail } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'https://quickpizza.grafana.com'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export function setup() { + const res = http.get(BASE_URL); + if (res.status !== 200) { + exec.test.abort(`Got unexpected status code ${res.status} when trying to setup. Exiting.`); + } +} + +export default async function () { + let checkData; + const page = await browser.newPage(); + + try { + await page.goto(BASE_URL); + + checkData = await page.locator('h1').textContent(); + check(page, { + header: checkData === 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + + await page.screenshot({ path: 'screenshot.png' }); + + checkData = await page.locator('div#recommendations').textContent(); + check(page, { + recommendation: checkData !== '', + }); + } catch (error) { + fail(`Browser iteration failed: ${error.message}`); + } finally { + await page.close(); + } + + sleep(1); +} +``` + +## Extending your browser test + +Once you're comfortable with the basics, you can extend your browser test by: + +- Simulating more complex user flows (multiple pages, form submissions, etc.). +- Adding more checks and assertions to validate UI elements and content. +- Creating hybrid performance tests to test both the frontend and backend simultaneously. + +Refer to the [Using k6 browser](https://grafana.com/docs/k6//using-k6-browser/) section for more details about the browser module and its features. + +## Next steps + +Now that you've written your first k6 browser test script, it's time to run it. Refer to [Running k6 browser tests](https://grafana.com/docs/k6//using-k6-browser/running-browser-tests/) for instructions on executing your script and analyzing results. diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/_index.md b/docs/sources/k6/v1.1.x/using-k6-browser/_index.md index 20584a8812..d394e0958f 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/_index.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/_index.md @@ -1,6 +1,6 @@ --- aliases: - - ./examples/crawl-webpage # docs/k6//examples/crawl-webpage + - ./using-k6-browser # docs/k6//using-k6-browser title: Using k6 browser description: 'The browser module brings browser automation and end-to-end testing to k6 while supporting core k6 features. Interact with real browsers and collect frontend metrics as part of your k6 tests.' weight: 300 @@ -31,58 +31,26 @@ The main use case for the browser module is to test performance on the browser l - Are all my elements interactive on the frontend? - Are there any loading spinners that take a long time to disappear? -## A simple browser test - -{{< code >}} - -```javascript -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', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const context = await browser.newContext(); - const page = await context.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - await Promise.all([page.waitForNavigation(), page.locator('input[type="submit"]').click()]); - - await check(page.locator('h2'), { - header: async (h2) => (await h2.textContent()) == 'Welcome, admin!', - }); - } finally { - await page.close(); - } -} -``` +## Create a browser test + +To create a browser test, you first need to: -{{< /code >}} +- [Install k6](https://grafana.com/docs/k6//set-up/install-k6/) in your machine. +- Install a Chromium-based browser, such as Google Chrome, in your machine. -The preceding code launches a Chromium-based browser, visits the application and mimics a user logging in to the application. Once submitted, it checks if the text of the header matches what is expected. +After, run the `k6 new` command with the `--template` option set to `browser`: -After running the test, the following [browser metrics](https://grafana.com/docs/k6//using-k6-browser/metrics) will be reported. +```bash +k6 new --template browser browser-script.js +``` -{{< code >}} +The command creates a test script you can run right away with the `k6 run` command: + +```bash +k6 run browser-script.js +``` + +After running the test, you can see the [end of test results](https://grafana.com/docs/k6//results-output/end-of-test/). It contains metrics that show the performance of the website on the script. ```bash /\ Grafana /‾‾/ @@ -91,38 +59,69 @@ After running the test, the following [browser metrics](https://grafana.com/docs / \ | ( | (‾) | / __________ \ |_|\_\ \_____/ - execution: local - script: test.js - output: - - - scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): - * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) - - -running (00m01.3s), 0/1 VUs, 1 complete and 0 interrupted iterations -ui ✓ [======================================] 1 VUs 00m01.3s/10m0s 1/1 shared iters - - ✓ header - - browser_data_received.......: 2.6 kB 2.0 kB/s - browser_data_sent...........: 1.9 kB 1.5 kB/s - browser_http_req_duration...: avg=215.4ms min=124.9ms med=126.65ms max=394.64ms p(90)=341.04ms p(95)=367.84ms - browser_http_req_failed.....: 0.00% ✓ 0 ✗ 3 - browser_web_vital_cls.......: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 - browser_web_vital_fcp.......: avg=344.15ms min=269.2ms med=344.15ms max=419.1ms p(90)=404.11ms p(95)=411.6ms - browser_web_vital_fid.......: avg=200µs min=200µs med=200µs max=200µs p(90)=200µs p(95)=200µs - browser_web_vital_inp.......: avg=8ms min=8ms med=8ms max=8ms p(90)=8ms p(95)=8ms - browser_web_vital_lcp.......: avg=419.1ms min=419.1ms med=419.1ms max=419.1ms p(90)=419.1ms p(95)=419.1ms - browser_web_vital_ttfb......: avg=322.4ms min=251ms med=322.4ms max=393.8ms p(90)=379.52ms p(95)=386.66ms - ✓ checks......................: 100.00% ✓ 1 ✗ 0 - data_received...............: 0 B 0 B/s - data_sent...................: 0 B 0 B/s - iteration_duration..........: avg=1.28s min=1.28s med=1.28s max=1.28s p(90)=1.28s p(95)=1.28s - iterations..................: 1 0.777541/s - vus.........................: 1 min=1 max=1 - vus_max.....................: 1 min=1 max=1 + execution: local + script: script.js + output: - + + scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): + * ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) + + + + █ TOTAL RESULTS + + checks_total.......................: 2 0.300669/s + checks_succeeded...................: 100.00% 2 out of 2 + checks_failed......................: 0.00% 0 out of 2 + + ✓ header + ✓ recommendation + + HTTP + http_req_duration.......................................................: avg=122ms min=122ms med=122ms max=122ms p(90)=122ms p(95)=122ms + { expected_response:true }............................................: avg=122ms min=122ms med=122ms max=122ms p(90)=122ms p(95)=122ms + http_req_failed.........................................................: 0.00% 0 out of 1 + http_reqs...............................................................: 1 0.150334/s + + EXECUTION + iteration_duration......................................................: avg=4.39s min=4.39s med=4.39s max=4.39s p(90)=4.39s p(95)=4.39s + iterations..............................................................: 1 0.150334/s + vus.....................................................................: 1 min=0 max=1 + vus_max.................................................................: 1 min=1 max=1 + + NETWORK + data_received...........................................................: 6.9 kB 1.0 kB/s + data_sent...............................................................: 543 B 82 B/s + + BROWSER + browser_data_received...................................................: 357 kB 54 kB/s + browser_data_sent.......................................................: 4.9 kB 738 B/s + browser_http_req_duration...............................................: avg=355.28ms min=124.04ms med=314.4ms max=1.45s p(90)=542.75ms p(95)=753.09ms + browser_http_req_failed.................................................: 0.00% 0 out of 18 + + WEB_VITALS + browser_web_vital_cls...................................................: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 + browser_web_vital_fcp...................................................: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s + browser_web_vital_fid...................................................: avg=300µs min=300µs med=300µs max=300µs p(90)=300µs p(95)=300µs + browser_web_vital_inp...................................................: avg=56ms min=56ms med=56ms max=56ms p(90)=56ms p(95)=56ms + browser_web_vital_lcp...................................................: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s + browser_web_vital_ttfb..................................................: avg=1.45s min=1.45s med=1.45s max=1.45s p(90)=1.45s p(95)=1.45s ``` -{{< /code >}} +You can also see at the end of the output the browser and Web Vital metrics that report performance specific to browser testing. -This gives you a representation of browser performance, via the web vitals, as well as the HTTP requests that came from the browser. +```bash +BROWSER +browser_data_received.........: 357 kB 54 kB/s +browser_data_sent.............: 4.9 kB 738 B/s +browser_http_req_duration.....: avg=355.28ms min=124.04ms med=314.4ms max=1.45s p(90)=542.75ms p(95)=753.09ms +browser_http_req_failed.......: 0.00% 0 out of 18 + +WEB_VITALS +browser_web_vital_cls.........: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 +browser_web_vital_fcp.........: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s +browser_web_vital_fid.........: avg=300µs min=300µs med=300µs max=300µs p(90)=300µs p(95)=300µs +browser_web_vital_inp.........: avg=56ms min=56ms med=56ms max=56ms p(90)=56ms p(95)=56ms +browser_web_vital_lcp.........: avg=2.33s min=2.33s med=2.33s max=2.33s p(90)=2.33s p(95)=2.33s +browser_web_vital_ttfb........: avg=1.45s min=1.45s med=1.45s max=1.45s p(90)=1.45s p(95)=1.45s +``` diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/_index.md b/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/_index.md new file mode 100644 index 0000000000..7fb3c5c1a5 --- /dev/null +++ b/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/_index.md @@ -0,0 +1,9 @@ +--- +title: How to write browser tests +description: 'Learn how to write k6 browser tests.' +weight: 300 +--- + +# How to write browser tests + +{{< section >}} diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md b/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md new file mode 100644 index 0000000000..92aa82c3a0 --- /dev/null +++ b/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations.md @@ -0,0 +1,150 @@ +--- +title: Asynchronous operations +description: 'Learn how the k6 browser module uses asynchronous operations.' +weight: 100 +--- + +# Asynchronous operations + +Most methods in the browser module return [JavaScript promises](#why-the-browser-module-uses-asynchronous-apis), and k6 scripts must be written to handle this properly. This usually means using the `await` keyword to wait for the async operation to complete. + +For example: + + + +```js +const page = await browser.newPage(); + +await page.goto('https://quickpizza.grafana.com/'); + +const locator = page.locator('button[name="pizza-please"]'); + +await locator.click(); +``` + +In addition to using `await`, another important part of writing k6 browser tests is handling page navigations. There are two recommended methods for doing that: using `Promise.all` or using the `waitFor` method. + +## Promise.all + +To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. + +{{< code >}} + +```javascript +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', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = await browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + await page.locator('input[name="login"]').type('admin'); + await page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + + await check(page.locator('h2'), { + header: async (lo) => (await lo.textContent()) == 'Welcome, admin!', + }); + } finally { + await page.close(); + } +} +``` + +{{< /code >}} + +The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. + +Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. + +## Wait for specific elements + +We also encourage the use of `locator.waitFor` where possible. When you navigate to a website, there are usually one or more elements that are important for your test. Once those elements load, you can safely proceed to the next step. For example, in a search scenario: + +1. Navigate to the search site +1. Wait for the search bar and submit button to appear +1. Fill in the search bar with the query +1. Click the submit button +1. Wait for the search results + +We should be able to do these actions like so: + + + +```js +await page.goto('https://my-search-engine.com'); + +const searchBar = page.locator('.search-bar'); +const submitButton = page.locator('.submit-button'); + +await searchBar.waitFor(); +await submitButton.waitFor(); + +await searchBar.fill('k6'); +await submitButton.click(); + +const searchResults = page.locator('.search-results-table'); + +await searchResults.waitFor(); +``` + +This avoids the use of `Promise.all` which can be confusing to work with, and instead makes the script easier to follow. + +## Why the browser module uses asynchronous APIs + +The browser module uses asynchronous APIs that require `await` for several reasons: + +1. JavaScript is single-threaded with a single event loop. Asynchronous APIs prevent blocking the thread and event loop with long-running or I/O-based tasks. +1. Consistency with [Playwright](https://playwright.dev/), a popular browser automation library. +1. Alignment with how developers expect to work with modern JavaScript APIs. + +For example: + + + +```js +const page = await browser.newPage(); + +await page.goto('https://quickpizza.grafana.com/'); + +const locator = page.locator('button[name="pizza-please"]'); + +await locator.click(); +``` + +API calls that interact with Chromium are asynchronous and require `await` to ensure completion before proceeding. Synchronous APIs, such as `page.locator`, do not require `await`, but using it does not cause issues since the JavaScript runtime will simply return the value immediately. + +If you don't add `await` on asynchronous APIs, it can cause the script to finish before the test completes, resulting in errors like `"Uncaught (in promise) TypeError: Object has no member 'goto'"`. That can happen because the page object from `browser.newPage()` without an `await` is actually a JavaScript promise. You can try and see the error using the following code snippet: + + + +```js +const page = browser.newPage(); + +page.goto('https://quickpizza.grafana.com/'); // An error should occur since we're not using await in the line above. + +const locator = page.locator('button[name="pizza-please"]'); + +locator.click(); +``` diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md b/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md new file mode 100644 index 0000000000..f671d96b06 --- /dev/null +++ b/docs/sources/k6/v1.1.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements.md @@ -0,0 +1,61 @@ +--- +title: Interact with elements on your webpage +description: 'Learn how to interact with elements on a webpage using the k6 browser module.' +weight: 200 +--- + +# Interact with elements on your webpage + +You can use `page.locator()` and pass in the element's selector you want to find on the page. `page.locator()` will create and return a [Locator](https://grafana.com/docs/k6//javascript-api/k6-browser/locator) object, which you can later use to interact with the element. + +To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). + +{{< admonition type="note" >}} + +You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-browser/locator). + +{{< /admonition >}} + +{{< code >}} + +```javascript +import { browser } from 'k6/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = await browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + // Enter login credentials + await page.locator('input[name="login"]').type('admin'); + await page.locator('input[name="password"]').type('123'); + + await page.screenshot({ path: 'screenshots/screenshot.png' }); + } finally { + await page.close(); + } +} +``` + +{{< /code >}} + +The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. + +Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/metrics.md b/docs/sources/k6/v1.1.x/using-k6-browser/metrics.md index d1b36d76bf..c78cb6a4ef 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/metrics.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/metrics.md @@ -1,7 +1,7 @@ --- title: 'Browser metrics' description: 'An overview of the different browser performance metrics that the browser module tracks.' -weight: 03 +weight: 400 --- # Browser metrics @@ -86,7 +86,7 @@ Currently, you can only use URLs to specify thresholds for different pages. If y {{< /admonition >}} -{{< code >}} + ```javascript export const options = { @@ -98,8 +98,6 @@ export const options = { }; ``` -{{< /code >}} - When the test is run, you should see a similar output as the one below. ```bash diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/migrating-to-k6-v0-52.md b/docs/sources/k6/v1.1.x/using-k6-browser/migrating-to-k6-v0-52.md index 3210f79587..44c18eea87 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/migrating-to-k6-v0-52.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/migrating-to-k6-v0-52.md @@ -3,7 +3,7 @@ aliases: - ./migrating-to-k6-v0-46/ # docs/k6//using-k6-browser/migrating-to-k6-v0-46 title: 'Migrating browser scripts to k6 v0.52' description: 'A migration guide to ease the process of transitioning to the new k6 browser module version bundled with k6 v0.52' -weight: 05 +weight: 600 --- # Migrating browser scripts to k6 v0.52 @@ -35,8 +35,7 @@ To ensure your scripts work with the latest release of the k6 browser module, yo For example, before: -{{< code >}} - + ```javascript @@ -50,12 +49,9 @@ export default async function () { } ``` -{{< /code >}} - And now: -{{< code >}} - + ```javascript @@ -69,8 +65,6 @@ export default async function () { } ``` -{{< /code >}} - You might have already encountered async APIs when working with the browser module, such as [page.click](http://grafana.com/docs/k6//javascript-api/k6-browser/page/click/), so the use of `async` and `await` keywords might be familiar to you. Below is a screenshot of a comparison between a generic browser test in `v0.51` and `v0.52` to help visualize the change: @@ -279,22 +273,18 @@ The k6 `check` API will not `await` promises, so calling a function that returns For example, before: -{{< code >}} - + ```javascript check(page.locator('h2'), { - header: lo => lo.textContent() == 'Welcome, admin!', + header: (lo) => lo.textContent() == 'Welcome, admin!', }); ``` -{{< /code >}} - And now: -{{< code >}} - + ```javascript @@ -303,12 +293,10 @@ import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; // ... await check(page.locator('h2'), { - 'header': async lo => await lo.textContent() === 'Welcome, admin!' + header: async (lo) => (await lo.textContent()) === 'Welcome, admin!', }); ``` -{{< /code >}} - ## Groups A note on [groups](https://grafana.com/docs/k6//using-k6/tags-and-groups/#groups), they don't work with async APIs either, there is no workaround as of yet. Here's the [GitHub issue](https://github.com/grafana/k6/issues/2728) that you can follow to keep up-to-date with relevant news on a group API that works with async APIs. diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/options.md b/docs/sources/k6/v1.1.x/using-k6-browser/options.md index 7aff806738..ff8d50b922 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/options.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/options.md @@ -1,13 +1,15 @@ --- title: 'Browser options' description: 'An overview of the different options you can use to customize the browser module behavior when running browser tests.' -weight: 04 +weight: 500 --- # Browser options To enable browser testing, add the `browser` configuration within the `options` property of the [Scenario options](https://grafana.com/docs/k6//using-k6/scenarios/#options). + + ```javascript export const options = { scenarios: { diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/_index.md b/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/_index.md index 2f59344031..1591515738 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/_index.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/_index.md @@ -1,11 +1,11 @@ --- title: 'Recommended practices' description: 'A list of different examples and recommended practices when working with the k6 browser module' -weight: 100 +weight: 350 --- # Recommended practices This section presents some examples and recommended practices when working with the `k6 browser` module to leverage browser automation as part of your k6 tests. -{{< section menuTitle="true">}} \ No newline at end of file +{{< section menuTitle="true">}} diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md b/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md index f046d7b260..c629c598b4 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md @@ -10,10 +10,24 @@ weight: 01 An alternative approach to [browser-based load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites/#browser-based-load-testing) that's much less resource-intensive is combining a small number of virtual users for a browser test with a large number of virtual users for a protocol-level test. -You can achieve hybrid performance in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. +You can achieve [hybrid performance](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. + +Some of the advantages of running a hybrid performance test are: + +- Testing real user flows on the frontend while generating a higher load in the backend. +- Measuring backend and frontend performance in the same test execution +- Increased collaboration between backend and frontend teams since the same tool can be used. + +{{< admonition type="note" >}} + +Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. + +{{< /admonition >}} ## Browser and HTTP test +To run a browser-level and protocol-level test concurrently in k6, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). + The code below shows an example of combining a browser and HTTP test in a single script. While the script exposes the backend to the typical load, it also checks the frontend for any unexpected issues. It also defines thresholds to check both HTTP and browser metrics against pre-defined SLOs. {{< code >}} @@ -71,7 +85,7 @@ export function getPizza() { const res = http.post(`${BASE_URL}/api/pizza`, JSON.stringify(restrictions), { headers: { 'Content-Type': 'application/json', - 'X-User-ID': randomIntBetween(1, 30000), + 'Authorization': 'token abcdef0123456789', }, }); @@ -87,7 +101,8 @@ export async function checkFrontend() { await page.goto(BASE_URL); await check(page.locator('h1'), { - 'header': async lo => await lo.textContent() == 'Looking to break out of your pizza routine?' + header: async (lo) => + (await lo.textContent()) == 'Looking to break out of your pizza routine?', }); await Promise.all([ @@ -97,7 +112,7 @@ export async function checkFrontend() { await page.screenshot({ path: `screenshots/${__ITER}.png` }); await check(page.locator('div#recommendations'), { - 'recommendation': async lo => await lo.textContent() != '', + recommendation: async (lo) => (await lo.textContent()) != '', }); } finally { await page.close(); @@ -127,7 +142,7 @@ The following code shows an example of how you could use the xk6-disruptor exten To find out more information about injecting faults to your service, check out the [Get started with xk6-disruptor guide](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/first-steps/). -{{< code >}} + ```javascript import { browser } from 'k6/browser'; @@ -186,7 +201,8 @@ export async function checkFrontend() { try { await page.goto(BASE_URL); await check(page.locator('h1'), { - 'header': async lo => await lo.textContent() == 'Looking to break out of your pizza routine?' + header: async (lo) => + (await lo.textContent()) == 'Looking to break out of your pizza routine?', }); await Promise.all([ @@ -196,7 +212,7 @@ export async function checkFrontend() { await page.screenshot({ path: `screenshots/${__ITER}.png` }); await check(page.locator('div#recommendations'), { - recommendation: async lo => await lo.textContent() != '', + recommendation: async (lo) => (await lo.textContent()) != '', }); } finally { await page.close(); @@ -204,8 +220,6 @@ export async function checkFrontend() { } ``` -{{< /code >}} - ## Recommended practices - **Start small**. Start with a small number of browser-based virtual users. A good starting point is to have 10% virtual users or less to monitor the user experience for your end-users, while the script emulates around 90% of traffic from the protocol level. diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/running-browser-tests.md b/docs/sources/k6/v1.1.x/using-k6-browser/running-browser-tests.md index 8cb6544aa6..cb2bece525 100644 --- a/docs/sources/k6/v1.1.x/using-k6-browser/running-browser-tests.md +++ b/docs/sources/k6/v1.1.x/using-k6-browser/running-browser-tests.md @@ -1,7 +1,7 @@ --- title: 'Running browser tests' description: 'Follow along to learn how to run a browser test, interact with elements on the page, wait for page navigation, write assertions and run both browser-level and protocol-level tests in a single script.' -weight: 02 +weight: 200 --- # Running browser tests @@ -160,194 +160,3 @@ To run a simple local script: ```bash docker run --rm -i --platform linux/amd64 -v $(pwd):/home/k6/screenshots -e K6_BROWSER_HEADLESS=false grafana/k6:master-with-browser run - /javascript-api/k6-browser/locator) object, which you can later use to interact with the element. - -To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). - -{{< admonition type="note" >}} - -You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-browser/locator). - -{{< /admonition >}} - -{{< code >}} - -```javascript -import { browser } from 'k6/browser'; - -export const options = { - scenarios: { - ui: { - executor: 'shared-iterations', - options: { - browser: { - type: 'chromium', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - // Enter login credentials - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - await page.screenshot({ path: 'screenshots/screenshot.png' }); - } finally { - await page.close(); - } -} -``` - -{{< /code >}} - -The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. - -Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. - -## Asynchronous operations - -Since many browser operations happen asynchronously, and to follow the Playwright API more closely, we are working on migrating most of the browser module methods to be asynchronous as well. - -At the moment, methods such as `page.goto()`, `page.waitForNavigation()` and `Element.click()` return [JavaScript promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), and scripts must be written to handle this properly. - -To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. - -{{< code >}} - -```javascript -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', - }, - }, - }, - }, - thresholds: { - checks: ['rate==1.0'], - }, -}; - -export default async function () { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/my_messages.php'); - - await page.locator('input[name="login"]').type('admin'); - await page.locator('input[name="password"]').type('123'); - - const submitButton = page.locator('input[type="submit"]'); - - await Promise.all([page.waitForNavigation(), submitButton.click()]); - - await check(page.locator('h2'), { - header: async (lo) => (await lo.textContent()) == 'Welcome, admin!', - }); - } finally { - await page.close(); - } -} -``` - -{{< /code >}} - -The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. - -Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. - -## Run both browser-level and protocol-level tests in a single script - -The real power of the browser module shines when it’s combined with the existing features of k6. A common scenario that you can try is to mix a smaller subset of browser-level tests with a larger protocol-level test which can simulate how your website responds to various performance events. This approach is what we refer to as [hybrid load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) and provides advantages such as: - -- testing real user flows on the frontend while generating a higher load in the backend -- measuring backend and frontend performance in the same test execution -- increased collaboration between backend and frontend teams since the same tool can be used - -To run a browser-level and protocol-level test concurrently, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). - -{{< admonition type="note" >}} - -Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. - -{{< /admonition >}} - -{{< code >}} - -```javascript -import { browser } from 'k6/browser'; -import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; -import http from 'k6/http'; - -export const options = { - scenarios: { - browser: { - executor: 'constant-vus', - exec: 'browserTest', - vus: 1, - duration: '10s', - options: { - browser: { - type: 'chromium', - }, - }, - }, - news: { - executor: 'constant-vus', - exec: 'news', - vus: 20, - duration: '1m', - }, - }, -}; - -export async function browserTest() { - const page = await browser.newPage(); - - try { - await page.goto('https://test.k6.io/browser.php'); - - await page.locator('#checkbox1').check(); - - await check(page.locator('#checkbox-info-display'), { - 'checkbox is checked': async (lo) => - (await lo.textContent()) === 'Thanks for checking the box', - }); - } finally { - await page.close(); - } -} - -export function news() { - const res = http.get('https://test.k6.io/news.php'); - - check(res, { - 'status is 200': (r) => r.status === 200, - }); -} -``` - -{{< /code >}} - -The preceding code contains two scenarios. One for the browser-level test called `browser` and one for the protocol-level test called `news`. Both scenarios are using the [constant-vus executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) which introduces a constant number of virtual users to execute as many iterations as possible for a specified amount of time. - -Since it's all in one script, this allows for greater collaboration amongst teams. diff --git a/docs/sources/k6/v1.1.x/using-k6-browser/write-your-first-browser-test.md b/docs/sources/k6/v1.1.x/using-k6-browser/write-your-first-browser-test.md new file mode 100644 index 0000000000..e4c36167e4 --- /dev/null +++ b/docs/sources/k6/v1.1.x/using-k6-browser/write-your-first-browser-test.md @@ -0,0 +1,223 @@ +--- +title: 'Write your first browser test' +description: 'Learn how to write your first k6 browser test script.' +weight: 100 +--- + +# Write your first browser test + +k6 browser tests allow you to simulate real user interactions with web applications, such as clicking buttons, filling out forms, and verifying page content. This helps you test not only backend performance, but also frontend reliability and user experience. + +In this guide, you'll learn how to write your first k6 browser test script using the browser module. + +## Before you begin + +To write and run k6 browser tests, you'll need: + +- A basic knowledge of JavaScript or TypeScript. +- A code editor to write your scripts, such as [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains editors](https://www.jetbrains.com/). +- A Chromium-based browser (such as Google Chrome) installed locally. +- [Install k6](https://grafana.com/docs/k6//set-up/install-k6/) on your machine. + +## Basic structure of a k6 browser test + +For k6 to be able to interpret and execute your test, every k6 script follows a common structure, revolving around a few core components: + +1. **Default function**: This is where the test logic resides. It defines what your test will do and how it will behave during execution. It should be exported as the default function in your script. For browser scripts, it's written as an `async` function to handle browser actions. +1. **Imports**: You can import additional [k6 modules](https://grafana.com/docs/k6//javascript-api/) or [JavaScript libraries (jslibs)](https://grafana.com/docs/k6//javascript-api/jslib/) to extend your script’s functionality, such as making HTTP requests. Note that k6 is not built upon Node.js, and instead uses its own JavaScript runtime. Compatibility with some npm modules may vary. +1. **Options**: Enable you to configure the execution of the test, such as defining the number of virtual users, the test duration, or setting performance thresholds. Refer to [Options](https://grafana.com/docs/k6//using-k6/k6-options/) for more details. +1. **Lifecycle operations (optional)**: Because your test might need to run code before and/or after the execution of the test logic, such as parsing data from a file, or download an object from Amazon S3, [lifecycle operations](https://grafana.com/docs/k6//using-k6/test-lifecycle/) allow you to write code, either as predefined functions or within specific code scopes, that will be executed at different stages of the test execution. + +## Key concepts + +To write k6 browser tests, its important to understand a few key concepts: + +1. **Browser type**: A browser script must have the `options.browser.type` field set in the `options` object. For example: + + ```js + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }; + ``` + + If that option isn't set, k6 will throw an error when trying to execute your script. + +1. **Asynchronous operations**: Browser interactions (like navigation and clicks) are asynchronous, so your test must use `async`/`await`. Refer to [Asynchronous operations](https://grafana.com/docs/k6//using-k6-browser/how-to-write-browser-tests/asynchronous-operations/) for more details. +1. **Locators**: The Locator API can be used to find and interact with elements on a page, such as buttons or headers. There are other ways to find elements, such as using the `Page.$()` method, but the Locator API provides several benefits, including finding an element even if the underlying frame navigates, and working with dynamic web pages and SPAs such as React. + +## Write your first browser test script + +Let's walk through creating a browser test that loads a page, clicks a button, takes a screenshot, and checks for recommendations. + +1. **Create a test file**: A test file can be named anything you like, and live wherever you see fit in your project, but it should have a `.js` or `.ts` extension. In this example, create a JavaScript file named `my-first-browser-test.js`. Open your terminal and run the following command: + + ```bash + touch my-first-browser-test.js + ``` + +1. **Import k6 modules**: Import the necessary modules for HTTP requests, browser automation, and utility functions. + + + + ```js + import http from 'k6/http'; + import exec from 'k6/execution'; + import { browser } from 'k6/browser'; + import { sleep, check, fail } from 'k6'; + ``` + +1. **Define options**: Configure the `options` object to use the browser executor and specify the browser type. + + + + ```js + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }; + ``` + +1. **Setup function**: Create a `setup` function to check if the target site is available, before running the main test logic. + + + + ```typescript + export function setup() { + let res = http.get(BASE_URL); + if (res.status !== 200) { + exec.test.abort(`Got unexpected status code ${res.status} when trying to setup. Exiting.`); + } + } + ``` + +1. **Default function**: The default exported function is the entry point for the test script. It will be executed repeatedly the number of times you define with the `iterations` option. In this function, using `async`/`await`, add the functions that open a browser page, interacts with the elements on the page, takes a screenshot, and performs a `check`: + + + + ```typescript + export default async function () { + let checkData; + const page = await browser.newPage(); + + try { + await page.goto(BASE_URL); + + checkData = await page.locator('h1').textContent(); + check(page, { + header: checkData === 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + + await page.screenshot({ path: 'screenshot.png' }); + + checkData = await page.locator('div#recommendations').textContent(); + check(page, { + recommendation: checkData !== '', + }); + } catch (error) { + fail(`Browser iteration failed: ${error.message}`); + } finally { + await page.close(); + } + + sleep(1); + } + ``` + +Your final script should look like this: + +```js +import http from 'k6/http'; +import exec from 'k6/execution'; +import { browser } from 'k6/browser'; +import { sleep, check, fail } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'https://quickpizza.grafana.com'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export function setup() { + const res = http.get(BASE_URL); + if (res.status !== 200) { + exec.test.abort(`Got unexpected status code ${res.status} when trying to setup. Exiting.`); + } +} + +export default async function () { + let checkData; + const page = await browser.newPage(); + + try { + await page.goto(BASE_URL); + + checkData = await page.locator('h1').textContent(); + check(page, { + header: checkData === 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + + await page.screenshot({ path: 'screenshot.png' }); + + checkData = await page.locator('div#recommendations').textContent(); + check(page, { + recommendation: checkData !== '', + }); + } catch (error) { + fail(`Browser iteration failed: ${error.message}`); + } finally { + await page.close(); + } + + sleep(1); +} +``` + +## Extending your browser test + +Once you're comfortable with the basics, you can extend your browser test by: + +- Simulating more complex user flows (multiple pages, form submissions, etc.). +- Adding more checks and assertions to validate UI elements and content. +- Creating hybrid performance tests to test both the frontend and backend simultaneously. + +Refer to the [Using k6 browser](https://grafana.com/docs/k6//using-k6-browser/) section for more details about the browser module and its features. + +## Next steps + +Now that you've written your first k6 browser test script, it's time to run it. Refer to [Running k6 browser tests](https://grafana.com/docs/k6//using-k6-browser/running-browser-tests/) for instructions on executing your script and analyzing results.