> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/grafana/k6/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser Testing

> Test web applications with Chromium-based browser automation in k6

k6 browser enables end-to-end testing of web applications using real browser automation. Built on the Chrome DevTools Protocol, it provides a Playwright-compatible API for interacting with web pages, perfect for testing user journeys and frontend performance.

## Getting Started

Browser tests use the `k6/browser` module and require async functions:

```javascript theme={null}
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',
        },
      },
    },
  },
};

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

  try {
    await page.goto('https://quickpizza.grafana.com/test.k6.io/', {
      waitUntil: 'networkidle'
    });
    
    console.log('Page title:', page.title());
  } finally {
    await page.close();
  }
}
```

<Note>
  Browser tests require the `browser` scenario option and must use async/await syntax. The browser module is currently available for Chromium-based browsers.
</Note>

## Browser Configuration

Configure browser behavior in scenario options:

```javascript theme={null}
export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
  thresholds: {
    checks: ['rate==1.0'],
    'browser_web_vital_fcp': ['p(95)<1000'],
    'browser_web_vital_lcp': ['p(95)<2500'],
  },
};
```

### Browser Options

| Option     | Type    | Description                              |
| ---------- | ------- | ---------------------------------------- |
| `type`     | String  | Browser type (currently only 'chromium') |
| `headless` | Boolean | Run in headless mode (default: true)     |
| `timeout`  | String  | Default timeout for operations           |

## Page Navigation

Navigate to pages and wait for different load states:

<Tabs>
  <Tab title="Basic Navigation">
    ```javascript theme={null}
    const page = await context.newPage();

    // Navigate and wait for load
    await page.goto('https://example.com');

    // Navigate with options
    await page.goto('https://example.com', {
      waitUntil: 'networkidle',  // Wait until network is idle
      timeout: '30s',
    });
    ```
  </Tab>

  <Tab title="Wait Conditions">
    ```javascript theme={null}
    // Wait for navigation to complete
    await Promise.all([
      page.waitForNavigation(),
      page.click('a.link'),
    ]);

    // Wait for network idle
    await page.goto('https://example.com', {
      waitUntil: 'networkidle'
    });

    // Wait for DOM content loaded
    await page.goto('https://example.com', {
      waitUntil: 'domcontentloaded'
    });
    ```
  </Tab>

  <Tab title="Concurrent Navigation">
    ```javascript theme={null}
    // Set up waiter before triggering navigation
    await Promise.all([
      page.waitForNavigation(),
      page.click('button[type="submit"]'),
    ]);
    ```
  </Tab>
</Tabs>

## Element Interaction

### Locators

Locators provide a robust way to find and interact with elements:

```javascript theme={null}
import { browser } from 'k6/browser';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
};

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

  try {
    await page.goto("https://quickpizza.grafana.com/flip_coin.php", {
      waitUntil: "networkidle",
    });

    // Create locators for elements
    const heads = page.getByRole("button", { name: "Bet on heads!" });
    const tails = page.getByRole("button", { name: "Bet on tails!" });
    const currentBet = page.getByText(/Your bet\: .*/);

    // Use locators across navigations
    await Promise.all([
      page.waitForNavigation(),
      tails.click(),
    ]);
    console.log(await currentBet.innerText());

    await Promise.all([
      page.waitForNavigation(),
      heads.click(),
    ]);
    console.log(await currentBet.innerText());
  } finally {
    await page.close();
  }
}
```

### Locator Methods

<CodeGroup>
  ```javascript By Role theme={null}
  // Recommended: Use ARIA roles
  const button = page.getByRole('button', { name: 'Submit' });
  const link = page.getByRole('link', { name: 'Login' });
  const heading = page.getByRole('heading', { name: 'Welcome' });
  ```

  ```javascript By Text theme={null}
  // Find by visible text
  const element = page.getByText('Click me');
  const regex = page.getByText(/Welcome.*/);
  ```

  ```javascript By Label theme={null}
  // Find form inputs by label
  const input = page.getByLabel('Username');
  ```

  ```javascript By Selector theme={null}
  // CSS selectors
  const element = page.locator('.classname');
  const byId = page.locator('#element-id');
  const byAttribute = page.locator('[data-test="submit"]');
  ```
