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

# Playwright for automation

Intuned's runtime is built on top of [Playwright](https://playwright.dev/), the open-source browser automation framework. When you're writing deterministic automation code, you'll mostly be using Playwright directly.

In Intuned, you deploy code-based **Projects** containing **APIs**. Each API is a handler function that receives a browser `page` and `context`, along with any parameters you define:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";

  interface Params {
    // Define your input parameters here
  }

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    // Your automation code here
    return {};
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page, BrowserContext
  from typing import TypedDict, Any

  class Params(TypedDict):
      # Define your input parameters here
      pass

  async def automation(page: Page, params: dict[str, Any] | None = None, **_kwargs):
      # Your automation code here
      return {}
  ```
</CodeGroup>

**Page** is your browser tab—where you navigate, find elements, interact, and extract data. **BrowserContext** is an isolated browser session with its own cookies, storage, and cache (like an incognito window). Intuned handles browser initialization and context creation; you just use the `page` and `context` you receive.

This guide covers Playwright concepts and patterns you'll use when building automations. If you want to jump straight into code, check out the [Playwright basics project](https://github.com/intuned/cookbook/tree/main/typescript-examples/playwright-basics) in the Intuned TypeScript cookbook or the [Playwright basics project](https://github.com/intuned/cookbook/tree/main/python-examples/playwright-basics) in the Python cookbook.

## Basics

### Navigation

Navigate to a page with `goto()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.goto("https://books.toscrape.com/");
  ```

  ```python Python theme={null}
  await page.goto("https://books.toscrape.com/")
  ```
</CodeGroup>

#### Wait for page load

After navigation, you may need to wait for the page to fully load. Playwright provides `waitForLoadState()`:

| State              | What it waits for                                       |
| ------------------ | ------------------------------------------------------- |
| `load`             | Full page load including images and subframes (default) |
| `domcontentloaded` | DOM is ready, but resources may still be loading        |
| `networkidle`      | No network connections for at least 500ms               |

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.goto("https://example.com");

  // Wait for full page load (default behavior)
  await page.waitForLoadState("load");

  // Wait for DOM to be ready (faster, use when you don't need images)
  await page.waitForLoadState("domcontentloaded");

  // Wait for network to be idle (use for SPAs or pages with async data)
  await page.waitForLoadState("networkidle");
  ```

  ```python Python theme={null}
  await page.goto("https://example.com")

  # Wait for full page load (default behavior)
  await page.wait_for_load_state("load")

  # Wait for DOM to be ready (faster, use when you don't need images)
  await page.wait_for_load_state("domcontentloaded")

  # Wait for network to be idle (use for SPAs or pages with async data)
  await page.wait_for_load_state("networkidle")
  ```
</CodeGroup>

#### Wait for URL

Wait for the page to navigate to a specific URL pattern:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait for exact URL
  await page.waitForURL("https://example.com/dashboard");

  // Wait for URL pattern (glob)
  await page.waitForURL("**/dashboard/**");

  // Wait for URL matching regex
  await page.waitForURL(/\/order\/\d+/);
  ```

  ```python Python theme={null}
  # Wait for exact URL
  await page.wait_for_url("https://example.com/dashboard")

  # Wait for URL pattern (glob)
  await page.wait_for_url("**/dashboard/**")

  # Wait for URL matching regex
  import re
  await page.wait_for_url(re.compile(r"/order/\d+"))
  ```
</CodeGroup>

#### Create new pages

Intuned initializes one page by default—the `page` you receive in your handler. You can create additional pages to run operations in parallel. Both pages run in the same browser on the same machine:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const newPage = await context.newPage();
  await newPage.goto("https://example.com");
  const pageTitle = await newPage.title();
  ```

  ```python Python theme={null}
  new_page = await page.context.new_page()
  await new_page.goto("https://example.com")
  page_title = await new_page.title()
  ```
</CodeGroup>

#### Handle new tabs and popups

When a click opens a new tab (like `target="_blank"` links), capture it with `waitForEvent`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const [newPage] = await Promise.all([
    context.waitForEvent("page"),
    page.locator(".product_pod h3 a").first().click(),
  ]);

  await newPage.waitForLoadState("domcontentloaded");
  const bookTitle = await newPage.locator(".product_main h1").innerText();
  ```

  ```python Python theme={null}
  async with page.context.expect_page() as new_page_info:
      await page.locator(".product_pod h3 a").first.click()

  new_page = await new_page_info.value
  await new_page.wait_for_load_state("domcontentloaded")
  book_title = await new_page.locator(".product_main h1").inner_text()
  ```
</CodeGroup>

For popups, use the `popup` event:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const [popup] = await Promise.all([
    page.waitForEvent("popup"),
    page.locator("#open-popup-button").click(),
  ]);

  await popup.waitForLoadState("domcontentloaded");
  ```

  ```python Python theme={null}
  async with page.expect_popup() as popup_info:
      await page.locator("#open-popup-button").click()

  popup = await popup_info.value
  await popup.wait_for_load_state("domcontentloaded")
  ```
</CodeGroup>

### Finding elements

Locators are the primary way to find elements in Playwright. They're lazy—not evaluated until you interact with them:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const button = page.locator("button#submit"); // Just a locator, no query yet
  await button.click(); // Now it's evaluated
  ```

  ```python Python theme={null}
  button = page.locator("button#submit")  # Just a locator, no query yet
  await button.click()  # Now it's evaluated
  ```
</CodeGroup>

#### CSS vs XPath selectors

| Feature     | CSS selector                     | XPath selector          |
| ----------- | -------------------------------- | ----------------------- |
| Syntax      | `div.class #id`                  | `//div[@class='class']` |
| Best for    | Simple queries, styling patterns | Complex DOM traversals  |
| Performance | Slightly faster                  | Slightly slower         |

