Snippets
A snippet in reflow.io is a custom action. It is configured with Typescript/Playwright, executed on the server, and can include anything you want.
Getting Started
To get started snippets, execute reflowio init
. This will create a reflow
subfolder, with the following contents:
reflow/snippets # This is for snippets and their definitions
reflow/snippets/api-request.ts # This is the example snippet
reflow/tests # This is for test definitions
reflow/actions # This is for action definitions
Snippet structure
The following snippet will make an arbitrary API request to a given URL from the reflow server, and demonstrates most common interactions.
// Documentation: https://playwright.dev/docs/api/class-page
import type { Page } from 'playwright-core';
// This represents the event that reflow will invoke the handler with
interface Event {
page: Page;
variables: Record<string, string>;
}
// When the action is executed, it delegates to a `handler` function. This function returns a Promise. If the promise is rejected,
// the test will be marked as failed.
export async function handler(event: Event): Promise<Event> {
switch (event.variables.method) {
case 'get':
await event.page.request.get(event.variables.url, {});
break;
case 'post':
await event.page.request.post(event.variables.url, {
data: JSON.parse(event.variables.bodyJSON),
});
break;
case 'put':
await event.page.request.put(event.variables.url, {
data: JSON.parse(event.variables.bodyJSON),
});
break;
case 'delete':
await event.page.request.delete(event.variables.url, {
data: JSON.parse(event.variables.bodyJSON),
});
break;
case 'patch':
await event.page.request.patch(event.variables.url, {
data: JSON.parse(event.variables.bodyJSON),
});
break;
}
return event;
}
// a constant `dependsOn` array can optionally be defined to identify the snippet's parameters.
// These parameters share a common namespace with page variables. If a parameter is not passed in (i.e. is ''), then
// the associated test variable values will be used instead.
export const dependsOn = ['url', 'method', 'bodyJSON'];
"handler" method
All snippets must contain a handler
. This handler
will be invoked when the snippet is executed. The handler is expected to return a Promise (this happens automatically when it is defined as an async
function).
- When the promise is resolved the action will be marked as successful, and the test will continue.
- When the promise is rejected the action will be marked as unsuccessful, and the test will stop.
- If the promise is neither resolved or rejected, the test will wait for up to the action timeout (or 60s if not defined). If the test is ongoing after 1 hour, the server will be killed via a server lifecycle management process. If you have a use-case for a very long-lived snippet, please contact us; this is a customer-specific limitation.
"dependsOn" array
Optionally snippets can export an array of strings containing parameters. If this is defined, when the custom action is created a user can pass in string values for each parameter.
Global scope
All snippets run in an isolated sandbox. As such, any global scope variables will be reset between executions. To share data between snippet executions, you need to store it in the browser via the playwright page
object.
Pushing changes
To push the set of snippets to reflow for use in tests, execute reflowio push snippet
.
When this is done reflow will:
- Iterate through the
reflow/snippets
folder, selecting any file which ends in.ts
. - Compile that snippet via
esbuild
, ignoring anynode
orplaywright
dependency, but bundling any other imports. Any import path with the prefix!inline!
will be bundled as if they were raw text, for use in injection to the browser. The compiled file will be outputted to*.compiled.js
for reference: it is expected that these are not checked into your vcs, e.g. via a.gitignore
rule - Convert the file name into a snippet name, via converting any
-
characters toapi-request.ts
=>api request
- Compare the compiled snippet content with the existing snippet content via a content SHA. If there is no content change, skip it
- Mark the existing snippet under the same name as deleted, to be deleted in 30 days time
- Push the compiled snippet to S3. Create a new snippet reference under the snippet name, visible to your team, with the provided snippet parameters.
Immediately after this process, the snippet will be visible under the Snippet
Web UI, when navigating from the snippet list.
Pulling the team's snippets
Whilst the existing snippets are expected to be tracked in your version control system, it can sometimes be meaningful to pull the current snippets from reflowio back into your codebase. This can be done with reflowio pull snippet
.
Versioning
Reflow snippets are expected to be checked into your Version Control System. As such, you hold the version information.
When a test is executed with a custom snippet action, the most recent snippet of the provided snippet name is used. I.e. when a snippet is pushed, all new test executions are immediately updated to use the latest version of that snippet.
If you have a locally running dashboard execution (e.g. via a recorder instance), you will retain a cached version of the snippet pulled during the test recorder setup. In this case, you will need to restart your recording instance before the latest changes are visible.
Reflow maintains an archive of overridden snippets (where override is defined to be a reflowio push snippet
with new compiled snippet content) for 30 days.
Importing dependencies
Reflow snippets are bundled by esbuild
before execution, and executed in a sandbox. As such, any dependency that you require
or import
will be inlined into your bundle. This can be observed by looking at the compiled code of your snippet in your file structure or in the Snippet Web UI.
Injecting dependencies into the page
To import dependencies into the page, import your dependency as a string with the !inline!
prefix in your import
or require
statement, and use it in a playwright pageFunction
. For example, the following snippet will inject the axe-core
dependency into the page as an init script following all page navigation/refresh events.
import type { Page } from 'playwright-core';
interface Event {
page: Page;
variables: Record<string, string>;
}
export async function handler(event: Event): Promise<Event> {
await event.page.addInitScript(require('!inline!axe-core/axe.min.js'))
return event;
}
export const dependsOn = [];
Security
Snippets run on a sandbox without Node.js built-in-modules. They are granted a relatively thin slice of functionality to prevent abuse. As such, if you require a dependency, it must be bundled with your snippet.
All test record/replay executions run on customer-specific servers with a temporary authorization token that grants access to only your workspace. Whilst we do not know of any sandbox escape attacks, please be cautious when importing third-party dependencies.
Please contact security@reflow.io for more information.