</CodeGroup>

## Form Interaction

Fill forms and submit data:

```javascript theme={null}
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' },
      },
    },
  },
};

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

  try {
    await page.goto('https://quickpizza.grafana.com/test.k6.io/', {
      waitUntil: 'networkidle'
    });

    // Navigate to login page
    await Promise.all([
      page.waitForNavigation(),
      page.getByRole('link', {name: '/my_messages.php'}).click(),
    ]);

    // Fill form fields
    await page.locator('input[name="login"]').type('admin');
    await page.locator('input[name="password"]').type("123");

    // Submit form
    await Promise.all([
      page.waitForNavigation(),
      page.getByText('Go!').click(),
    ]);

    // Verify login success
    await check(page.locator('h2'), {
      'header': async lo => {
        return await lo.textContent() == 'Welcome, admin!'
      }
    });

    // Check cookies
    await check(context, {
      'session cookie is set': async ctx => {
        const cookies = await ctx.cookies();
        return cookies.find(c => c.name == 'AWSALB') !== undefined;
      }
    });
  } finally {
    await page.close();
  }
}
```

## Screenshots and Debugging

Capture screenshots for visual verification:

```javascript theme={null}
import { browser } from 'k6/browser';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
};

export default async function() {
  const context = await browser.newContext();
  const page = await context.newPage();
  
  try {
    await page.goto('https://quickpizza.grafana.com/test.k6.io/');
    
    // Take full page screenshot
    await page.screenshot({ path: 'screenshot.png' });
    
    // Screenshot specific element
    const element = page.locator('#main-content');
    await element.screenshot({ path: 'element.png' });
  } finally {
    await page.close();
  }
}
```

## Waiting Strategies

Different ways to wait for elements and conditions:

<CodeGroup>
  ```javascript Wait for Selector theme={null}
  // Wait for element to appear
  await page.waitForSelector('.dynamic-content');

  // Wait with timeout
  await page.waitForSelector('.slow-loader', { timeout: '10s' });
  ```

  ```javascript Wait for Function theme={null}
  // Wait for custom condition
  await page.waitForFunction(() => {
    return document.querySelector('.data').textContent.length > 0;
  });

  // Wait with polling
  await page.waitForFunction(
    () => window.loadComplete === true,
    { polling: '100ms', timeout: '30s' }
  );
  ```

  ```javascript Wait for Event theme={null}
  // Wait for specific event
  await page.waitForEvent('load');
  await page.waitForEvent('requestfinished');
  ```
</CodeGroup>

## Advanced Interactions

### Mouse and Keyboard

```javascript theme={null}
// Mouse actions
await page.mouse.click(100, 200);
await page.mouse.move(150, 250);

// Keyboard input
await page.keyboard.type('Hello World');
await page.keyboard.press('Enter');
await page.keyboard.down('Shift');
await page.keyboard.up('Shift');
```

### Cookies

```javascript theme={null}
// Get cookies
const cookies = await context.cookies();
console.log('Cookies:', cookies);

// Set cookies
await context.addCookies([
  {
    name: 'session',
    value: 'abc123',
    domain: 'example.com',
    path: '/',
  },
]);

// Clear cookies
await context.clearCookies();
```

### Local Storage and Session Storage

```javascript theme={null}
// Evaluate JavaScript to access storage
const value = await page.evaluate(() => {
  return localStorage.getItem('key');
});

await page.evaluate(() => {
  localStorage.setItem('key', 'value');
  sessionStorage.setItem('session-key', 'session-value');
});
```

## Network Control

### Request Interception