<CodeGroup>
  ```typescript TypeScript theme={null}
  const cssLocator = page.locator("div.container > button");
  const xpathLocator = page.locator("//div[@class='container']/button");
  ```

  ```python Python theme={null}
  css_locator = page.locator("div.container > button")
  xpath_locator = page.locator("//div[@class='container']/button")
  ```
</CodeGroup>

#### CSS selector extensions

When using `page.locator()` with CSS selectors, Playwright adds special pseudo-classes that aren't available in standard CSS:

| Extension           | Description                      | Example                    |
| ------------------- | -------------------------------- | -------------------------- |
| `:has-text("text")` | Contains text anywhere inside    | `div:has-text("Welcome")`  |
| `:text("text")`     | Smallest element containing text | `span:text("Price")`       |
| `:text-is("text")`  | Exact text match                 | `button:text-is("Submit")` |
| `:visible`          | Only visible elements            | `button:visible`           |
| `:has(selector)`    | Contains matching child          | `div:has(> img)`           |

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Find div containing "Welcome" text
  const welcomeDiv = page.locator('div:has-text("Welcome")');

  // Find visible buttons only
  const visibleButtons = page.locator("button:visible");

  // Find cards that contain images
  const imageCards = page.locator("div.card:has(img)");
  ```

  ```python Python theme={null}
  # Find div containing "Welcome" text
  welcome_div = page.locator('div:has-text("Welcome")')

  # Find visible buttons only
  visible_buttons = page.locator("button:visible")

  # Find cards that contain images
  image_cards = page.locator("div.card:has(img)")
  ```
</CodeGroup>

For more details, see the [Playwright locators documentation](https://playwright.dev/docs/other-locators).

#### Semantic locators

Instead of CSS selectors, Playwright provides methods that find elements by their semantic meaning—role, label, placeholder, or test ID. These are more resilient to markup changes and make your code easier to read:

**`getByRole()`** — Find by ARIA role and accessible name:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.getByRole("button", { name: "Submit" }).click();
  await page.getByRole("link", { name: "Learn more" }).click();
  await page.getByRole("checkbox", { name: "Accept terms" }).check();
  ```

  ```python Python theme={null}
  await page.get_by_role("button", name="Submit").click()
  await page.get_by_role("link", name="Learn more").click()
  await page.get_by_role("checkbox", name="Accept terms").check()
  ```
</CodeGroup>

**`getByText()`** — Find by visible text:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const link = page.getByText("Learn more", { exact: true });
  ```

  ```python Python theme={null}
  link = page.get_by_text("Learn more", exact=True)
  ```
</CodeGroup>

**`getByLabel()`** — Find form fields by their label:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const emailInput = page.getByLabel("Email");
  await emailInput.fill("example@example.com");
  ```

  ```python Python theme={null}
  email_input = page.get_by_label("Email")
  await email_input.fill("example@example.com")
  ```
</CodeGroup>

**`getByPlaceholder()`** — Find inputs by placeholder text:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const searchInput = page.getByPlaceholder("Search");
  await searchInput.fill("Playwright");
  ```

  ```python Python theme={null}
  search_input = page.get_by_placeholder("Search")
  await search_input.fill("Playwright")
  ```
</CodeGroup>

**`getByAltText()`** — Find images by alt text:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const logo = page.getByAltText("Company Logo");
  ```

  ```python Python theme={null}
  logo = page.get_by_alt_text("Company Logo")
  ```
</CodeGroup>

**`getByTitle()`** — Find by title attribute:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const helpIcon = page.getByTitle("Help");
  ```

  ```python Python theme={null}
  help_icon = page.get_by_title("Help")
  ```
</CodeGroup>

**`getByTestId()`** — Find by `data-testid` attribute:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const submitBtn = page.getByTestId("submit-button");
  ```

  ```python Python theme={null}
  submit_btn = page.get_by_test_id("submit-button")
  ```
</CodeGroup>

#### Chaining and filtering

When a locator matches multiple elements, narrow it down:

