Coming from Playwright

For AI agents: this page is Markdown at https://squiggle.sh/docs/flows/playwright.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.

A Playwright test scripts a browser; a Squiggle flow lists the same journey as YAML steps, and runs on your React web app and your iOS app alike. Most Playwright calls have a step that does the same.

This page is for a React app that renders with react-dom. Squiggle drives it in Chrome, Chromium or Edge, served by your dev server. See React web apps for setup.

The same test, both ways

Playwright
test('sign in', async ({ page }) => {  await page.goto('http://localhost:5173/');  await page.getByTestId('email').fill('dana@example.com');  await page.getByTestId('password').fill('correct-horse');  await page.getByText('Sign in').click();  await expect(page).toHaveURL(/home/);  await expect(page.getByText('Welcome back')).toBeVisible();});
Squiggle
flow: sign insteps:  - launchApp  - type: { into: { testID: email }, value: dana@example.com }  - type: { into: { testID: password }, value: correct-horse }  - tap: { text: Sign in }  - waitFor: { route: home }  - assert: { visible: { text: Welcome back } }

A flow has no await and no URL in it. Each step waits for the page to settle before the next one starts, and launchApp opens the address you gave squiggle up:

Terminal
squiggle up --url 5173squiggle flow sign-in --udid web:http://localhost:5173

When your iOS app uses the same testIDs, the same file runs on it too.

Calls and steps

PlaywrightSquiggle
page.goto(url)launchApp, or openLink: <url> for another page
page.getByTestId('save').click()tap: { testID: save }
page.getByText('Save').click()tap: { text: Save }
page.getByRole('switch').nth(1).click()tap: { role: switch, nth: 2 } (nth counts from 1)
locator.dblclick()doubleTap: <selector>
locator.fill('dana@example.com')type: { into: <selector>, value: dana@example.com }
page.keyboard.press('Enter')pressKey: Enter
locator.scrollIntoViewIfNeeded()scrollUntilVisible: { text: Item 20 }
page.goBack()back
expect(locator).toBeVisible()assert: { visible: { text: Saved } }
expect(locator).toBeHidden()assert: { notVisible: { text: Saving } }
expect(locator).toBeChecked()assert: { visible: { testID: remember, checked: true } }
expect(page).toHaveURL(/checkout/)waitFor: { route: checkout }
page.waitForLoadState('networkidle')waitFor: { networkIdle: true }
page.route('**/orders', route => route.fulfill(…))network.stub: { urlMatch: /orders, status: 200, body: … }
context.setOffline(true)setAirplaneMode: enabled
context.setGeolocation(…)setLocation: { latitude: 45.81, longitude: 15.98 }
context.grantPermissions(['geolocation'])setPermissions: { permissions: { location: allow } }
page.setViewportSize(…) · devices['iPhone 13']emulate: { viewport: { width: 390, height: 844 } } · emulate: { viewport: phone }
page.emulateMedia({ colorScheme: 'dark' })emulate: { colorScheme: dark }
newContext({ locale })emulate: { locale: de-DE }
newContext({ userAgent })emulate: { userAgent: … }
context.addCookies([…])launchApp: { seed: { cookies: [{ name: session, value: 7f3c2a91 }] } }
newContext({ storageState })launchApp: { seed: { cookies, localStorage, sessionStorage } }
fileChooser.setFiles('receipt.pdf')addMedia: [receipt.pdf], then tap the file input
page.evaluate(() => document.title)evalScript: document.title
page.screenshot({ path: 'home.png' })takeScreenshot: home

Cookies in a seed take the same fields as Playwright’s addCookies. A seed always reopens the app, so its first page load reads them. Every option of each step is in the flow reference.

What works differently

  • No timeouts to tune. A tap waits for what it causes, including a slow navigation, before the next step runs. Use waitFor only for something that happens on its own, such as a request finishing.
  • Selectors name React components and controls. testID matches data-testid, and text, label and role match what the page shows and its accessibility names. There are no CSS or XPath selectors.
  • A passed step means the action happened. To check what it caused, add an assert. See Assertions.
  • Each flow drives one app. A popup or a second tab of the same app is driven like the first. Pages on other sites are not.
  • Logic goes in steps, not code. Use when, repeat and runFlow for branches, loops and shared parts, and runScript to run a JavaScript file in the page. See Repeatable flows.

What Squiggle doesn’t do

  • Firefox and WebKit. Squiggle drives Chromium browsers only.
  • A production build without --production: its component names are minified, so Squiggle refuses it. See React web apps.
  • Apps that don’t render with React. Squiggle reads your app’s React tree, not the page’s HTML.
  • Pinning the page’s clock. deterministic_session turns off animations and pins the locale, but not the time.

What you get on top

The same flow runs on your iOS app. A moved control heals instead of failing (see Healing), and asserts can check what the app did, not only what it shows: renders, dropped frames, and store and query state (see Assertions). To get a first draft, record yourself using the app in Studio (see Record and crawl).