Drıftstack docs
Docs

Playwright and Selenium calls as steps

If you already write Playwright or Selenium tests, this page shows, call by call, the step that does the same thing on Driftstack, and says plainly where there is none yet. For the concepts (sessions, profiles, concurrency) and a first full port, read Migrating from Puppeteer / Playwright first.

You do not connect Playwright or Selenium to a Driftstack session: there is no CDP or WebDriver endpoint. Your code sends a list of steps to POST /v1/agent-sessions/{id}/steps, and they run in order, exactly as written, with no AI planning:

{
  "steps": [
    { "kind": "navigate", "url": "https://shop.example.com/login" },
    { "kind": "interact", "action": "type", "selector": "#email", "value": "[email protected]" },
    { "kind": "interact", "action": "tap", "selector": "[data-testid=\"sign-in\"]" },
    { "kind": "wait", "condition": "selector_visible", "selector": "#account", "timeoutMs": 10000 },
    { "kind": "extract", "selector": "#account h1" }
  ]
}

Four things differ from a Playwright or Selenium script:

  • One list runs up to 8 steps. A longer script is several lists, sent one after another.
  • A step that cannot run stops the list. Its row in results says why, and the steps after it are not sent. A wait that runs out of time is the one exception: its row fails and the next step runs.
  • Every element is named by a CSS selector. There are no locator objects and no text, label or XPath locators yet (see Locating elements).
  • Steps act the way a person on the phone does. A tap is a touch, and typing uses the on-screen keyboard, so there is no hover, no right click and no keyboard shortcut.

In the tables, “not supported yet” means no step does it today. Where there is a way round it, the table says so.

Playwright Selenium Driftstack
page.goto(url) driver.get(url) { "kind": "navigate", "url": "https://example.com/" } — a full http:// or https:// address.
page.goBack() driver.back() { "kind": "back" }
page.goForward() driver.forward() Not a step. Between lists, call POST /v1/agent-sessions/{id}/history with { "direction": "forward" }.
page.reload() driver.refresh() Not supported yet. Send a navigate step to the same address instead.
page.url(), page.title() driver.current_url, driver.title GET /v1/agent-sessions/{id}/page-state returns url and title. Where this deployment has it switched on, each successful step row also carries them in page.
page.waitForLoadState() after a click or goto Selenium’s page-load wait Built in: before the first tap, tap_at, type or key press on a new page, the session waits for the page to settle. Add an idle wait (see Waiting) where you need one yourself.

Locating elements

Every step that acts on an element takes a CSS selector. The session looks it up when the step runs and acts on the first match. A selector that is not CSS is refused before it is sent: the row fails with invalid_request.