| Method                | What it does                                                      |
| --------------------- | ----------------------------------------------------------------- |
| `.first()` / `.first` | First matching element (method in TypeScript, property in Python) |
| `.last()` / `.last`   | Last matching element (method in TypeScript, property in Python)  |
| `.nth(index)`         | Element at specific position (0-indexed)                          |
| `.filter()`           | Add conditions to narrow results                                  |

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get the first button
  const firstButton = page.getByRole("button").first();

  // Get the 3rd list item (index 2)
  const thirdItem = page.getByRole("listitem").nth(2);

  // Filter buttons by text
  const saveButton = page.getByRole("button").filter({ hasText: "Save" });

  // Filter by containing a specific child element
  const cardWithImage = page.locator(".card").filter({ has: page.locator("img") });

  // Exclude elements
  const nonPromoCards = page.locator(".card").filter({ hasNot: page.locator(".promo-badge") });
  ```

  ```python Python theme={null}
  # Get the first button
  first_button = page.get_by_role("button").first

  # Get the 3rd list item (index 2)
  third_item = page.get_by_role("listitem").nth(2)

  # Filter buttons by text
  save_button = page.get_by_role("button").filter(has_text="Save")

  # Filter by containing a specific child element
  card_with_image = page.locator(".card").filter(has=page.locator("img"))

  # Exclude elements
  non_promo_cards = page.locator(".card").filter(has_not=page.locator(".promo-badge"))
  ```
</CodeGroup>

#### Combining locators

Combine locators with `and()` and `or()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Element must match both conditions
  const enabledSubmit = page.getByRole("button", { name: "Submit" }).and(page.locator(":not([disabled])"));

  // Element can match either condition
  const actionButton = page.getByRole("button", { name: "Save" }).or(page.getByRole("button", { name: "Update" }));
  ```

  ```python Python theme={null}
  # Element must match both conditions
  enabled_submit = page.get_by_role("button", name="Submit").and_(page.locator(":not([disabled])"))

  # Element can match either condition
  action_button = page.get_by_role("button", name="Save").or_(page.get_by_role("button", name="Update"))
  ```
</CodeGroup>

#### Checking element state

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Check visibility
  const isVisible = await page.getByRole("button", { name: "Login" }).isVisible();
  const isHidden = await page.getByText("Loading...").isHidden();

  // Check other states
  const isEnabled = await page.locator("#submit").isEnabled();
  const isChecked = await page.locator("#agree").isChecked();

  // Count matching elements
  const itemCount = await page.getByRole("listitem").count();
  ```

  ```python Python theme={null}
  # Check visibility
  is_visible = await page.get_by_role("button", name="Login").is_visible()
  is_hidden = await page.get_by_text("Loading...").is_hidden()

  # Check other states
  is_enabled = await page.locator("#submit").is_enabled()
  is_checked = await page.locator("#agree").is_checked()

  # Count matching elements
  item_count = await page.get_by_role("listitem").count()
  ```
</CodeGroup>

<Warning>
  **Strict mode**: Playwright requires locators to match exactly one element. If your locator matches zero or multiple elements, you'll get a strict mode violation error. Use `.first()`, `.nth()`, or `.filter()` to be specific.
</Warning>

### Auto-waiting and timeouts

Playwright automatically waits for elements to be ready before performing actions. This auto-waiting happens within a configurable timeout (30 seconds by default). Understanding this helps you write reliable automations without unnecessary delays.

#### What Playwright waits for

When you call an action like `click()` or `fill()`, Playwright automatically waits until the element is:

| Check               | Description                                                    |
| ------------------- | -------------------------------------------------------------- |
| **Attached**        | Element exists in the DOM                                      |
| **Visible**         | Element has non-empty bounding box and no `visibility: hidden` |
| **Stable**          | Element isn't animating (same position for 2 animation frames) |
| **Enabled**         | Element isn't disabled                                         |
| **Receives events** | Element isn't obscured by other elements                       |

This means you rarely need explicit waits before actions:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // No need to wait—Playwright handles it
  await page.getByRole("button", { name: "Submit" }).click();
  ```

  ```python Python theme={null}
  # No need to wait—Playwright handles it
  await page.get_by_role("button", name="Submit").click()
  ```
</CodeGroup>

#### When you need explicit waits

Use explicit waits when:

* Waiting for elements to appear or disappear (loading spinners)
* Waiting for navigation to complete
* Waiting for network requests to finish

**Wait for elements:**

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait until element is visible
  await page.waitForSelector(".product_pod h3 a", { state: "visible", timeout: 5000 });

  // Wait until element is hidden
  await page.waitForSelector(".loading-spinner", { state: "hidden" });

  // Wait until element is removed from DOM
  await page.waitForSelector(".modal", { state: "detached" });
  ```

  ```python Python theme={null}
  # Wait until element is visible
  await page.wait_for_selector(".product_pod h3 a", state="visible", timeout=5000)

  # Wait until element is hidden
  await page.wait_for_selector(".loading-spinner", state="hidden")

  # Wait until element is removed from DOM
  await page.wait_for_selector(".modal", state="detached")
  ```
</CodeGroup>

**Wait for network requests:**

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait for a specific API response
  const response = await page.waitForResponse("**/api/products");
  const data = await response.json();

  // Wait for response matching a condition
  const response = await page.waitForResponse(
    (resp) => resp.url().includes("/api/") && resp.status() === 200
  );

  // Click and wait for API response
  const [response] = await Promise.all([
    page.waitForResponse("**/api/search"),
    page.locator("#search-button").click(),
  ]);
  ```

  ```python Python theme={null}
  # Wait for a specific API response
  response = await page.wait_for_response("**/api/products")
  data = await response.json()

  # Wait for response matching a condition
  response = await page.wait_for_response(
      lambda resp: "/api/" in resp.url and resp.status == 200
  )

  # Click and wait for API response
  async with page.expect_response("**/api/search") as response_info:
      await page.locator("#search-button").click()
  response = await response_info.value
  ```
</CodeGroup>

<Note>
  Avoid `waitForTimeout()` (fixed delays). Waiting for specific elements or network states is more reliable and faster.
</Note>

For more reliable waiting, the Intuned SDK offers [`waitForDomSettled`](/automation-sdks/intuned-sdk/typescript/helpers/functions/waitForDomSettled) and [`withNetworkSettledWait`](/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait) helpers.

#### Configuring timeouts

The timeout controls how long Playwright waits during auto-waiting before throwing an error. The default is 30 seconds.

**Default timeout** — Set for all actions on a page:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Set default timeout to 10 seconds for all actions
  page.setDefaultTimeout(10000);

  // Set default timeout for navigation only
  page.setDefaultNavigationTimeout(30000);
  ```

  ```python Python theme={null}
  # Set default timeout to 10 seconds for all actions
  page.set_default_timeout(10000)

  # Set default timeout for navigation only
  page.set_default_navigation_timeout(30000)
  ```
