Skip to main content
Intuned’s runtime is built on top of Playwright, 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:
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 {};
}
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 in the Intuned TypeScript cookbook or the Playwright basics project in the Python cookbook.

Basics

Navigate to a page with goto():
await page.goto("https://books.toscrape.com/");

Wait for page load

After navigation, you may need to wait for the page to fully load. Playwright provides waitForLoadState():
StateWhat it waits for
loadFull page load including images and subframes (default)
domcontentloadedDOM is ready, but resources may still be loading
networkidleNo network connections for at least 500ms
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");

Wait for URL

Wait for the page to navigate to a specific URL pattern:
// 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+/);

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:
const newPage = await context.newPage();
await newPage.goto("https://example.com");
const pageTitle = await newPage.title();

Handle new tabs and popups

When a click opens a new tab (like target="_blank" links), capture it with waitForEvent:
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();
For popups, use the popup event:
const [popup] = await Promise.all([
  page.waitForEvent("popup"),
  page.locator("#open-popup-button").click(),
]);

await popup.waitForLoadState("domcontentloaded");

Finding elements

Locators are the primary way to find elements in Playwright. They’re lazy—not evaluated until you interact with them:
const button = page.locator("button#submit"); // Just a locator, no query yet
await button.click(); // Now it's evaluated

CSS vs XPath selectors

FeatureCSS selectorXPath selector
Syntaxdiv.class #id//div[@class='class']
Best forSimple queries, styling patternsComplex DOM traversals
PerformanceSlightly fasterSlightly slower
const cssLocator = page.locator("div.container > button");
const xpathLocator = page.locator("//div[@class='container']/button");

CSS selector extensions

When using page.locator() with CSS selectors, Playwright adds special pseudo-classes that aren’t available in standard CSS:
ExtensionDescriptionExample
:has-text("text")Contains text anywhere insidediv:has-text("Welcome")
:text("text")Smallest element containing textspan:text("Price")
:text-is("text")Exact text matchbutton:text-is("Submit")
:visibleOnly visible elementsbutton:visible
:has(selector)Contains matching childdiv:has(> img)
// 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)");
For more details, see the Playwright locators documentation.

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:
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("link", { name: "Learn more" }).click();
await page.getByRole("checkbox", { name: "Accept terms" }).check();
getByText() — Find by visible text:
const link = page.getByText("Learn more", { exact: true });
getByLabel() — Find form fields by their label:
const emailInput = page.getByLabel("Email");
await emailInput.fill("[email protected]");
getByPlaceholder() — Find inputs by placeholder text:
const searchInput = page.getByPlaceholder("Search");
await searchInput.fill("Playwright");
getByAltText() — Find images by alt text:
const logo = page.getByAltText("Company Logo");
getByTitle() — Find by title attribute:
const helpIcon = page.getByTitle("Help");
getByTestId() — Find by data-testid attribute:
const submitBtn = page.getByTestId("submit-button");

Chaining and filtering

When a locator matches multiple elements, narrow it down:
MethodWhat it does
.first()First matching element
.last()Last matching element
.nth(index)Element at specific position (0-indexed)
.filter()Add conditions to narrow results
// 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") });

Combining locators

Combine locators with and() and or():
// 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" }));

Checking element state

// 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();
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.

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:
CheckDescription
AttachedElement exists in the DOM
VisibleElement has non-empty bounding box and no visibility: hidden
StableElement isn’t animating (same position for 2 animation frames)
EnabledElement isn’t disabled
Receives eventsElement isn’t obscured by other elements
This means you rarely need explicit waits before actions:
// No need to wait—Playwright handles it
await page.getByRole("button", { name: "Submit" }).click();

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:
// 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" });
Wait for network requests:
// 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(),
]);
Avoid waitForTimeout() (fixed delays). Waiting for specific elements or network states is more reliable and faster.
For more reliable waiting, the Intuned SDK offers waitForDomSettled and 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:
// Set default timeout to 10 seconds for all actions
page.setDefaultTimeout(10000);

// Set default timeout for navigation only
page.setDefaultNavigationTimeout(30000);
Per-action timeout — Override for specific actions:
// 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 });

Extracting data

Text content

