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.