</CodeGroup>

**Per-action timeout** — Override for specific actions:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Wait up to 60 seconds for this specific action
  await page.locator("#slow-element").click({ timeout: 60000 });

  // Wait up to 5 seconds for element to appear
  await page.waitForSelector(".loaded", { timeout: 5000 });
  ```

  ```python Python theme={null}
  # Wait up to 60 seconds for this specific action
  await page.locator("#slow-element").click(timeout=60000)

  # Wait up to 5 seconds for element to appear
  await page.wait_for_selector(".loaded", timeout=5000)
  ```
</CodeGroup>

## Extracting data

### Text content

Extract the visible text from elements:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const title = await page.locator(".product_pod h3 a").first().textContent();
  ```

  ```python Python theme={null}
  title = await page.locator(".product_pod h3 a").first.text_content()
  ```
</CodeGroup>

| Method          | What it returns                                          |
| --------------- | -------------------------------------------------------- |
| `textContent()` | All text including hidden elements, preserves whitespace |
| `innerText()`   | Visible text only, normalized whitespace                 |
| `innerHTML()`   | Raw HTML content                                         |

### Attributes

Extract values from HTML attributes like `href`, `src`, `data-*`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const href = await page.locator(".product_pod h3 a").first().getAttribute("href");
  const imageSrc = await page.locator("img.product-image").getAttribute("src");
  const productId = await page.locator(".product").getAttribute("data-product-id");
  ```

  ```python Python theme={null}
  href = await page.locator(".product_pod h3 a").first.get_attribute("href")
  image_src = await page.locator("img.product-image").get_attribute("src")
  product_id = await page.locator(".product").get_attribute("data-product-id")
  ```
</CodeGroup>

### Bulk extraction

Use `all()` to get all matching elements as an array, then extract data from each:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const items = await page.locator(".product_pod").all();
  for (const item of items) {
    const title = await item.locator("h3 a").textContent();
    const price = await item.locator(".price_color").textContent();
    console.log({ title, price });
  }
  ```

  ```python Python theme={null}
  items = await page.locator(".product_pod").all()
  for item in items:
      title = await item.locator("h3 a").text_content()
      price = await item.locator(".price_color").text_content()
      print({"title": title, "price": price})
  ```
</CodeGroup>

<Note>
  `all()` returns an empty array if no elements match. If you need to wait for elements first, use `waitForSelector()` before calling `all()`.
</Note>

For simple cases where you only need text from a single selector, `allTextContents()` and `allInnerTexts()` are convenience methods:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const allTitles = await page.locator(".product_pod h3 a").allTextContents();
  const allVisibleTexts = await page.locator(".product_pod h3 a").allInnerTexts();
  ```

  ```python Python theme={null}
  all_titles = await page.locator(".product_pod h3 a").all_text_contents()
  all_visible_texts = await page.locator(".product_pod h3 a").all_inner_texts()
  ```
</CodeGroup>

### Lists and iteration

Loop through multiple elements using `count()` and `nth()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const items = page.locator(".product_pod h3 a");
  const count = await items.count();

  const titles: string[] = [];
  for (let i = 0; i < count; i++) {
    const title = await items.nth(i).innerText();
    titles.push(title);
  }
  ```

  ```python Python theme={null}
  items = page.locator(".product_pod h3 a")
  count = await items.count()

  titles = []
  for i in range(count):
      title = await items.nth(i).inner_text()
      titles.append(title)
  ```
</CodeGroup>

### Input values

In rare cases, you may need to read the current value from form inputs—for example, when scraping pre-filled forms or verifying form state:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const searchValue = await page.locator("#search-input").inputValue();
  ```

  ```python Python theme={null}
  search_value = await page.locator("#search-input").input_value()
  ```
</CodeGroup>

### Related Intuned SDK helpers

The Intuned SDK provides helper functions that simplify common data extraction patterns. These handle edge cases and reduce boilerplate:

| Helper                                                                                                 | What it does                                     |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| [`scrollToLoadContent`](/automation-sdks/intuned-sdk/typescript/helpers/functions/scrollToLoadContent) | Scrolls to load infinite scroll content          |
| [`clickUntilExhausted`](/automation-sdks/intuned-sdk/typescript/helpers/functions/clickUntilExhausted) | Clicks "Load more" buttons until no more content |
| [`extractMarkdown`](/automation-sdks/intuned-sdk/typescript/helpers/functions/extractMarkdown)         | Extracts page content as clean markdown          |

See the [Pagination recipe](/main/01-learn/recipes/pagination) for multi-page scraping patterns.

## Performing actions

### Clicking

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator('a:has-text("Travel")').click();

  // Wait for navigation after click
  await page.waitForLoadState("networkidle");
  ```

  ```python Python theme={null}
  await page.locator('a:has-text("Travel")').click()

  # Wait for navigation after click
  await page.wait_for_load_state("networkidle")
  ```
</CodeGroup>

#### Double-click

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator(".editable-cell").dblclick();
  ```

  ```python Python theme={null}
  await page.locator(".editable-cell").dblclick()
  ```
</CodeGroup>