```javascript theme={null}
// Intercept and modify requests
await page.route('**/*.{png,jpg,jpeg}', route => {
  route.abort();
});

// Continue with modifications
await page.route('**/api/**', route => {
  route.continue({
    headers: {
      ...route.request().headers(),
      'X-Custom-Header': 'value',
    },
  });
});
```

### Network Throttling

```javascript theme={null}
// Emulate slow network
await context.throttleNetwork({
  download: 1000,  // 1 Mbps
  upload: 500,     // 0.5 Mbps
  latency: 100,    // 100ms
});
```

## Device Emulation

```javascript theme={null}
import { browser } from 'k6/browser';

export default async function() {
  const context = await browser.newContext({
    viewport: { width: 375, height: 667 },
    userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
    deviceScaleFactor: 2,
    isMobile: true,
    hasTouch: true,
  });

  const page = await context.newPage();
  
  try {
    await page.goto('https://example.com');
    // Mobile test logic
  } finally {
    await page.close();
  }
}
```

## Multiple Pages and Contexts

```javascript theme={null}
import { browser } from 'k6/browser';

export default async function() {
  // Create isolated contexts (separate sessions)
  const context1 = await browser.newContext();
  const context2 = await browser.newContext();

  // Open multiple pages in same context
  const page1 = await context1.newPage();
  const page2 = await context1.newPage();

  try {
    // Pages in same context share cookies/storage
    await page1.goto('https://example.com/login');
    await page2.goto('https://example.com/dashboard');

    // Different contexts are isolated
    await context2.newPage();
  } finally {
    await page1.close();
    await page2.close();
  }
}
```

## Web Vitals

k6 browser automatically collects Web Vitals metrics:

```javascript theme={null}
export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
  thresholds: {
    'browser_web_vital_fcp': ['p(95)<1000'],      // First Contentful Paint
    'browser_web_vital_lcp': ['p(95)<2500'],      // Largest Contentful Paint
    'browser_web_vital_fid': ['p(95)<100'],       // First Input Delay
    'browser_web_vital_cls': ['p(95)<0.1'],       // Cumulative Layout Shift
    'browser_web_vital_ttfb': ['p(95)<800'],      // Time to First Byte
    'browser_web_vital_inp': ['p(95)<200'],       // Interaction to Next Paint
  },
};
```

## Page Object Model

Organize tests using the Page Object pattern:

```javascript theme={null}
import { browser } from 'k6/browser';

class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.locator('input[name="username"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.submitButton = page.getByRole('button', { name: 'Login' });
  }

  async login(username, password) {
    await this.usernameInput.type(username);
    await this.passwordInput.type(password);
    await Promise.all([
      this.page.waitForNavigation(),
      this.submitButton.click(),
    ]);
  }
}

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

  try {
    await page.goto('https://example.com/login');
    
    const loginPage = new LoginPage(page);
    await loginPage.login('user', 'password');
  } finally {
    await page.close();
  }
}
```

<Warning>
  Browser tests consume more resources than protocol-level tests. Use them strategically for critical user journeys and complement with HTTP tests for higher load levels.
</Warning>

## Best Practices

1. **Always close pages and contexts** to free up resources
2. **Use locators over selectors** for resilience across page changes
3. **Wait for navigation** when actions trigger page loads
4. **Set up waiters before actions** to avoid race conditions
5. **Use Page Object Model** for maintainable test code
6. **Combine with HTTP tests** for comprehensive coverage
7. **Monitor Web Vitals** to ensure performance standards
8. **Use headless mode** in CI/CD environments

## Metrics

k6 browser collects comprehensive metrics:

* `browser_web_vital_*` - Core Web Vitals (FCP, LCP, CLS, etc.)
* `browser_http_req_duration` - HTTP request durations
* `browser_http_req_failed` - Failed HTTP requests
* `browser_data_sent` / `browser_data_received` - Network traffic

## Related Resources

* [HTTP Protocol Testing](/protocols/http)
* [Metrics Reference](/concepts/metrics)
* [Playwright Documentation](https://playwright.dev/)