Playwright Selenium Driftstack
page.locator('#pay') find_element(By.CSS_SELECTOR, '#pay') "selector": "#pay"
page.getByTestId('checkout') find_element(By.CSS_SELECTOR, '[data-testid="checkout"]') "selector": "[data-testid=\"checkout\"]" — a test id is a plain attribute match. Playwright’s data-testid=checkout locator form is refused.
page.getByPlaceholder('Email') find_element(By.CSS_SELECTOR, '[placeholder="Email"]') "selector": "[placeholder=\"Email\"]" matches only a hint that is exactly Email, letter case and spaces included. Playwright’s getByPlaceholder matches part of the hint by default; for that, write [placeholder*="mail"], which still counts letter case.
page.getByRole('button', { name: 'Pay' }) find_element(By.CSS_SELECTOR, 'button[aria-label="Pay"]') Partly: match what the page writes as attributes, as in button[aria-label="Pay"] or [role="button"][aria-label="Pay"]. A name the browser works out from label text, alt text or the element’s own text is not matched.
page.getByText('Sign in'), text=Sign in, :has-text() By.LINK_TEXT, By.PARTIAL_LINK_TEXT Not supported yet. Text locators, :has-text(), :contains() and text= are refused. Match an attribute or the page’s structure instead (a[aria-label="Sign in"], nav > a:first-child).
page.getByLabel('Email') — Not supported yet. Name the field by its own id or name (#email, input[name="email"]).
page.locator('xpath=//button') find_element(By.XPATH, '//button') Not supported: XPath is refused on a tap, type, wait and extract.
locator('form').locator('button'), >> chaining Finding inside an element One descendant selector: "selector": "form button". Playwright’s >> chaining is refused.
locator.nth(2) find_elements(...)[2] On an extract: "index": 2 counts every match in page order, from 0. A tap or type does not take index yet; name the position inside its parent instead (#results > li:nth-of-type(3) a), which counts from 1 among siblings of the same tag.
locator.first() find_element(...) The default: every step acts on the first match.
A locator inside a shadow root element.shadow_root On a tap, type or wait, a >>> path steps into an open shadow root: "selector": "checkout-form >>> button[type=\"submit\"]". A closed shadow root cannot be reached.
page.frameLocator('#card') driver.switch_to.frame(...) A frame on the step itself; see Frames.

Tapping

Playwright Selenium Driftstack
locator.click(), locator.tap() element.click() { "kind": "interact", "action": "tap", "selector": "#pay" } — a touch on the element. The summary names what was tapped (tapped #pay: the button “Pay”).
page.mouse.click(x, y) ActionChains(...).move_by_offset(x, y).click() { "kind": "tap_at", "x": 120, "y": 480 } — viewport pixels from the top-left corner.
locator.check(), locator.uncheck() element.click() on a checkbox A tap toggles it. To set a state, read it first with { "kind": "extract", "selector": "#terms", "property": "checked" } and tap only when it differs.
locator.dblclick() ActionChains(...).double_click() Not supported yet.
Long press (click({ delay })) ActionChains(...).click_and_hold() Not supported yet.
locator.hover() ActionChains(...).move_to_element() None: a touch screen has no hover. A tap is a touch.
click({ button: 'right' }) ActionChains(...).context_click() None: a phone has no right click.
locator.dragTo(target), mouse drags ActionChains(...).drag_and_drop() Not supported yet. An interact step with swipe is not run in a list.
Pinch or zoom gestures — Not supported yet.

Typing and keys

Playwright Selenium Driftstack
locator.pressSequentially(text) element.send_keys(text) { "kind": "interact", "action": "type", "selector": "#q", "value": "blue shoes" } — the text is added to what the field holds.
locator.clear() element.clear() A type step with an empty value: { "kind": "interact", "action": "type", "selector": "#q", "value": "" }
locator.fill(text) element.clear() then element.send_keys(text) Two type steps: "" to clear, then the text. A step that types a saved credential or a session secret clears the field first on its own.
locator.press('Enter') send_keys(Keys.ENTER) { "kind": "interact", "action": "press", "value": "Enter" } — on the focused field. The keys an iPhone keyboard has: Enter (or Return), Backspace, Space and one printable character.
press('Tab'), press('Escape'), arrows Keys.TAB, Keys.ESCAPE, arrow keys None: an iPhone’s on-screen keyboard has no such key. The row fails and the list stops.
press('Control+A') and other shortcuts Keys.CONTROL chords None, for the same reason.
locator.selectOption('CA') Select(el).select_by_value('CA') { "kind": "interact", "action": "type", "selector": "#country", "value": "CA" } — on a session whose browser has the iPhone’s list picker, and only for a list on the page itself. The option’s value is matched exactly first, then its whole text, ignoring letter case.
selectOption({ label: 'Canada' }) select_by_visible_text('Canada') The same type step with the option’s whole text as value.
selectOption({ index: 2 }) select_by_index(2) Not supported yet. Read the options first, then choose by value or text.
selectOption([...]) on a multiple list Several select_by_* calls Not supported yet: a list that takes several choices is refused.
locator.focus(), locator.blur() Focus by clicking A tap focuses a field. There is no blur step; tap somewhere else.
Typing a password from an environment variable The same Register it as a session secret or save it as a saved credential and type its placeholder ({{credential:sec_…}}). Add "sensitive": true to type a card number or a code straight through, without a person’s typing mistakes.

Waiting

Playwright Selenium Driftstack
page.waitForSelector(sel), expect(locator).toBeVisible() WebDriverWait(...).until(visibility_of_element_located(...)) { "kind": "wait", "condition": "selector_visible", "selector": "#dashboard", "timeoutMs": 10000 } — until the element is shown, not only present. timeoutMs is rounded up to whole seconds; without it the wait lasts up to 30 seconds.
page.waitForLoadState('load'), 'networkidle' Page-load waits { "kind": "wait", "condition": "idle" } — until the page has loaded, and never more than 3 seconds after the load. Nothing matches Playwright’s networkidle: on a self-hosted deployment, POST /v1/sessions/:id/navigate accepts wait_until: 'networkidle', and it currently waits for the load event, the same as load (on hosted, direct sessions answer 503 driver-not-integrated). Wait for the element you need instead.
page.waitForURL('**/order/confirmed') WebDriverWait(...).until(url_contains('/order/confirmed')) A preview, only on accounts it is switched on for: { "kind": "wait", "condition": "url_matches", "url": "/order/confirmed" }. The address must contain the text exactly as written; it is never a pattern. See Preview steps.
waitForSelector(sel, { state: 'hidden' }), 'detached' invisibility_of_element_located(...) Not supported yet.
waitForSelector(sel, { state: 'attached' }) presence_of_element_located(...) Not supported yet: selector_visible waits for the element to be shown.
expect(locator).toHaveText(...) text_to_be_present_in_element(...) Not supported yet. Wait for the element, then read its text with extract.
page.waitForResponse(...), waitForRequest(...) — Not supported yet. The network log can be read after the list.
page.waitForTimeout(2000) time.sleep(2) { "kind": "behavioral_pause", "duration_ms": 2000 } — the session pauses between two steps of the same list.
Implicit waits driver.implicitly_wait(...) None. Each wait is a step of its own.

Reading text, attributes and counts

Every read is an extract step. Its answer comes back in the step’s row of results and is never stored by Driftstack.

Playwright Selenium Driftstack
locator.innerText() element.text { "kind": "extract", "selector": "#kpi" } — the element’s rendered text, in the row’s summary.
locator.textContent() element.get_property('textContent') { "kind": "extract", "selector": "#kpi", "property": "textContent" } — every character, hidden text included, in the row’s value.
locator.getAttribute('href') element.get_attribute('href') { "kind": "extract", "selector": "a.terms", "attribute": "href" }
locator.inputValue() element.get_property('value') { "kind": "extract", "selector": "#email", "property": "value" } — what the field holds now, in value.
locator.isChecked() element.is_selected() "property": "checked" — a boolean in value.
locator.isDisabled(), isEnabled() element.is_enabled() "property": "disabled" — a boolean in value.
locator.isVisible() element.is_displayed() Not a field yet. A short selector_visible wait answers it: its row succeeds or fails.
locator.allInnerTexts() [e.text for e in find_elements(...)] { "kind": "extract", "selector": "li.result h3", "all": true } — each match’s text in values, at most 200.
locator.count() len(find_elements(...)) count on a successful extract with all, index, attribute or property: how many elements the selector matched (1,000 means 1,000 or more). A selector that matches nothing fails with element_not_found.
page.content() driver.page_source { "kind": "extract", "selector": "html", "property": "outerHTML" } for the markup (on a session whose browser can read a live property; cut at 100,000 characters), or { "kind": "extract", "body": true } for the page’s text.
page.evaluate(...) driver.execute_script(...) None: running your own script in the page is left out on purpose, because the page can see it. Use the reads above.
context.cookies() driver.get_cookies() Not a step: GET /v1/agent-sessions/{id}/cookies, between lists.
localStorage or sessionStorage reads execute_script('return localStorage…') Not supported yet.

Screenshots and PDFs

Playwright Selenium Driftstack
page.screenshot() driver.save_screenshot(...) { "kind": "capture", "capture": "screenshot" } — the screen as the phone shows it. Fetch the image by the row’s captureId with GET /v1/agent-sessions/{id}/captures/{captureId} soon after the list ends.
page.screenshot({ fullPage: true }) — Not supported yet: a screenshot is the screen, not the whole page.
locator.screenshot() element.screenshot(...) Not supported yet.
page.pdf() driver.print_page(...) Not supported yet. capture lists pdf, but no session can make one: the row fails and the list stops.

Files and downloads

Playwright Selenium Driftstack
locator.setInputFiles(path) send_keys(path) on a file input Not a step. While the page is open, upload the file with POST /v1/agent-sessions/{id}/files, then tap the page’s upload button. The page gets the newest unused upload for its address.
page.waitForEvent('download') A download folder setting Not supported yet: the session does not save a page’s downloads, so the downloads list stays empty.

Frames

Playwright’s frameLocator and Selenium’s switch_to.frame become a frame on each step that acts inside the frame. A step without frame acts on the page itself, as after switch_to.default_content().

Playwright Selenium Driftstack
page.frameLocator('iframe').first() switch_to.frame(0) "frame": [0] — the browser’s numbering, listed by GET /v1/agent-sessions/{id}/frames.
page.frameLocator('iframe[name="card"]') switch_to.frame('card') "frame": [{ "name": "card" }] — the frame whose iframe name is exactly that text.
page.frameLocator('iframe[src*="pay"]') switch_to.frame(find_element(...)) "frame": [{ "src": "pay.example.com" }] — the frame whose address contains that text.
A frame inside a frame Two switch_to.frame calls "frame": [0, 1], outermost first.
A frame inside a shadow root — "frame": ["checkout-form >>> iframe"], on a session whose browser can enter one.

A type, an extract with body: true and a capture of dom_snapshot can name a frame. A tap and a wait for selector_visible can on a session whose browser can act inside a frame, and an extract by selector on one that can read an element there. A frame found by name or src needs a session whose browser can find a frame that way. A scroll or a key press never names one. The full rules are in Steps inside an embedded frame.

{
  "steps": [
    {
      "kind": "wait",
      "condition": "selector_visible",
      "selector": "input[name=\"cardnumber\"]",
      "frame": [{ "name": "card" }],
      "timeoutMs": 10000
    },
    {
      "kind": "interact",
      "action": "type",
      "selector": "input[name=\"cardnumber\"]",
      "value": "{{credential:sec_4be1c09d2f7a4e6b8c35d1a0f9e27b64}}",
      "frame": [{ "name": "card" }]
    }
  ]
}

Tabs and popups

Tabs are calls of their own, between lists, not steps. While a list of steps runs, a tab call answers 409; on an account without tabs, 404.

Playwright Selenium Driftstack
context.newPage() then goto(url) switch_to.new_window('tab') then get(url) POST /v1/agent-sessions/{id}/tabs with { "action": "open", "url": "https://mail.example.com/" }. At most 8 tabs.
page.bringToFront() switch_to.window(handle) { "action": "switch", "tab_id": "t48213" }
page.close() driver.close() { "action": "close", "tab_id": "t48213" }
context.pages() driver.window_handles { "action": "list" }
page.waitForEvent('popup') A new handle after a click Not supported yet: a tab the page opens itself (target="_blank", window.open) is not in the tab list.

Every list of steps, message and history step acts on the tab in front.

Network

Playwright Selenium Driftstack
page.on('request'), page.on('response') Browser performance logs Not a step: GET /v1/agent-sessions/{id}/network lists the requests the pages made (address, method, status).
page.route(...), request interception — Not supported.
page.waitForResponse(...) — Not supported yet.

Dialogs

Playwright Selenium Driftstack
page.on('dialog', (d) => d.accept()) driver.switch_to.alert.accept() { "kind": "dialog", "action": "accept" } — taps OK on the iPhone’s sheet, on a session whose browser can answer dialogs.
dialog.dismiss() alert.dismiss() { "kind": "dialog", "action": "dismiss" }
dialog.accept('Jane') on a prompt alert.send_keys('Jane'), accept() { "kind": "dialog", "action": "accept", "text": "Jane" }
httpCredentials in the browser context Credentials in the address A dialog step answers the browser’s sign-in sheet; see Dialogs.

Scrolling

Playwright Selenium Driftstack
page.mouse.wheel(0, 600) ActionChains(...).scroll_by_amount(0, 600) { "kind": "scroll", "direction": "down", "amount_px": 600 } — up or down; without amount_px, 600 pixels.
locator.scrollIntoViewIfNeeded() ActionChains(...).scroll_to_element(el) { "kind": "interact", "action": "scroll", "selector": "#reviews" } on a session whose browser can scroll to an element; on any other it is a plain scroll. A tap scrolls toward its own target first.
Scrolling sideways, or to the top or bottom scroll_by_amount(600, 0) A preview, only on accounts it is switched on for: "direction": "left" or "right", and "amount": "one_screen", "to_top" or "to_bottom".
Scrolling inside a scrollable panel — Not supported yet: a scroll moves the page itself.

Assertions, retries and longer scripts

  • Assertions (expect(...) in Playwright, assert in a Selenium test) are not steps. Read the value with extract and check it in your own code.
  • Optional steps (a try around a click on a cookie banner) are not supported yet: a tap that fails stops the list. Send the optional step in a list of its own and carry on whatever its row says.
  • Retries. A failed row carries diagnosis.retryable, which says whether sending the same step again can help. Retry in your own code.
  • Longer scripts. One list runs up to 8 steps. Send the next list when the previous one has answered.

Where to go next