#### Hover

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Hover to reveal dropdown menu
  await page.locator(".nav-menu").hover();
  await page.locator(".dropdown-item").click();
  ```

  ```python Python theme={null}
  # Hover to reveal dropdown menu
  await page.locator(".nav-menu").hover()
  await page.locator(".dropdown-item").click()
  ```
</CodeGroup>

#### Force click

Use `force: true` when you know the element is there but Playwright's actionability checks fail (e.g., element is covered by an overlay you want to ignore):

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator("#hidden-button").click({ force: true });
  ```

  ```python Python theme={null}
  await page.locator("#hidden-button").click(force=True)
  ```
</CodeGroup>

<Warning>
  Use `force` sparingly. If you're using it frequently, there may be a better way to handle the interaction.
</Warning>

### Text input

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator("input[name='search']").fill("test text");

  // Clear field before filling
  await page.locator("input[name='search']").clear();
  await page.locator("input[name='search']").fill("new text");
  ```

  ```python Python theme={null}
  await page.locator("input[name='search']").fill("test text")

  # Clear field before filling
  await page.locator("input[name='search']").clear()
  await page.locator("input[name='search']").fill("new text")
  ```
</CodeGroup>

#### Type character by character

Use `pressSequentially()` when a page has special keyboard handling that doesn't work with `fill()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.locator("#autocomplete-input").pressSequentially("new york", { delay: 100 });
  ```

  ```python Python theme={null}
  await page.locator("#autocomplete-input").press_sequentially("new york", delay=100)
  ```
</CodeGroup>

### Keyboard actions

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Press a single key
  await page.keyboard.press("Enter");
  await page.keyboard.press("Escape");

  // Key combinations
  await page.keyboard.press("Control+a");  // Select all
  await page.keyboard.press("Control+c");  // Copy
  await page.keyboard.press("Control+v");  // Paste

  // Press key on a specific element
  await page.locator("#search-input").press("Enter");

  // Type text with full key events
  await page.keyboard.type("Hello World");
  ```

  ```python Python theme={null}
  # Press a single key
  await page.keyboard.press("Enter")
  await page.keyboard.press("Escape")

  # Key combinations
  await page.keyboard.press("Control+a")  # Select all
  await page.keyboard.press("Control+c")  # Copy
  await page.keyboard.press("Control+v")  # Paste

  # Press key on a specific element
  await page.locator("#search-input").press("Enter")

  # Type text with full key events
  await page.keyboard.type("Hello World")
  ```
</CodeGroup>

### Forms

#### Dropdowns

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Select by value
  await page.locator("select#sort-by").selectOption("price-desc");

  // Select by label text
  await page.locator("#dropdown").selectOption({ label: "Option 2" });

  // Select by index
  await page.locator("#dropdown").selectOption({ index: 2 });

  // Select multiple options
  await page.locator("#multi-select").selectOption(["option1", "option2"]);
  ```

  ```python Python theme={null}
  # Select by value
  await page.locator("select#sort-by").select_option("price-desc")

  # Select by label text
  await page.locator("#dropdown").select_option(label="Option 2")

  # Select by index
  await page.locator("#dropdown").select_option(index=2)

  # Select multiple options
  await page.locator("#multi-select").select_option(["option1", "option2"])
  ```
</CodeGroup>

#### Checkboxes and radio buttons

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Check a checkbox or radio button
  await page.locator("input[name='rating'][value='4']").check();

  // Uncheck a checkbox
  await page.locator("#newsletter").uncheck();

  // Set checkbox to specific state
  await page.locator("#terms").setChecked(true);
  ```

  ```python Python theme={null}
  # Check a checkbox or radio button
  await page.locator("input[name='rating'][value='4']").check()

  # Uncheck a checkbox
  await page.locator("#newsletter").uncheck()

  # Set checkbox to specific state
  await page.locator("#terms").set_checked(True)
  ```
</CodeGroup>

### Handling dialogs

JavaScript dialogs (`alert`, `confirm`, `prompt`) block the page until handled. Set up a listener *before* the action that triggers the dialog:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Handle an alert
  page.on("dialog", async (dialog) => {
    console.log(dialog.message());
    await dialog.accept();
  });
  await page.locator("#show-alert").click();

  // Handle a confirm dialog
  page.on("dialog", async (dialog) => {
    if (dialog.type() === "confirm") {
      await dialog.accept(); // Click OK
      // or: await dialog.dismiss(); // Click Cancel
    }
  });

  // Handle a prompt dialog with input
  page.on("dialog", async (dialog) => {
    if (dialog.type() === "prompt") {
      await dialog.accept("My answer"); // Enter text and click OK
    }
  });
  ```

  ```python Python theme={null}
  # Handle an alert
  async def handle_dialog(dialog):
      print(dialog.message)
      await dialog.accept()

  page.on("dialog", handle_dialog)
  await page.locator("#show-alert").click()

  # Handle a confirm dialog
  async def handle_confirm(dialog):
      if dialog.type == "confirm":
          await dialog.accept()  # Click OK
          # or: await dialog.dismiss()  # Click Cancel

  page.on("dialog", handle_confirm)

  # Handle a prompt dialog with input
  async def handle_prompt(dialog):
      if dialog.type == "prompt":
          await dialog.accept("My answer")  # Enter text and click OK

  page.on("dialog", handle_prompt)
  ```
</CodeGroup>

For one-time dialog handling:

<CodeGroup>
  ```typescript TypeScript theme={null}
  page.once("dialog", (dialog) => dialog.accept());
  await page.locator("#delete-button").click();
  ```

  ```python Python theme={null}
  page.once("dialog", lambda dialog: dialog.accept())
  await page.locator("#delete-button").click()
  ```
</CodeGroup>

<Warning>
  If you don't handle a dialog, it will auto-dismiss, but the page may hang waiting for it. Always set up dialog handlers before triggering actions that show dialogs.
</Warning>

### File uploads

<CodeGroup>
  ```typescript TypeScript theme={null}
  await page.setInputFiles("#file-input", "path/to/file.pdf");

  // Multiple files
  await page.setInputFiles("#file-input", ["file1.pdf", "file2.pdf"]);

  // Clear file selection
  await page.setInputFiles("#file-input", []);
  ```

  ```python Python theme={null}
  await page.set_input_files("#file-input", "path/to/file.pdf")

  # Multiple files
  await page.set_input_files("#file-input", ["file1.pdf", "file2.pdf"])

  # Clear file selection
  await page.set_input_files("#file-input", [])
  ```
</CodeGroup>

### File downloads

Listen for the `download` event before triggering the download:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const [download] = await Promise.all([
    page.waitForEvent("download"),
    page.locator("#download-button").click(),
  ]);

  const filePath = await download.path();
  const fileName = download.suggestedFilename();
  ```

  ```python Python theme={null}
  async with page.expect_download() as download_info:
      await page.locator("#download-button").click()

  download = await download_info.value
  file_path = await download.path()
  file_name = download.suggested_filename
  ```
