<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>How to write browser tests on Grafana Labs</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/how-to-write-browser-tests/</link><description>Recent content in How to write browser tests on Grafana Labs</description><generator>Hugo -- gohugo.io</generator><language>en</language><atom:link href="/docs/k6/v2.3.x/using-k6-browser/how-to-write-browser-tests/index.xml" rel="self" type="application/rss+xml"/><item><title>Asynchronous operations</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/how-to-write-browser-tests/asynchronous-operations/</guid><content><![CDATA[&lt;h1 id=&#34;asynchronous-operations&#34;&gt;Asynchronous operations&lt;/h1&gt;
&lt;p&gt;Most methods in the browser module return &lt;a href=&#34;#why-the-browser-module-uses-asynchronous-apis&#34;&gt;JavaScript promises&lt;/a&gt;, and k6 scripts must be written to handle this properly. This usually means using the &lt;code&gt;await&lt;/code&gt; keyword to wait for the async operation to complete.&lt;/p&gt;
&lt;p&gt;For example:&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;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;const page = await browser.newPage();

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

const locator = page.locator(&amp;#39;button[name=&amp;#34;pizza-please&amp;#34;]&amp;#39;);

await locator.click();&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In addition to using &lt;code&gt;await&lt;/code&gt;, another important part of writing k6 browser tests is handling page navigations. There are two recommended methods for doing that: using &lt;code&gt;Promise.all&lt;/code&gt; or using the &lt;code&gt;waitFor&lt;/code&gt; method.&lt;/p&gt;
&lt;h2 id=&#34;promiseall&#34;&gt;Promise.all&lt;/h2&gt;
&lt;p&gt;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.&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;;

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://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;);

    await Promise.all([page.waitForNavigation(), submitButton.click()]);

    await check(page.locator(&amp;#39;h2&amp;#39;), {
      header: async (lo) =&amp;gt; (await lo.textContent()) == &amp;#39;Welcome, admin!&amp;#39;,
    });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The preceding code uses &lt;code&gt;Promise.all([])&lt;/code&gt; to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, &lt;code&gt;page.waitForNavigation()&lt;/code&gt; is needed because the page won&amp;rsquo;t be ready until the navigation completes. This is required because there can be a race condition if these two actions don&amp;rsquo;t happen simultaneously.&lt;/p&gt;
&lt;p&gt;Then, you can use 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6/check/&#34;&gt;&lt;code&gt;check&lt;/code&gt;&lt;/a&gt; from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser.&lt;/p&gt;
&lt;h2 id=&#34;wait-for-specific-elements&#34;&gt;Wait for specific elements&lt;/h2&gt;
&lt;p&gt;We also encourage the use of &lt;code&gt;locator.waitFor&lt;/code&gt; 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:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to the search site&lt;/li&gt;
&lt;li&gt;Wait for the search bar and submit button to appear&lt;/li&gt;
&lt;li&gt;Fill in the search bar with the query&lt;/li&gt;
&lt;li&gt;Click the submit button&lt;/li&gt;
&lt;li&gt;Wait for the search results&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We should be able to do these actions like so:&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;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;await page.goto(&amp;#39;https://my-search-engine.com&amp;#39;);

const searchBar = page.locator(&amp;#39;.search-bar&amp;#39;);
const submitButton = page.locator(&amp;#39;.submit-button&amp;#39;);

await searchBar.waitFor();
await submitButton.waitFor();

await searchBar.fill(&amp;#39;k6&amp;#39;);
await submitButton.click();

const searchResults = page.locator(&amp;#39;.search-results-table&amp;#39;);

await searchResults.waitFor();&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This avoids the use of &lt;code&gt;Promise.all&lt;/code&gt; which can be confusing to work with, and instead makes the script easier to follow.&lt;/p&gt;
&lt;h2 id=&#34;why-the-browser-module-uses-asynchronous-apis&#34;&gt;Why the browser module uses asynchronous APIs&lt;/h2&gt;
&lt;p&gt;The browser module uses asynchronous APIs that require &lt;code&gt;await&lt;/code&gt; for several reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Consistency with &lt;a href=&#34;https://playwright.dev/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Playwright&lt;/a&gt; and other frontend testing frameworks.&lt;/li&gt;
&lt;li&gt;Alignment with how developers expect to work with modern JavaScript APIs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example:&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;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;const page = await browser.newPage();

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

const locator = page.locator(&amp;#39;button[name=&amp;#34;pizza-please&amp;#34;]&amp;#39;);

await locator.click();&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;API calls that interact with Chromium are asynchronous and require &lt;code&gt;await&lt;/code&gt; to ensure completion before proceeding. Synchronous APIs, such as &lt;code&gt;page.locator&lt;/code&gt;, do not require &lt;code&gt;await&lt;/code&gt;, but using it does not cause issues since the JavaScript runtime will simply return the value immediately.&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t add &lt;code&gt;await&lt;/code&gt; on asynchronous APIs, it can cause the script to finish before the test completes, resulting in errors like &lt;code&gt;&amp;quot;Uncaught (in promise) TypeError: Object has no member &#39;goto&#39;&amp;quot;&lt;/code&gt;. That can happen because the page object from &lt;code&gt;browser.newPage()&lt;/code&gt; without an &lt;code&gt;await&lt;/code&gt; is actually a JavaScript promise. You can try and see the error using the following code snippet:&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;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;const page = browser.newPage();

page.goto(&amp;#39;https://quickpizza.grafana.com/&amp;#39;); // An error should occur since we&amp;#39;re not using await in the line above.

const locator = page.locator(&amp;#39;button[name=&amp;#34;pizza-please&amp;#34;]&amp;#39;);

locator.click();&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h1 id="asynchronous-operations">Asynchronous operations&lt;/h1>
&lt;p>Most methods in the browser module return &lt;a href="#why-the-browser-module-uses-asynchronous-apis">JavaScript promises&lt;/a>, and k6 scripts must be written to handle this properly. This usually means using the &lt;code>await&lt;/code> keyword to wait for the async operation to complete.&lt;/p></description></item><item><title>Interact with elements on your webpage</title><link>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements/</link><pubDate>Mon, 21 Sep 2026 15:16:28 +0000</pubDate><guid>https://grafana.com/docs/k6/v2.3.x/using-k6-browser/how-to-write-browser-tests/interact-with-elements/</guid><content><![CDATA[&lt;h1 id=&#34;interact-with-elements-on-your-webpage&#34;&gt;Interact with elements on your webpage&lt;/h1&gt;
&lt;p&gt;You can use &lt;code&gt;page.locator()&lt;/code&gt; and pass in the element&amp;rsquo;s selector you want to find on the page. &lt;code&gt;page.locator()&lt;/code&gt; will create and return a 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/locator/&#34;&gt;Locator&lt;/a&gt; object, which you can later use to interact with the element.&lt;/p&gt;
&lt;p&gt;To find out which selectors the browser module supports, check out 
    &lt;a href=&#34;/docs/k6/v2.3.x/using-k6-browser/recommended-practices/selecting-elements/&#34;&gt;Selecting Elements&lt;/a&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;You can also use &lt;code&gt;page.$()&lt;/code&gt; instead of &lt;code&gt;page.locator()&lt;/code&gt;. You can find the differences between &lt;code&gt;page.locator()&lt;/code&gt; and &lt;code&gt;page.$&lt;/code&gt; in the 
    &lt;a href=&#34;/docs/k6/v2.3.x/javascript-api/k6-browser/locator/&#34;&gt;Locator API documentation&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&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;,
        },
      },
    },
  },
  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://test.k6.io/my_messages.php&amp;#39;);

    // Enter login credentials
    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;);

    await page.screenshot({ path: &amp;#39;screenshots/screenshot.png&amp;#39; });
  } finally {
    await page.close();
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments.&lt;/p&gt;
&lt;p&gt;Within the Locator API, various methods such as &lt;code&gt;type()&lt;/code&gt; can be used to interact with the elements. The &lt;code&gt;type()&lt;/code&gt; method types a text to an input field.&lt;/p&gt;
]]></content><description>&lt;h1 id="interact-with-elements-on-your-webpage">Interact with elements on your webpage&lt;/h1>
&lt;p>You can use &lt;code>page.locator()&lt;/code> and pass in the element&amp;rsquo;s selector you want to find on the page. &lt;code>page.locator()&lt;/code> will create and return a
&lt;a href="/docs/k6/v2.3.x/javascript-api/k6-browser/locator/">Locator&lt;/a> object, which you can later use to interact with the element.&lt;/p></description></item></channel></rss>