Extract the visible text from elements:
const title = await page.locator(".product_pod h3 a").first().textContent();
MethodWhat 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-*:
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");

Bulk extraction

Use all() to get all matching elements as an array, then extract data from each:
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 });
}
all() returns an empty array if no elements match. If you need to wait for elements first, use waitForSelector() before calling all().
For simple cases where you only need text from a single selector, allTextContents() and allInnerTexts() are convenience methods:
const allTitles = await page.locator(".product_pod h3 a").allTextContents();
const allVisibleTexts = await page.locator(".product_pod h3 a").allInnerTexts();

Lists and iteration

Loop through multiple elements using count() and nth():
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);
}

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:
const searchValue = await page.locator("#search-input").inputValue();
The Intuned SDK provides helper functions that simplify common data extraction patterns. These handle edge cases and reduce boilerplate:
HelperWhat it does
scrollToLoadContentScrolls to load infinite scroll content
clickUntilExhaustedClicks “Load more” buttons until no more content
extractMarkdownExtracts page content as clean markdown
See the Pagination recipe for multi-page scraping patterns.

Performing actions

Clicking

await page.locator('a:has-text("Travel")').click();

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

Double-click

await page.locator(".editable-cell").dblclick();

Hover

// Hover to reveal dropdown menu
await page.locator(".nav-menu").hover();
await page.locator(".dropdown-item").click();

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):
await page.locator("#hidden-button").click({ force: true });
Use force sparingly. If you’re using it frequently, there may be a better way to handle the interaction.

Text input

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");

Type character by character

Use pressSequentially() when a page has special keyboard handling that doesn’t work with fill():
await page.locator("#autocomplete-input").pressSequentially("new york", { delay: 100 });

Keyboard actions

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

Forms

// 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"]);

Checkboxes and radio buttons

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

Handling dialogs

JavaScript dialogs (alert, confirm, prompt) block the page until handled. Set up a listener before the action that triggers the dialog:
// 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
  }
});
For one-time dialog handling:
page.once("dialog", (dialog) => dialog.accept());
await page.locator("#delete-button").click();
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.

File uploads

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", []);

File downloads

Listen for the download event before triggering the download:
const [download] = await Promise.all([
  page.waitForEvent("download"),
  page.locator("#download-button").click(),
]);

const filePath = await download.path();
const fileName = download.suggestedFilename();
The Intuned SDK provides helpers that simplify file handling and cloud storage:
HelperWhat it does
downloadFileDownloads a file from a URL
uploadFileToS3Uploads a file to your S3 bucket
saveFileToS3Downloads and uploads a file to S3 in one step
See the download file recipe and upload to S3 recipe.

Complete form filling example

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("[email protected]").fill("[email protected]");
  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 };
}

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.
<iframe src="https://payments.example.com"></iframe>
Use frameLocator() to interact with iframe content:
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();
For lower-level control, use contentFrame():
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");
Shadow DOM: Playwright automatically pierces open shadow DOM—no special handling needed. Your regular locators will find elements inside shadow roots.

Screenshots

// 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"),
  ],
});

Network interception

Intercept and modify network requests using route().

Block resources

Speed up automations by blocking unnecessary resources:
// 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());

Modify requests

// Add custom headers
await page.route("**/api/**", (route) => {
  route.continue({
    headers: {
      ...route.request().headers(),
      "X-Custom-Header": "value",
    },
  });
});
For more interception patterns, see the network interception recipe.

Executing JavaScript

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

Scrolling

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

DOM manipulation

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

Pass arguments to evaluate

const selector = ".product";
const result = await page.evaluate((sel) => {
  return document.querySelectorAll(sel).length;
}, selector);

Making API requests

Playwright provides a built-in API client through page.request that shares the browser’s cookies and session:
BenefitDescription
Shares cookiesAutomatically includes session cookies
Same authUses the browser’s authenticated session
TraceableAppears in Playwright traces for debugging
// 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" },
});

Cookies and storage

In most cases, you won’t need to manage cookies directly—especially if you use AuthSessions, which handle authentication and session state automatically. The methods below are included for reference when you need low-level control.

Read cookies

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

Set cookies

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
  },
]);

Clear cookies

await context.clearCookies();

Access localStorage and sessionStorage

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

Drag and drop

// 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 },
});