</CodeGroup>

The Intuned SDK provides helpers that simplify file handling and cloud storage:

| Helper                                                                                       | What it does                                   |
| -------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [`downloadFile`](/automation-sdks/intuned-sdk/typescript/helpers/functions/downloadFile)     | Downloads a file from a URL                    |
| [`uploadFileToS3`](/automation-sdks/intuned-sdk/typescript/helpers/functions/uploadFileToS3) | Uploads a file to your S3 bucket               |
| [`saveFileToS3`](/automation-sdks/intuned-sdk/typescript/helpers/functions/saveFileToS3)     | Downloads and uploads a file to S3 in one step |

See the [download file recipe](/main/01-learn/recipes/download-file) and [upload to S3 recipe](/main/01-learn/recipes/upload-files).

### Complete form filling example

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { BrowserContext, Page } from "playwright";

  interface Params {}

  export default async function handler(
    params: Params,
    page: Page,
    context: BrowserContext
  ) {
    await page.goto("https://demoqa.com/automation-practice-form");

    // Text inputs
    await page.getByPlaceholder("First Name").fill("John");
    await page.getByPlaceholder("Last Name").fill("Doe");
    await page.getByPlaceholder("name@example.com").fill("john.doe@test.com");
    await page.getByPlaceholder("Mobile Number").fill("0791234567");

    // Radio button
    await page.getByText("Male", { exact: true }).click();

    // Date picker
    await page.locator("#dateOfBirthInput").click();
    await page.getByRole("option", { name: "15" }).click();

    // Autocomplete field
    await page.locator("#subjectsInput").fill("Maths");
    await page.keyboard.press("Enter");

    // Checkbox
    await page.getByText("Sports").click();

    // File upload
    await page.setInputFiles("#uploadPicture", "test-files/sample.png");

    // Textarea
    await page.locator("#currentAddress").fill("Test Street, Automation City");

    // Submit
    await page.locator("#submit").click();

    return { submitted: true };
  }
  ```

  ```python Python theme={null}
  from playwright.async_api import Page, BrowserContext
  from typing import Any

  async def automation(page: Page, params: dict[str, Any] | None = None, **_kwargs):
      await page.goto("https://demoqa.com/automation-practice-form")

      # Text inputs
      await page.get_by_placeholder("First Name").fill("John")
      await page.get_by_placeholder("Last Name").fill("Doe")
      await page.get_by_placeholder("name@example.com").fill("john.doe@test.com")
      await page.get_by_placeholder("Mobile Number").fill("0791234567")

      # Radio button
      await page.get_by_text("Male", exact=True).click()

      # Date picker
      await page.locator("#dateOfBirthInput").click()
      await page.get_by_role("option", name="15").click()

      # Autocomplete field
      await page.locator("#subjectsInput").fill("Maths")
      await page.keyboard.press("Enter")

      # Checkbox
      await page.get_by_text("Sports").click()

      # File upload
      await page.set_input_files("#uploadPicture", "test-files/sample.png")

      # Textarea
      await page.locator("#currentAddress").fill("Test Street, Automation City")

      # Submit
      await page.locator("#submit").click()

      return {"submitted": True}
  ```
</CodeGroup>

## Advanced topics

### Working with frames

Frames (iframes) embed a separate webpage inside another. You can't interact with iframe content directly from the main page—you need to enter the frame context first.

```html theme={null}
<iframe src="https://payments.example.com"></iframe>
```

Use `frameLocator()` to interact with iframe content:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const loginFrame = page.frameLocator("iframe#login-iframe");

  await loginFrame.locator("input[name='username']").fill("test_user");
  await loginFrame.locator("input[name='password']").fill("secret");
  await loginFrame.locator("button[type='submit']").click();
  ```

  ```python Python theme={null}
  login_frame = page.frame_locator("iframe#login-iframe")

  await login_frame.locator("input[name='username']").fill("test_user")
  await login_frame.locator("input[name='password']").fill("secret")
  await login_frame.locator("button[type='submit']").click()
  ```
