Flows

Browser automation (Playwright)

Drive a web application from a flow. Experimental.

Web applications are tested through Playwright. Playwright automations have their own YAML files, and an application integrates with them by calling playwright.run with the path to one.

Install the browsers first

Installing this package brings in Playwright, but not the browsers it drives — those are a separate download, and until they are there every browser step fails with Executable doesn't exist. Run this once on the machine that runs the flows:

npx playwright install

That fetches all three browsers. npx playwright install chromium fetches only the one you need. On a bare Linux box (CI, a container) the browsers also need system libraries: npx playwright install --with-deps chromium installs those too, and needs root.

The YAML file decides the browser: browserType (chromium, firefox or webkit, chromium by default), device (a key of Playwright's device list, such as Desktop Chrome) and launchOptions such as headless. The seeded browser application's playwright.signIn.yaml is a complete example.

Sessions

A browser step opens a browser, runs its YAML and closes it again — so three browser steps in a row means three browsers, and each one starts from a blank page.

A session keeps one browser, one context and one page alive across steps. Name it on the step, and the next step naming the same one finds the page exactly as this one left it: same tab, same cookies, same half-filled form.

application: shop
method: search
session: storefront      # the browser this step runs in
closeSession: true       # ... and this step is the last one that needs it
Key Where What it does
session step, or the YAML file The session the run belongs to. The step wins over the file.
session: false step A throw-away browser, even when the YAML file names a session.
closeSession step, or the YAML file Close the session once this step is done with it.

closeSession is optional: whatever a flow leaves open is closed when the flow ends, pass or fail. Leaving it out helps while writing a flow — what went wrong is still on screen when a step fails. A session opened by a YAML file with keepOpen: true is the exception: it is left running on purpose.

An application supports all of this by passing its context to playwright.run, which is what it already does:

export const search = applications.handler([
  // ctx carries the session the flow step asked for
  (ctx, parameters) => playwright.run(ctx, 'playwright.search.yaml', parameters)
], 'search');

playwright.run takes a fourth argument for what the step cannot decide, and the helper exposes the sessions themselves:

playwright.run(ctx, 'search.yaml', parameters, { session: 'storefront' });

playwright.hasSession('storefront');   // is it open?
playwright.openSessions();             // the names of the open ones
playwright.closeSession('storefront'); // close one
playwright.closeSessions();            // close them all — what the runner does

Two things a session cannot do. It is one page, so a step that opens a new tab is on its own; and the browser it was opened with is the browser it keeps, so a later step asking for a different browserType or device is given the one already running.

Putting a file into the browser

upload gives a file to an <input type="file">, which is what a page's "Choose a file" or "Upload" button is really driving. The file goes on the input itself rather than through the file dialog — so it works with the hidden inputs most designs use, and there is no native dialog to get stuck on.

- method: upload
  parameters:
    selector: 'input[type="file"]'
    file: 'fixtures/invoice.pdf'   # read from the application's own folder

A relative path is read from the application folder, the way its YAML files are, so a script names a file that sits next to it. An absolute path is taken as it is, and files: takes a list for an input that accepts several. A file that is not there fails the step rather than uploading nothing.

The button next to the input is usually still worth waiting for: it is where the page says whether the upload is allowed at all.

Taking values out of the browser

Three methods bring something back: scrape reads it off the page, and cookies and storage read it out of the browser itself. All three take the same shape — the key on the left is the name the value comes back under, and what is under it says where to find it.

- method: cookies
  parameters:
    sessionId:
      name: connect.sid       # the cookie's name
    csrf:
      name: /^csrf_/          # ... or a pattern, when the app generates it
    expiresAt:
      name: connect.sid
      field: expires          # value (default), domain, path, expires, httpOnly…
    every: {}                 # no name at all: every cookie, as an object

- method: storage
  parameters:
    token:
      key: access_token       # a localStorage key
    userId:
      key: auth.user
      json: id                # the value is JSON — take this path out of it
    cartCount:
      key: cart.count
      type: session           # sessionStorage instead of localStorage
      output: number
    everything: {}            # no key at all: the whole store

output and regex work exactly as they do for scrape. A cookie or a key that is not there comes back as null rather than failing the run — so a flow can assert that it is there. Cookies are read from the context, so one set on the identity provider is still readable after the redirect back; storage is read from the page, so it is the storage of the origin the browser is on.

All of it is merged, in step order, into the body of the step. Which means a later step of the same YAML file reads it as {{ steps.<id>.result.<key> }}, and the flow asserts on it the usual way:

application: shop
method: login
test:
  body:
    sessionId: "$expr: value && value.length > 10"
    token: "$expr: value !== null"

None of it reaches the flow memory on its own — a flow says what is worth keeping, and under which name, with the step's memory mapping:

application: shop
method: login
memory:
  shop_bearer_token: "{{ body.token }}"

See Passing data between steps for what that mapping can read.

A harvested value is reported to the terminal and persisted with the test run, like any other response body. Name the keys accordingly: the reporter masks a value whose key contains token, password, secret or authorization.

This part is experimental: the set of available methods lives with the Playwright helper. The seeded browser example application shows a complete automation end to end -- it serves its own page on localhost, so it runs with no internet and no account anywhere, and the example flow 04 · Browser, scraping and memory drives it.