<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Recommended practices on Grafana Labs</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/</link><description>Recent content in Recommended practices on Grafana Labs</description><generator>Hugo -- gohugo.io</generator><language>en</language><atom:link href="/docs/k6/v2.3.x/using-k6-browser/recommended-practices/index.xml" rel="self" type="application/rss+xml"/><item><title>Handle stale or dynamic elements after navigation</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/handle-dynamic-elements/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/handle-dynamic-elements/</guid><content><![CDATA[&lt;h1 id=&#34;handle-stale-or-dynamic-elements-after-navigation&#34;&gt;Handle stale or dynamic elements after navigation&lt;/h1&gt;
&lt;p&gt;Modern websites often update the DOM asynchronously after navigation or user interactions. Waiting for navigation to complete isn&amp;rsquo;t sufficient, as test scripts may still fail or attempt to interact with elements that aren&amp;rsquo;t yet available.&lt;/p&gt;
&lt;p&gt;To avoid these issues, wait for specific elements to appear before you continue your test. Use 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/locator/&#34;&gt;locator APIs&lt;/a&gt; such as &lt;code&gt;waitFor&lt;/code&gt; to ensure elements are ready for interaction.&lt;/p&gt;
&lt;p&gt;This approach is especially important when you test single-page applications (SPAs) or any pages with dynamic content, where elements may be added, removed, or updated asynchronously.&lt;/p&gt;
&lt;h2 id=&#34;example&#34;&gt;Example&lt;/h2&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  await page.goto(&amp;#39;https://test.k6.io/browser.php&amp;#39;);
  const text = page.locator(&amp;#39;#input-text-hidden&amp;#39;);
  await text.waitFor({
    state: &amp;#39;hidden&amp;#39;,
  });
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="handle-stale-or-dynamic-elements-after-navigation">Handle stale or dynamic elements after navigation&lt;/h1>
&lt;p>Modern websites often update the DOM asynchronously after navigation or user interactions. Waiting for navigation to complete isn&amp;rsquo;t sufficient, as test scripts may still fail or attempt to interact with elements that aren&amp;rsquo;t yet available.&lt;/p></description></item><item><title>Hybrid approach to performance</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance/</guid><content><![CDATA[&lt;h1 id=&#34;hybrid-performance-with-k6-browser&#34;&gt;Hybrid performance with k6 browser&lt;/h1&gt;
&lt;p&gt;An alternative approach to 
    &lt;a href=&#34;/docs/k6/v2.3.x/testing-guides/load-testing-websites/#browser-based-load-testing&#34;&gt;browser-based load testing&lt;/a&gt; that&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;You can achieve 
    &lt;a href=&#34;/docs/k6/v2.3.x/testing-guides/load-testing-websites/#hybrid-load-testing&#34;&gt;hybrid performance&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;Some of the advantages of running a hybrid performance test are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Testing real user flows on the frontend while generating a higher load in the backend.&lt;/li&gt;
&lt;li&gt;Measuring backend and frontend performance in the same test execution&lt;/li&gt;
&lt;li&gt;Increased collaboration between backend and frontend teams since the same tool can be used.&lt;/li&gt;
&lt;/ul&gt;


&lt;div class=&#34;admonition admonition-note&#34;&gt;&lt;blockquote&gt;&lt;p class=&#34;title text-uppercase&#34;&gt;Note&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;

&lt;h2 id=&#34;browser-and-http-test&#34;&gt;Browser and HTTP test&lt;/h2&gt;
&lt;p&gt;To run a browser-level and protocol-level test concurrently in k6, you can use 
    &lt;a href=&#34;/docs/k6/v2.3.x/using-k6/scenarios/&#34;&gt;scenarios&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { check } from &amp;#39;https://jslib.k6.io/k6-utils/1.5.0/index.js&amp;#39;;
import { randomIntBetween } from &amp;#39;https://jslib.k6.io/k6-utils/1.2.0/index.js&amp;#39;;
import http from &amp;#39;k6/http&amp;#39;;

const BASE_URL = __ENV.BASE_URL || &amp;#39;https://quickpizza.grafana.com&amp;#39;;

export const options = {
  scenarios: {
    load: {
      exec: &amp;#39;getPizza&amp;#39;,
      executor: &amp;#39;ramping-vus&amp;#39;,
      stages: [
        { duration: &amp;#39;5s&amp;#39;, target: 5 },
        { duration: &amp;#39;10s&amp;#39;, target: 5 },
        { duration: &amp;#39;5s&amp;#39;, target: 0 },
      ],
      startTime: &amp;#39;10s&amp;#39;,
    },
    browser: {
      exec: &amp;#39;checkFrontend&amp;#39;,
      executor: &amp;#39;constant-vus&amp;#39;,
      vus: 1,
      duration: &amp;#39;30s&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
  thresholds: {
    http_req_failed: [&amp;#39;rate&amp;lt;0.01&amp;#39;],
    http_req_duration: [&amp;#39;p(95)&amp;lt;500&amp;#39;, &amp;#39;p(99)&amp;lt;1000&amp;#39;],
    browser_web_vital_fcp: [&amp;#39;p(95) &amp;lt; 1000&amp;#39;],
    browser_web_vital_lcp: [&amp;#39;p(95) &amp;lt; 2000&amp;#39;],
  },
};

export function getPizza() {
  const restrictions = {
    maxCaloriesPerSlice: 500,
    mustBeVegetarian: false,
    excludedIngredients: [&amp;#39;pepperoni&amp;#39;],
    excludedTools: [&amp;#39;knife&amp;#39;],
    maxNumberOfToppings: 6,
    minNumberOfToppings: 2,
  };

  const res = http.post(`${BASE_URL}/api/pizza`, JSON.stringify(restrictions), {
    headers: {
      &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39;,
      &amp;#39;Authorization&amp;#39;: &amp;#39;token abcdef0123456789&amp;#39;,
    },
  });

  check(res, {
    &amp;#39;status is 200&amp;#39;: (res) =&amp;gt; res.status === 200,
  });
}

export async function checkFrontend() {
  const page = await browser.newPage();

  try {
    await page.goto(BASE_URL);

    await check(page.locator(&amp;#39;h1&amp;#39;), {
      header: async (lo) =&amp;gt;
        (await lo.textContent()) == &amp;#39;Looking to break out of your pizza routine?&amp;#39;,
    });

    await Promise.all([
      page.locator(&amp;#39;//button[. = &amp;#34;Pizza, Please!&amp;#34;]&amp;#39;).click(),
      page.waitForTimeout(500),
    ]);
    await page.screenshot({ path: `screenshots/${__ITER}.png` });

    await check(page.locator(&amp;#39;div#recommendations&amp;#39;), {
      recommendation: async (lo) =&amp;gt; (await lo.textContent()) != &amp;#39;&amp;#39;,
    });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you save that script to a local file named &lt;code&gt;test.js&lt;/code&gt;, you can run it with:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;k6 run test.js&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;To work on one part of the hybrid test, 
    &lt;a href=&#34;/docs/k6/v2.3.x/using-k6/scenarios/#run-selected-scenarios&#34;&gt;select its scenario&lt;/a&gt; without editing the script:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;sh&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;k6 run --scenario load test.js
k6 run --scenario browser test.js&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;load&lt;/code&gt; selection keeps its ramping stages and delayed start. The &lt;code&gt;browser&lt;/code&gt; selection keeps its browser settings and runs with one VU for 30 seconds. The example&amp;rsquo;s thresholds have no &lt;code&gt;scenario&lt;/code&gt; filter, so all remain active when you select either scenario; selection does not remove thresholds merely because their metrics come from the excluded workload.&lt;/p&gt;
&lt;p&gt;The script also includes a common best practice by defining a &lt;code&gt;BASE_URL&lt;/code&gt; variable, and using the 
    &lt;a href=&#34;/docs/k6/v2.3.x/using-k6/environment-variables/&#34;&gt;environment variable&lt;/a&gt; value &lt;code&gt;__ENV.BASE_URL&lt;/code&gt; if it exists. That&amp;rsquo;s useful if you want to use the same script for multiple environments, such as staging and production, and you could pass that value to your script with the command:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;k6 run -e BASE_URL=https://quickpizza.grafana.com test.js&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;browser-and-failure-injection-test&#34;&gt;Browser and failure injection test&lt;/h2&gt;
&lt;p&gt;You can also run a browser test together with a failure injection test by using the &lt;a href=&#34;https://github.com/grafana/xk6-disruptor&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;xk6-disruptor&lt;/a&gt; extension. This approach lets you find issues in your frontend if any services it depends on are suddenly injected with failures, such as delays or server errors.&lt;/p&gt;
&lt;p&gt;The following code shows an example of how you could use the xk6-disruptor extension to introduce faults to a Kubernetes service. At the same time, the &lt;code&gt;browser&lt;/code&gt; scenario runs to ensure the frontend application is free of any unexpected errors that may not have been handled properly.&lt;/p&gt;
&lt;p&gt;To find out more information about injecting faults to your service, check out the 
    &lt;a href=&#34;/docs/k6/v2.3.x/testing-guides/injecting-faults-with-xk6-disruptor/first-steps/&#34;&gt;Get started with xk6-disruptor guide&lt;/a&gt;.&lt;/p&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { check } from &amp;#39;https://jslib.k6.io/k6-utils/1.5.0/index.js&amp;#39;;
import { ServiceDisruptor } from &amp;#39;k6/x/disruptor&amp;#39;;

const BASE_URL = __ENV.BASE_URL;

export const options = {
  scenarios: {
    disrupt: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      iterations: 1,
      vus: 1,
      exec: &amp;#39;disrupt&amp;#39;,
    },
    browser: {
      executor: &amp;#39;constant-vus&amp;#39;,
      vus: 1,
      duration: &amp;#39;10s&amp;#39;,
      startTime: &amp;#39;10s&amp;#39;,
      exec: &amp;#39;browser&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
  thresholds: {
    browser_web_vital_fcp: [&amp;#39;p(95) &amp;lt; 1000&amp;#39;],
    browser_web_vital_lcp: [&amp;#39;p(95) &amp;lt; 2000&amp;#39;],
  },
};

// Add faults to the service by introducing a delay of 1s and 503 errors to 10% of the requests.
const fault = {
  averageDelay: &amp;#39;1000ms&amp;#39;,
  errorRate: 0.1,
  errorCode: 503,
};

export function disrupt() {
  const disruptor = new ServiceDisruptor(&amp;#39;pizza-info&amp;#39;, &amp;#39;pizza-ns&amp;#39;);
  const targets = disruptor.targets();
  if (targets.length == 0) {
    throw new Error(&amp;#39;expected list to have one target&amp;#39;);
  }

  disruptor.injectHTTPFaults(fault, &amp;#39;20s&amp;#39;);
}

export async function checkFrontend() {
  const page = await browser.newPage();

  try {
    await page.goto(BASE_URL);
    await check(page.locator(&amp;#39;h1&amp;#39;), {
      header: async (lo) =&amp;gt;
        (await lo.textContent()) == &amp;#39;Looking to break out of your pizza routine?&amp;#39;,
    });

    await Promise.all([
      page.locator(&amp;#39;//button[. = &amp;#34;Pizza, Please!&amp;#34;]&amp;#39;).click(),
      page.waitForTimeout(500),
    ]);
    await page.screenshot({ path: `screenshots/${__ITER}.png` });

    await check(page.locator(&amp;#39;div#recommendations&amp;#39;), {
      recommendation: async (lo) =&amp;gt; (await lo.textContent()) != &amp;#39;&amp;#39;,
    });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;recommended-practices&#34;&gt;Recommended practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start small&lt;/strong&gt;. 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combine browser test with different load testing types&lt;/strong&gt;. To fully understand the impact of different traffic patterns on your end-user experience, experiment with running your browser test with different 
    &lt;a href=&#34;/docs/k6/v2.3.x/testing-guides/test-types/&#34;&gt;load testing types&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Focus on high-risk user journeys as a start&lt;/strong&gt;. Identify the high-risk user journeys first so you can start monitoring the web performance metrics for them while your backend applications are being exposed to high traffic or service faults.&lt;/li&gt;
&lt;/ul&gt;
]]></content><description>&lt;h1 id="hybrid-performance-with-k6-browser">Hybrid performance with k6 browser&lt;/h1>
&lt;p>An alternative approach to
&lt;a href="/docs/k6/v2.3.x/testing-guides/load-testing-websites/#browser-based-load-testing">browser-based load testing&lt;/a> that&amp;rsquo;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.&lt;/p></description></item><item><title>Page object model</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/page-object-model-pattern/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/page-object-model-pattern/</guid><content><![CDATA[&lt;h1 id=&#34;page-object-model&#34;&gt;Page object model&lt;/h1&gt;
&lt;p&gt;When working with large test suites, a popular design pattern to improve your code&amp;rsquo;s maintainability and readability is the &lt;a href=&#34;https://martinfowler.com/bliki/PageObject.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;page object model&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A page object commonly represents an HTML page or significant elements/components within a page, such as a header or a footer. It&amp;rsquo;s a form of encapsulation that hides the details of the UI structure from other places, such as your test files. Through page object models, any changes you need to make on a specific page or element within a page are constrained into a single place, resulting in ease of maintenance and avoiding code duplication.&lt;/p&gt;
&lt;p&gt;Since k6 browser is inspired by Playwright, you can leverage any existing page objects you have and re-use them with your k6 browser tests.&lt;/p&gt;
&lt;h2 id=&#34;implementation&#34;&gt;Implementation&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s take an example of a website with a booking form added to the homepage. Imagine you want to write a test that checks that a user can fill out the booking form successfully.&lt;/p&gt;
&lt;p&gt;To model a page object for the homepage, we&amp;rsquo;ve created a page object class called &lt;code&gt;homepage.js&lt;/code&gt;. Different locators are created inside the constructor so that when the homepage class is instantiated, the page locator elements are ready to be used.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;homepage.js&lt;/code&gt; class also contains different methods for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigating to the homepage&lt;/li&gt;
&lt;li&gt;Submitting the form&lt;/li&gt;
&lt;li&gt;Getting the verification message&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When locators need to be updated or other specific changes related to the homepage are made, you only need to update the &lt;code&gt;homepage.js&lt;/code&gt; class.&lt;/p&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { bookingData } from &amp;#39;../data/booking-data.js&amp;#39;;

export class Homepage {
  constructor(page) {
    this.page = page;
    this.nameField = page.locator(&amp;#39;[data-testid=&amp;#34;ContactName&amp;#34;]&amp;#39;);
    this.emailField = page.locator(&amp;#39;[data-testid=&amp;#34;ContactEmail&amp;#34;]&amp;#39;);
    this.phoneField = page.locator(&amp;#39;[data-testid=&amp;#34;ContactPhone&amp;#34;]&amp;#39;);
    this.subjectField = page.locator(&amp;#39;[data-testid=&amp;#34;ContactSubject&amp;#34;]&amp;#39;);
    this.descField = page.locator(&amp;#39;[data-testid=&amp;#34;ContactDescription&amp;#34;]&amp;#39;);
    this.submitButton = page.locator(&amp;#39;#submitContact&amp;#39;);
    this.verificationMessage = page.locator(&amp;#39;.row.contact h2&amp;#39;);
  }

  async goto() {
    await this.page.goto(&amp;#39;https://myexamplewebsite/&amp;#39;);
  }

  async submitForm() {
    const { name, email, phone, subject, description } = bookingData;

    this.nameField.type(name);
    this.emailField.type(email);
    this.phoneField.type(phone);
    this.subjectField.type(subject);
    this.descField.type(description);
    await this.submitButton.click();
  }

  async getVerificationMessage() {
    return this.verificationMessage.innerText();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You can import the &lt;code&gt;Homepage&lt;/code&gt; class within your test class and invoke the methods you need. This makes the code easier to understand and enforces the separation between your test and business logic.&lt;/p&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { expect } from &amp;#39;https://jslib.k6.io/k6chaijs/4.3.4.0/index.js&amp;#39;;

import { Homepage } from &amp;#39;../pages/homepage.js&amp;#39;;
import { bookingData } from &amp;#39;../data/booking-data.js&amp;#39;;

export default async function () {
  const page = await browser.newPage();

  const { name } = bookingData;

  const homepage = new Homepage(page);
  await homepage.goto();
  await homepage.submitForm();

  expect(await homepage.getVerificationMessage()).to.contain(name);

  await page.close();
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="page-object-model">Page object model&lt;/h1>
&lt;p>When working with large test suites, a popular design pattern to improve your code&amp;rsquo;s maintainability and readability is the &lt;a href="https://martinfowler.com/bliki/PageObject.html" target="_blank" rel="noopener noreferrer">page object model&lt;/a>.&lt;/p></description></item><item><title>Prevent cookie banners from blocking interactions</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/prevent-cookie-banners-blocking/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/prevent-cookie-banners-blocking/</guid><content><![CDATA[&lt;h1 id=&#34;prevent-cookie-banners-from-blocking-interactions&#34;&gt;Prevent cookie banners from blocking interactions&lt;/h1&gt;
&lt;p&gt;Cookie banners often appear shortly after a page loads or remain hidden until you interact with the page. Leaving a banner open can block page elements and cause interaction failures. For example, a button might not be clickable because the banner overlaps it. If you find yourself using &lt;em&gt;force click&lt;/em&gt; frequently, the element is likely hidden or overlapped by a cookie banner.&lt;/p&gt;
&lt;p&gt;To avoid these failures, trigger and dismiss the cookie banner early in the test flow.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Trigger the banner&lt;/strong&gt;: simulate a small user action, such as clicking or scrolling, immediately after navigation to reveal banners that appear only after interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dismiss the banner&lt;/strong&gt;: once the banner is visible, close or accept it before continuing with the rest of the test.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;example&#34;&gt;Example&lt;/h2&gt;
&lt;p&gt;The example below navigates to a page and clicks a button. Replace the placeholder steps with a click to an element on your website, or scrolling a page, that reveals and dismisses your site&amp;rsquo;s cookie banner.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { expect } from &amp;#39;https://jslib.k6.io/k6-testing/0.6.1/index.js&amp;#39;;

export const options = {
  scenarios: {
    ui: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
  thresholds: {
    checks: [&amp;#39;rate==1.0&amp;#39;],
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    await page.goto(&amp;#39;https://quickpizza.grafana.com/&amp;#39;);

    // Click an element to trigger a cookie banner.
    // Clicking an element also scrolls the page if the
    // element is outside the frame.
    const button = page.locator(&amp;#39;button[name=&amp;#34;pizza-please&amp;#34;]&amp;#39;)
    await expect(button).toBeEnabled();
    await button.click();

    // Dismiss the cookie banner
    const cookieBannerButton = page.locator(&amp;#39;button[name=&amp;#34;dismiss-cookie&amp;#34;]&amp;#39;)
    await expect(button).toBeEnabled();
    await button.click();
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="prevent-cookie-banners-from-blocking-interactions">Prevent cookie banners from blocking interactions&lt;/h1>
&lt;p>Cookie banners often appear shortly after a page loads or remain hidden until you interact with the page. Leaving a banner open can block page elements and cause interaction failures. For example, a button might not be clickable because the banner overlaps it. If you find yourself using &lt;em>force click&lt;/em> frequently, the element is likely hidden or overlapped by a cookie banner.&lt;/p></description></item><item><title>Prevent too many time series error</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/prevent-too-many-time-series-error/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/prevent-too-many-time-series-error/</guid><content><![CDATA[&lt;h1 id=&#34;prevent-too-many-time-series-error&#34;&gt;Prevent too many time series error&lt;/h1&gt;
&lt;p&gt;Modern websites are complex and make a large number of requests to function as intended by their developers. These requests no longer serve only content for display to the end user but also retrieve insights, analytics, advertisements, and cache-busting purposes. Such requests are usually generated dynamically and may contain frequently changing IDs, posing challenges when correlating and analyzing your k6 test results.&lt;/p&gt;
&lt;p&gt;When load testing a website using the k6 browser module, these dynamic requests can result in a high number of similar-looking requests, making it difficult to correlate them and extract valuable insights. This can also lead to test errors, such as a &lt;code&gt;too-many-metrics&lt;/code&gt; error, due to high cardinality from metrics tagged with similar but dynamically changing URLs.&lt;/p&gt;
&lt;p&gt;This issue also affects synthetic tests. While you may not encounter the &lt;em&gt;too-many-metrics&lt;/em&gt; error, you may end up with a large amount of uncorrelated metric data that cannot be tracked effectively over time.&lt;/p&gt;
&lt;p&gt;To address this, the browser module has a 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/on/&#34;&gt;page.on(&amp;lsquo;metric&amp;rsquo;)&lt;/a&gt; method, which allows you to define URL patterns using regex for matching. When a match is found, the URL and name tags for the metric are replaced with the new name.&lt;/p&gt;
&lt;h2 id=&#34;example-usage&#34;&gt;Example usage&lt;/h2&gt;
&lt;!-- eslint-skip--&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    ui: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  // Here, we set up an event listener using page.on(&amp;#39;metric&amp;#39;).
  // You can call page.on(&amp;#39;metric&amp;#39;) multiple times, and each callback function
  // will be executed in the order that page.on was called.
  page.on(&amp;#39;metric&amp;#39;, (metric) =&amp;gt; {
    // Currently, metric.tag is the only available method on the metric object.
    // It enables matching on the URL tag using a specified regex pattern.
    // You can call metric.tag multiple times within the callback function.
    metric.tag({
      // This is the new name assigned to any metric that matches the defined
      // URL pattern below.
      name: &amp;#39;test&amp;#39;,
      // Provide one or more match patterns here. Any metrics that match a pattern
      // will use the new name specified above.
      matches: [
        // Each match pattern can include a URL and an optional method.
        // When a method is specified, the metric must match both the URL pattern
        // and the method. If no method is provided, the pattern will match all
        // HTTP methods.
        { url: /^https:\/\/test\.k6\.io\/\?q=[0-9a-z]&amp;#43;$/, method: &amp;#39;GET&amp;#39; },
      ],
    });
  });

  try {
    // The following lines are for demonstration purposes.
    // Visiting URLs with different query parameters (q) to illustrate matching.
    await page.goto(&amp;#39;https://test.k6.io/?q=abc123&amp;#39;);
    await page.goto(&amp;#39;https://test.k6.io/?q=def456&amp;#39;);
  } finally {
    // Ensure the page is closed after testing.
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="prevent-too-many-time-series-error">Prevent too many time series error&lt;/h1>
&lt;p>Modern websites are complex and make a large number of requests to function as intended by their developers. These requests no longer serve only content for display to the end user but also retrieve insights, analytics, advertisements, and cache-busting purposes. Such requests are usually generated dynamically and may contain frequently changing IDs, posing challenges when correlating and analyzing your k6 test results.&lt;/p></description></item><item><title>Select elements</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/select-elements/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/select-elements/</guid><content><![CDATA[&lt;h1 id=&#34;select-elements&#34;&gt;Select elements&lt;/h1&gt;
&lt;p&gt;Selectors are strings that represents a specific DOM element on the page. When writing browser-level tests, it&amp;rsquo;s recommended to use selectors that are robust to avoid test flakiness when the DOM structure changes.&lt;/p&gt;
&lt;p&gt;Currently, the browser module supports the standard &lt;strong&gt;CSS and XPath selectors&lt;/strong&gt;.&lt;/p&gt;


&lt;div class=&#34;admonition admonition-note&#34;&gt;&lt;blockquote&gt;&lt;p class=&#34;title text-uppercase&#34;&gt;Note&lt;/p&gt;&lt;p&gt;Text-based selectors are currently not supported. This will be supported in future releases.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;

&lt;h2 id=&#34;recommended-practices&#34;&gt;Recommended practices&lt;/h2&gt;
&lt;p&gt;The selectors that you choose should not be tightly coupled to any behaviour or styling changes. If your application is prone to changes frequently, it&amp;rsquo;s recommended to use user-facing attributes or custom data attributes as these are not tightly coupled to the element.&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Selector&lt;/th&gt;
              &lt;th&gt;Notes&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;✅ &lt;code&gt;page.locator(&#39;[aria-label=&amp;quot;Login&amp;quot;]&#39;)&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;User-facing attributes such as ARIA labels are rarely changed so these are great candidates.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;✅ &lt;code&gt;page.locator(&#39;[data-test=&amp;quot;login&amp;quot;]&#39;)&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Custom data attributes are not tightly coupled to the element.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;✅ &lt;code&gt;page.locator(&#39;//button[text()=&amp;quot;Submit&amp;quot;]&#39;)&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Text selectors are also great as text content rarely changes. While we don&amp;rsquo;t support text-based selectors as of yet, xpath can be used as a workaround.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;⚠️ &lt;code&gt;page.locator(&#39;#login-btn&#39;)&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Selecting an element via its ID is also recommended if the ID doesn&amp;rsquo;t change.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;⚠️ &lt;code&gt;page.locator(&#39;.login-btn&#39;)&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Selecting an element via its class name should be kept to a minimum as class names can be duplicated.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;❌ &lt;code&gt;page.locator(&#39;button&#39;)&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Generic elements are not recommended because this has no context.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;❌ &lt;code&gt;page.locator(&#39;/html[1]/body[1]/main[1]&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Absolute paths are not recommended as these are tightly coupled to the DOM structure.&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;]]></content><description>&lt;h1 id="select-elements">Select elements&lt;/h1>
&lt;p>Selectors are strings that represents a specific DOM element on the page. When writing browser-level tests, it&amp;rsquo;s recommended to use selectors that are robust to avoid test flakiness when the DOM structure changes.&lt;/p></description></item><item><title>Simulate user input delay</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/simulate-user-input-delay/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/simulate-user-input-delay/</guid><content><![CDATA[&lt;h1 id=&#34;simulate-user-input-delay&#34;&gt;Simulate user input delay&lt;/h1&gt;
&lt;p&gt;On this page, you&amp;rsquo;ll learn how to best work with &lt;code&gt;sleep&lt;/code&gt; in &lt;code&gt;k6&lt;/code&gt; and the various &lt;code&gt;wait*&lt;/code&gt; prepended methods available in &lt;code&gt;k6/browser&lt;/code&gt; to simulate user input delay, wait for navigations, and wait for element state changes. By the end of this page, you should be able to successfully use the correct API where necessary.&lt;/p&gt;


&lt;div class=&#34;admonition admonition-note&#34;&gt;&lt;blockquote&gt;&lt;p class=&#34;title text-uppercase&#34;&gt;Note&lt;/p&gt;&lt;p&gt;While using the &lt;code&gt;sleep&lt;/code&gt; or &lt;code&gt;page.waitForTimeout&lt;/code&gt; functions to wait for element state changes may seem helpful, it&amp;rsquo;s best to avoid them to prevent flakey tests. Instead, it&amp;rsquo;s better to use the other &lt;code&gt;wait*&lt;/code&gt; prepended methods listed on this page.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;

&lt;h2 id=&#34;what-is-sleep&#34;&gt;What is &lt;code&gt;sleep&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6/sleep/&#34;&gt;sleep&lt;/a&gt; is a first class function built into k6. It&amp;rsquo;s main use is to &lt;em&gt;&amp;ldquo;suspend VU execution for the specified duration&amp;rdquo;&lt;/em&gt; which is most useful when you want to simulate user input delay, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigating to a page.&lt;/li&gt;
&lt;li&gt;Sleeping for one second to simulate a user looking for a specific element on the page.&lt;/li&gt;
&lt;li&gt;Clicking on the element.&lt;/li&gt;
&lt;/ul&gt;


&lt;div class=&#34;admonition admonition-warning&#34;&gt;&lt;blockquote&gt;&lt;p class=&#34;title text-uppercase&#34;&gt;Warning&lt;/p&gt;&lt;p&gt;&lt;code&gt;sleep&lt;/code&gt; is a synchronous function that blocks the JavaScript event loop, which means that all asynchronous work will also be suspended until &lt;code&gt;sleep&lt;/code&gt; completes.&lt;/p&gt;
&lt;p&gt;The browser module predominantly provides asynchronous APIs, so it&amp;rsquo;s best to avoid working with &lt;code&gt;sleep&lt;/code&gt;. Instead, &lt;em&gt;use the &lt;a href=&#34;#pagewaitfortimeout&#34;&gt;page.waitForTimeout&lt;/a&gt; function&lt;/em&gt;. Refer to 
    &lt;a href=&#34;/docs/k6/v2.3.x/using-k6-browser/recommended-practices/sleep-vs-page-wait-for-timeout/&#34;&gt;When to use sleep() and page.waitForTimeout()&lt;/a&gt; for more details.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;

&lt;h2 id=&#34;what-is-wait&#34;&gt;What is &lt;code&gt;wait*&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;In the browser modules there are various asynchronous APIs that can be used to wait for certain states:&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Method&lt;/th&gt;
              &lt;th&gt;Description&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#browsercontextwaitforevent&#34;&gt;browserContext.waitForEvent&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Waits for the selected event to fire and returns its value.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitforfunction&#34;&gt;page.waitForFunction&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Waits for the given function to return a truthy value.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitforloadstate&#34;&gt;page.waitForLoadState&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Waits for the specified page life cycle event.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitfornavigation&#34;&gt;page.waitForNavigation&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Waits for the navigation to complete after one starts.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitforresponse&#34;&gt;page.waitForResponse&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Wait for an HTTP response that matches the specified URL pattern.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitforrequest&#34;&gt;page.waitForRequest&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Wait for an HTTP request that matches the specified URL pattern.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitfortimeout&#34;&gt;page.waitForTimeout&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Waits the given time. &lt;em&gt;Use this instead of &lt;code&gt;sleep&lt;/code&gt; in your frontend tests&lt;/em&gt;.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#pagewaitforurl&#34;&gt;page.waitForURL&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Wait for the page to navigate to the specified URL.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;a href=&#34;#locatorwaitfor&#34;&gt;locator.waitFor&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;Wait for the element to be in a particular state.&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;h3 id=&#34;browsercontextwaitforevent&#34;&gt;browserContext.waitForEvent&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/browsercontext/waitforevent/&#34;&gt;browserContext.waitForEvent&lt;/a&gt; is used when waiting for specific events to fire, which then returns the element associated to that event. At the moment the only event that is available to use is &lt;code&gt;page&lt;/code&gt;. It can be useful when you want to track and retrieve a new tab opening. When working with a predicate function, it can be used to wait for a specific page to open before returning &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;!-- eslint-skip--&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    ui: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  await page.goto(&amp;#39;https://test.k6.io/&amp;#39;);

  await page.keyboard.down(&amp;#39;ControlOrMeta&amp;#39;);

  // Open the link in a new tab with the help of the meta key.
  // Wait for the new page to be created.
  const browserContext = browser.context();
  const [newTab] = await Promise.all([
    browserContext.waitForEvent(&amp;#39;page&amp;#39;),
    page.locator(&amp;#39;a[href=&amp;#34;/my_messages.php&amp;#34;]&amp;#39;).click(),
  ]);

  await page.keyboard.up(&amp;#39;ControlOrMeta&amp;#39;);

  // Wait for the new page (tab) to load.
  await newTab.waitForLoadState(&amp;#39;load&amp;#39;);

  // Take screenshots of each page.
  await page.screenshot({ path: `screenshot-page.png` });
  await newTab.screenshot({ path: `screenshot-newTab.png` });

  await newTab.close();
  await page.close();
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitforfunction&#34;&gt;page.waitForFunction&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitforfunction/&#34;&gt;page.waitForFunction&lt;/a&gt; is useful when you want more control over when a test progresses with a javascript function that returns true when a condition (or many conditions) is met. It can be used to poll for changes in the DOM or non-DOM elements and variables.&lt;/p&gt;
&lt;!-- eslint-skip--&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { check } from &amp;#39;https://jslib.k6.io/k6-utils/1.5.0/index.js&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    // Setting up the example that will mutate the h1 element by setting the
    // h1 elements text value to &amp;#34;Hello&amp;#34;.
    await page.evaluate(() =&amp;gt; {
      setTimeout(() =&amp;gt; {
        const el = document.createElement(&amp;#39;h1&amp;#39;);
        el.innerHTML = &amp;#39;Hello&amp;#39;;
        document.body.appendChild(el);
      }, 1000);
    });

    // Waiting until the h1 element has mutated.
    const ok = await page.waitForFunction(&amp;#34;document.querySelector(&amp;#39;h1&amp;#39;)&amp;#34;, {
      polling: &amp;#39;mutation&amp;#39;,
      timeout: 2000,
    });

    await check(ok, {
      &amp;#39;waitForFunction successfully resolved&amp;#39;: async (ok) =&amp;gt; (await ok.innerHTML()) == &amp;#39;Hello&amp;#39;,
    });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitforloadstate&#34;&gt;page.waitForLoadState&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitforloadstate/&#34;&gt;page.waitForLoadState&lt;/a&gt; is useful when there’s no explicit navigation, but you need to wait for the page or network to settle. This is mainly used when working with single-page applications or when no full page reloads happen.&lt;/p&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    // Goto a SPA
    await page.goto(&amp;#39;&amp;lt;url&amp;gt;&amp;#39;);

    // ... perform some actions that reload part of the page.

    // waits for the default `load` event.
    await page.waitForLoadState();
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitfornavigation&#34;&gt;page.waitForNavigation&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitfornavigation/&#34;&gt;page.waitForNavigation&lt;/a&gt; is a very useful API when performing other actions that could start a page navigation, and they don&amp;rsquo;t automatically wait for the navigation to end. Usually, you&amp;rsquo;ll find it in our examples with a &lt;code&gt;click&lt;/code&gt; API call. Note that 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/goto/&#34;&gt;goto&lt;/a&gt; is an example of an API that &lt;em&gt;doesn&amp;rsquo;t&lt;/em&gt; require &lt;code&gt;waitForNavigation&lt;/code&gt; since it will automatically wait for the navigation to complete before returning.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important to call this in a &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Promise.all&lt;/a&gt; along with the API that will cause the navigation to start.&lt;/p&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    await page.goto(&amp;#39;https://test.k6.io/my_messages.php&amp;#39;);

    await page.locator(&amp;#39;input[name=&amp;#34;login&amp;#34;]&amp;#39;).type(&amp;#39;admin&amp;#39;);
    await page.locator(&amp;#39;input[name=&amp;#34;password&amp;#34;]&amp;#39;).type(&amp;#39;123&amp;#39;);

    const submitButton = page.locator(&amp;#39;input[type=&amp;#34;submit&amp;#34;]&amp;#39;);

    // The click action will start a navigation, and the waitForNavigation
    // will help the test wait until the navigation completes.
    await Promise.all([page.waitForNavigation(), submitButton.click()]);
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitforresponse&#34;&gt;page.waitForResponse&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitForResponse/&#34;&gt;page.waitForResponse&lt;/a&gt; waits for an HTTP response that matches the specified URL pattern. This method is particularly useful for waiting for responses from AJAX/fetch requests, API calls, or specific resources to be loaded before proceeding with the test.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;js&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-js&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { check } from &amp;#39;k6&amp;#39;;

export const options = {
  scenarios: {
    ui: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    await page.goto(&amp;#39;https://quickpizza.grafana.com/&amp;#39;);

    // Test waitForResponse with user interaction
    const pizzaResponsePromise = page.waitForResponse(&amp;#39;https://quickpizza.grafana.com/api/pizza&amp;#39;);

    await page.getByRole(&amp;#39;button&amp;#39;, { name: /pizza/i }).click();

    const pizzaResponse = await pizzaResponsePromise;

    // Check that the pizza API call was successful
    check(pizzaResponse, {
      &amp;#39;pizza API status is 200&amp;#39;: (r) =&amp;gt; r.status() === 200,
      &amp;#39;pizza API URL is correct&amp;#39;: (r) =&amp;gt; r.url() === &amp;#39;https://quickpizza.grafana.com/api/pizza&amp;#39;,
    });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitforrequest&#34;&gt;page.waitForRequest&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitforrequest/&#34;&gt;page.waitForRequest&lt;/a&gt; waits for an HTTP request that matches the specified URL pattern. This method is particularly useful for waiting for requests to be initiated before proceeding with the test, such as verifying that form submissions or API calls are triggered.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;js&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-js&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;
import { check } from &amp;#39;k6&amp;#39;;

export const options = {
  scenarios: {
    ui: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    await page.goto(&amp;#39;https://quickpizza.grafana.com/&amp;#39;);

    // Test waitForRequest with user interaction
    const pizzaRequestPromise = page.waitForRequest(&amp;#39;https://quickpizza.grafana.com/api/pizza&amp;#39;);

    await page.getByRole(&amp;#39;button&amp;#39;, { name: /pizza/i }).click();

    const pizzaRequest = await pizzaRequestPromise;

    // Check that the pizza API request was initiated
    check(pizzaRequest, {
      &amp;#39;pizza API URL is correct&amp;#39;: (r) =&amp;gt; r.url() === &amp;#39;https://quickpizza.grafana.com/api/pizza&amp;#39;,
      &amp;#39;pizza API method is POST&amp;#39;: (r) =&amp;gt; r.method() === &amp;#39;POST&amp;#39;,
    });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitfortimeout&#34;&gt;page.waitForTimeout&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitfortimeout/&#34;&gt;page.waitForTimeout&lt;/a&gt; will wait the given amount of time. It&amp;rsquo;s functionally the same as k6&amp;rsquo;s &lt;a href=&#34;#whatissleep&#34;&gt;sleep&lt;/a&gt;, but it&amp;rsquo;s asynchronous, which means it will not block the event loop and allows the background tasks to continue to be worked on.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;js&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-js&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    await page.goto(&amp;#39;https://quickpizza.grafana.com/&amp;#39;);

    // Slow the test down to mimic a user looking for the element on the page.
    await page.waitForTimeout(1000);

    // ... perform the next action
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;pagewaitforurl&#34;&gt;page.waitForURL&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitforurl/&#34;&gt;page.waitForURL&lt;/a&gt; waits for the page to navigate to the specified URL. This method is useful for ensuring that navigation to a particular URL has completed before proceeding with the test. This is especially useful if there are multiple redirects before hitting the end destination.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;js&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-js&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  try {
    await page.goto(&amp;#39;https://quickpizza.grafana.com/test.k6.io/&amp;#39;);

    // Wait for navigation to a specific URL
    await Promise.all([
      page.click(&amp;#39;a[href=&amp;#34;/my_messages.php&amp;#34;]&amp;#39;),
      page.waitForURL(&amp;#39;https://quickpizza.grafana.com/my_messages.php&amp;#39;),
    ]);

    await page.goto(&amp;#39;https://quickpizza.grafana.com/test.k6.io/&amp;#39;);

    // Wait for navigation using URL pattern with RegExp
    await Promise.all([page.click(&amp;#39;a[href=&amp;#34;/browser.php&amp;#34;]&amp;#39;), page.waitForURL(/\/browser\.php$/)]);
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;locatorwaitfor&#34;&gt;locator.waitFor&lt;/h3&gt;
&lt;p&gt;
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/locator/waitfor/&#34;&gt;locator.waitFor&lt;/a&gt; will wait until the element meets the waiting criteria. It&amp;rsquo;s useful when dealing with dynamic websites where elements may take time to appear or change state. For example, if elements load after some delay due to async calls, or because of slow JavaScript execution.&lt;/p&gt;
&lt;!-- md-k6:skip --&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;js&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-js&#34;&gt;import { browser } from &amp;#39;k6/browser&amp;#39;;

export const options = {
  scenarios: {
    browser: {
      executor: &amp;#39;shared-iterations&amp;#39;,
      options: {
        browser: {
          type: &amp;#39;chromium&amp;#39;,
        },
      },
    },
  },
};

export default async function () {
  const page = await browser.newPage();

  await page.goto(&amp;#39;https://test.k6.io/browser.php&amp;#39;);
  const text = page.locator(&amp;#39;#input-text-hidden&amp;#39;);
  await text.waitFor({
    state: &amp;#39;hidden&amp;#39;,
  });
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="simulate-user-input-delay">Simulate user input delay&lt;/h1>
&lt;p>On this page, you&amp;rsquo;ll learn how to best work with &lt;code>sleep&lt;/code> in &lt;code>k6&lt;/code> and the various &lt;code>wait*&lt;/code> prepended methods available in &lt;code>k6/browser&lt;/code> to simulate user input delay, wait for navigations, and wait for element state changes. By the end of this page, you should be able to successfully use the correct API where necessary.&lt;/p></description></item><item><title>Use Page.close to clean up test resources</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/clean-up-test-resources-page-close/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/clean-up-test-resources-page-close/</guid><content><![CDATA[&lt;h1 id=&#34;use-pageclose-to-clean-up-test-resources&#34;&gt;Use Page.close to clean up test resources&lt;/h1&gt;
&lt;p&gt;When running k6 browser tests with event-based APIs, such as &lt;code&gt;page.on&lt;/code&gt;, tests may not always properly flush metrics or clean up resources automatically, which can lead to incomplete metric reporting or leftover event handlers.&lt;/p&gt;
&lt;p&gt;To prevent this, always call 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/close/&#34;&gt;&lt;code&gt;page.close()&lt;/code&gt;&lt;/a&gt; at the end of your browser tests. Doing so ensures accurate and complete metric collection, cleans up event listeners to prevent resource leaks, and simplifies test teardown for improved reliability.&lt;/p&gt;
&lt;h2 id=&#34;example&#34;&gt;Example&lt;/h2&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;JavaScript&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;import { browser } from &amp;#34;k6/browser&amp;#34;;
import { check } from &amp;#34;k6&amp;#34;;

const BASE_URL = __ENV.BASE_URL || &amp;#34;https://quickpizza.grafana.com/&amp;#34;;

export const options = {
  scenarios: {
    ui: {
      executor: &amp;#34;shared-iterations&amp;#34;,
      options: {
        browser: {
          type: &amp;#34;chromium&amp;#34;,
        },
      },
    },
  },
};

export default async function () {
  let checkData;
  const page = await browser.newPage();
  try {

    await page.goto(BASE_URL);
    checkData = await page.locator(&amp;#34;h1&amp;#34;).textContent();
    check(page, {
      header: checkData == &amp;#34;Looking to break out of your pizza routine?&amp;#34;,
    });

    await page.locator(&amp;#39;//button[. = &amp;#34;Pizza, Please!&amp;#34;]&amp;#39;).click();
    await page.waitForTimeout(500);
    await page.screenshot({ path: &amp;#34;screenshot.png&amp;#34; });
    checkData = await page.locator(&amp;#34;div#recommendations&amp;#34;).textContent();
    check(page, {
      recommendation: checkData != &amp;#34;&amp;#34;,
    });
  } finally {
    //Always close the page at the end
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="use-pageclose-to-clean-up-test-resources">Use Page.close to clean up test resources&lt;/h1>
&lt;p>When running k6 browser tests with event-based APIs, such as &lt;code>page.on&lt;/code>, tests may not always properly flush metrics or clean up resources automatically, which can lead to incomplete metric reporting or leftover event handlers.&lt;/p></description></item><item><title>When to use sleep() and page.waitForTimeout()</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/sleep-vs-page-wait-for-timeout/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/recommended-practices/sleep-vs-page-wait-for-timeout/</guid><content><![CDATA[&lt;h1 id=&#34;when-to-use-sleep-and-pagewaitfortimeout&#34;&gt;When to use sleep() and page.waitForTimeout()&lt;/h1&gt;
&lt;p&gt;In Grafana k6, both 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6/sleep/&#34;&gt;&lt;code&gt;sleep()&lt;/code&gt;&lt;/a&gt; and 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitfortimeout/&#34;&gt;&lt;code&gt;page.waitForTimeout()&lt;/code&gt;&lt;/a&gt; can be used to add pauses in your test scripts, but they work very differently. Understanding these differences is essential for accurate and reliable browser testing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sleep()&lt;/code&gt; is &lt;em&gt;synchronous&lt;/em&gt;, blocking the thread and halting all background activity.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;page.waitForTimeout()&lt;/code&gt; is &lt;em&gt;asynchronous&lt;/em&gt;, allowing background events to continue while pausing the script.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most browser-based scenarios, you should use &lt;code&gt;page.waitForTimeout()&lt;/code&gt; to simulate user delays without interrupting normal browser behavior or affecting performance measurements.&lt;/p&gt;
&lt;h2 id=&#34;when-to-use-sleep&#34;&gt;When to use &lt;code&gt;sleep()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sleep()&lt;/code&gt; is a built-in k6 function designed to suspend Virtual User (VU) execution for a specified duration. It&amp;rsquo;s a synchronous function that blocks the JavaScript event loop, which means that all asynchronous work is also suspended until sleep completes.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s best used to simulate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigating to a page&lt;/li&gt;
&lt;li&gt;Sleeping for one second to simulate a user looking for a specific element on the page&lt;/li&gt;
&lt;li&gt;Clicking on the element&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;when-to-use-pagewaitfortimeout&#34;&gt;When to use &lt;code&gt;page.waitForTimeout()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;page.waitForTimeout()&lt;/code&gt; is a function provided by the k6 browser module that pauses script execution for a specified amount of time &lt;em&gt;without blocking&lt;/em&gt; the JavaScript event loop.&lt;/p&gt;
&lt;p&gt;Unlike &lt;code&gt;sleep()&lt;/code&gt;, it&amp;rsquo;s &lt;em&gt;asynchronous&lt;/em&gt;, meaning other browser activities, such as rendering, network requests, and event handling, can continue while the delay is in effect. This makes it ideal for browser based testing, where multiple asynchronous operations happen in the background.&lt;/p&gt;
&lt;p&gt;You can use &lt;code&gt;page.waitForTimeout()&lt;/code&gt; to simulate real user pauses, like reading a page, waiting for a visual change, or mimicking natural interaction timing, all without interrupting the browser&amp;rsquo;s internal processes.&lt;/p&gt;
]]></content><description>&lt;h1 id="when-to-use-sleep-and-pagewaitfortimeout">When to use sleep() and page.waitForTimeout()&lt;/h1>
&lt;p>In Grafana k6, both
&lt;a href="/docs/k6/v2.3.x/javascript-api/k6/sleep/">&lt;code>sleep()&lt;/code>&lt;/a> and
&lt;a href="/docs/k6/v2.3.x/javascript-api/k6-browser/page/waitfortimeout/">&lt;code>page.waitForTimeout()&lt;/code>&lt;/a> can be used to add pauses in your test scripts, but they work very differently. Understanding these differences is essential for accurate and reliable browser testing.&lt;/p></description></item></channel></rss>