</CodeGroup>

For lower-level control, use `contentFrame()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const iframeHandle = await page.waitForSelector("iframe#login-frame");
  const frame = await iframeHandle.contentFrame();

  if (!frame) {
    throw new Error("Frame content not available");
  }

  await frame.fill("input[name='username']", "test_user");
  ```

  ```python Python theme={null}
  iframe = await page.wait_for_selector("iframe#login-frame")
  frame = await iframe.content_frame()

  if not frame:
      raise Exception("Frame content not available")

  await frame.fill("input[name='username']", "test_user")
  ```
</CodeGroup>

<Note>
  **Shadow DOM**: Playwright automatically pierces open shadow DOM—no special handling needed. Your regular locators will find elements inside shadow roots.
</Note>

### Screenshots

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Screenshot of visible viewport
  await page.screenshot({ path: "screenshot.png" });

  // Full page screenshot (entire scrollable area)
  await page.screenshot({ path: "fullpage.png", fullPage: true });

  // Element screenshot
  await page.locator(".product-card").first().screenshot({ path: "product.png" });

  // Mask sensitive elements
  await page.screenshot({
    path: "screenshot.png",
    mask: [
      page.locator(".credit-card-number"),
      page.locator(".ssn-field"),
    ],
  });
  ```

  ```python Python theme={null}
  # Screenshot of visible viewport
  await page.screenshot(path="screenshot.png")

  # Full page screenshot (entire scrollable area)
  await page.screenshot(path="fullpage.png", full_page=True)

  # Element screenshot
  await page.locator(".product-card").first.screenshot(path="product.png")

  # Mask sensitive elements
  await page.screenshot(
      path="screenshot.png",
      mask=[
          page.locator(".credit-card-number"),
          page.locator(".ssn-field"),
      ],
  )
  ```
</CodeGroup>

### Network interception

Intercept and modify network requests using `route()`.

#### Block resources

Speed up automations by blocking unnecessary resources:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Block images
  await page.route("**/*.{png,jpg,jpeg,gif,webp,svg}", (route) => route.abort());

  // Block by resource type
  await page.route("**/*", (route) => {
    const resourceType = route.request().resourceType();
    if (["image", "font", "stylesheet"].includes(resourceType)) {
      route.abort();
    } else {
      route.continue();
    }
  });

  // Block specific domains (analytics, ads)
  await page.route("**/*google-analytics*/**", (route) => route.abort());
  await page.route("**/*doubleclick*/**", (route) => route.abort());
  ```

  ```python Python theme={null}
  # Block images
  await page.route("**/*.{png,jpg,jpeg,gif,webp,svg}", lambda route: route.abort())

  # Block by resource type
  async def block_resources(route):
      if route.request.resource_type in ["image", "font", "stylesheet"]:
          await route.abort()
      else:
          await route.continue_()

  await page.route("**/*", block_resources)

  # Block specific domains (analytics, ads)
  await page.route("**/*google-analytics*/**", lambda route: route.abort())
  await page.route("**/*doubleclick*/**", lambda route: route.abort())
  ```
</CodeGroup>

#### Modify requests

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Add custom headers
  await page.route("**/api/**", (route) => {
    route.continue({
      headers: {
        ...route.request().headers(),
        "X-Custom-Header": "value",
      },
    });
  });
  ```

  ```python Python theme={null}
  # Add custom headers
  async def add_headers(route):
      headers = route.request.headers
      headers["X-Custom-Header"] = "value"
      await route.continue_(headers=headers)

  await page.route("**/api/**", add_headers)
  ```
</CodeGroup>

For more interception patterns, see the [network interception recipe](/main/01-learn/recipes/network-interception).

### Executing JavaScript

Use `page.evaluate()` when you need to access or manipulate the DOM directly, execute custom JavaScript, or access page-level variables.

#### Scrolling

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Scroll to bottom of page
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));

  // Scroll by a specific amount
  await page.evaluate(() => window.scrollBy(0, 500));

  // Scroll element into view
  await page.locator("#target-element").scrollIntoViewIfNeeded();
  ```

  ```python Python theme={null}
  # Scroll to bottom of page
  await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")

  # Scroll by a specific amount
  await page.evaluate("window.scrollBy(0, 500)")

  # Scroll element into view
  await page.locator("#target-element").scroll_into_view_if_needed()
  ```
</CodeGroup>

