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
Navigation
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():
| 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 |
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
| 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 |
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:
| 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) |
// 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:
| Method | What 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:
| 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:
// 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 });
Text content
Extract the visible text from elements:
const title = await page.locator(".product_pod h3 a").first().textContent();
| 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-*:
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");
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);
}
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:
| Helper | What it does |
|---|
scrollToLoadContent | Scrolls to load infinite scroll content |
clickUntilExhausted | Clicks “Load more” buttons until no more content |
extractMarkdown | Extracts page content as clean markdown |
See the Pagination recipe for multi-page scraping patterns.
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");
Dropdowns
// 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"]);
// 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:
| Helper | What it does |
|---|
downloadFile | Downloads a file from a URL |
uploadFileToS3 | Uploads a file to your S3 bucket |
saveFileToS3 | Downloads and uploads a file to S3 in one step |
See the download file recipe and upload to S3 recipe.
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.
// 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:
| Benefit | Description |
|---|
| Shares cookies | Automatically includes session cookies |
| Same auth | Uses the browser’s authenticated session |
| Traceable | Appears 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 },
});