#### DOM manipulation

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Hide all links
  await page.evaluate(() => {
    document.querySelectorAll("a").forEach(el => {
      (el as HTMLElement).style.display = "none";
    });
  });

  // Get computed styles
  const color = await page.evaluate(() => {
    const el = document.querySelector(".header");
    return window.getComputedStyle(el!).color;
  });
  ```

  ```python Python theme={null}
  # Hide all links
  await page.evaluate("""
      document.querySelectorAll('a').forEach(el => {
          el.style.display = 'none';
      })
  """)

  # Get computed styles
  color = await page.evaluate("""
      const el = document.querySelector('.header');
      window.getComputedStyle(el).color;
  """)
  ```
</CodeGroup>

#### Pass arguments to evaluate

<CodeGroup>
  ```typescript TypeScript theme={null}
  const selector = ".product";
  const result = await page.evaluate((sel) => {
    return document.querySelectorAll(sel).length;
  }, selector);
  ```

  ```python Python theme={null}
  selector = ".product"
  result = await page.evaluate(
      "(sel) => document.querySelectorAll(sel).length",
      selector
  )
  ```
</CodeGroup>

### Making API requests

Playwright provides a built-in API client through `page.request` that shares the browser's cookies and session:

| Benefit        | Description                                |
| -------------- | ------------------------------------------ |
| Shares cookies | Automatically includes session cookies     |
| Same auth      | Uses the browser's authenticated session   |
| Traceable      | Appears in Playwright traces for debugging |

<CodeGroup>
  ```typescript TypeScript theme={null}
  // GET request
  const response = await page.request.get("https://api.example.com/data", {
    headers: { Accept: "application/json" },
  });

  if (!response.ok()) {
    throw new Error(`Request failed: ${response.status()}`);
  }

  const data = await response.json();

  // POST request
  const postResponse = await page.request.post("https://api.example.com/data", {
    headers: { "Content-Type": "application/json" },
    data: { name: "Test", status: "active" },
  });
  ```

  ```python Python theme={null}
  # GET request
  response = await page.request.get(
      "https://api.example.com/data",
      headers={"Accept": "application/json"},
  )

  if not response.ok:
      raise Exception(f"Request failed: {response.status}")

  data = await response.json()

  # POST request
  post_response = await page.request.post(
      "https://api.example.com/data",
      headers={"Content-Type": "application/json"},
      data={"name": "Test", "status": "active"},
  )
  ```
</CodeGroup>

### Cookies and storage

In most cases, you won't need to manage cookies directly—especially if you use [AuthSessions](/main/02-features/auth-sessions), which handle authentication and session state automatically. The methods below are included for reference when you need low-level control.

#### Read cookies

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get all cookies
  const cookies = await context.cookies();

  // Get cookies for specific URLs
  const siteCookies = await context.cookies(["https://example.com"]);

  // Find a specific cookie
  const sessionCookie = cookies.find((c) => c.name === "session_id");
  ```

  ```python Python theme={null}
  # Get all cookies
  cookies = await context.cookies()

  # Get cookies for specific URLs
  site_cookies = await context.cookies(["https://example.com"])

  # Find a specific cookie
  session_cookie = next((c for c in cookies if c["name"] == "session_id"), None)
  ```
</CodeGroup>

#### Set cookies

<CodeGroup>
  ```typescript TypeScript theme={null}
  await context.addCookies([
    {
      name: "session_id",
      value: "abc123",
      domain: ".example.com",
      path: "/",
    },
    {
      name: "preferences",
      value: "dark_mode",
      domain: ".example.com",
      path: "/",
      expires: Math.floor(Date.now() / 1000) + 86400, // 1 day from now
    },
  ]);
  ```

  ```python Python theme={null}
  await context.add_cookies([
      {
          "name": "session_id",
          "value": "abc123",
          "domain": ".example.com",
          "path": "/",
      },
      {
          "name": "preferences",
          "value": "dark_mode",
          "domain": ".example.com",
          "path": "/",
          "expires": int(time.time()) + 86400,  # 1 day from now
      },
  ])
  ```
</CodeGroup>

#### Clear cookies

<CodeGroup>
  ```typescript TypeScript theme={null}
  await context.clearCookies();
  ```

  ```python Python theme={null}
  await context.clear_cookies()
  ```
</CodeGroup>

#### Access localStorage and sessionStorage

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Get localStorage value
  const token = await page.evaluate(() => localStorage.getItem("authToken"));

  // Set localStorage value
  await page.evaluate(() => localStorage.setItem("theme", "dark"));

  // Get all localStorage
  const allStorage = await page.evaluate(() => JSON.stringify(localStorage));

  // Clear localStorage
  await page.evaluate(() => localStorage.clear());

  // Same methods work for sessionStorage
  const sessionData = await page.evaluate(() => sessionStorage.getItem("tempData"));
  ```

  ```python Python theme={null}
  # Get localStorage value
  token = await page.evaluate("localStorage.getItem('authToken')")

  # Set localStorage value
  await page.evaluate("localStorage.setItem('theme', 'dark')")

  # Get all localStorage
  all_storage = await page.evaluate("JSON.stringify(localStorage)")

  # Clear localStorage
  await page.evaluate("localStorage.clear()")

  # Same methods work for sessionStorage
  session_data = await page.evaluate("sessionStorage.getItem('tempData')")
  ```
</CodeGroup>

### Drag and drop

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Drag element to target
  await page.locator("#draggable").dragTo(page.locator("#drop-zone"));

  // With precise positioning
  await page.locator("#draggable").dragTo(page.locator("#drop-zone"), {
    sourcePosition: { x: 10, y: 10 },
    targetPosition: { x: 50, y: 50 },
  });
  ```

  ```python Python theme={null}
  # Drag element to target
  await page.locator("#draggable").drag_to(page.locator("#drop-zone"))

  # With precise positioning
  await page.locator("#draggable").drag_to(
      page.locator("#drop-zone"),
      source_position={"x": 10, "y": 10},
      target_position={"x": 50, "y": 50},
  )
  ```
</CodeGroup>

## Related resources

* **[Playwright basics (TypeScript)](https://github.com/intuned/cookbook/tree/main/typescript-examples/playwright-basics)** — Cookbook example with TypeScript
* **[Playwright basics (Python)](https://github.com/intuned/cookbook/tree/main/python-examples/playwright-basics)** — Cookbook example with Python
* **[Intuned SDK overview](/automation-sdks/overview)** — Helper functions for common tasks
* **[Playwright official docs](https://playwright.dev/docs/intro)** — Full Playwright documentation
