Xwright (pronounced "cross-wright") is an end-to-end testing framework that drives the web, mobile and other devices from a single test, and records what every device did on one timeline.
It runs on Playwright Test, uses Playwright for the web and Mobilewright for mobile, and lets you add any other device through adapters.
The trace of examples/laundry on CI: a test pays and starts a wash on an iOS simulator, the
fake washer runs it, and the dashboard on the web follows.
Warning
Xwright is in early development, and its API may change between minor versions.
| Package | Description |
|---|---|
xwright |
The CLI, the runner, use() and step() |
@xwright/core |
The SDK to write adapters |
@xwright/playwright |
The official adapter for the web, built on Playwright |
@xwright/mobilewright |
The official adapter for iOS and Android, built on Mobilewright |
@xwright/appium |
The official adapter for Appium, built on WebdriverIO |
@xwright/ui |
The UI that shows the traces |
Install xwright and the adapters of the devices to drive:
npm install --save-dev xwright @xwright/playwright @xwright/mobilewrightThen install what the devices of xwright.config.ts need, such as the browsers of Playwright, through their adapters:
npx xwright install # every device of the configuration
npx xwright install --device web -- --with-deps # the browsers, with their system dependencies on Linux
npx xwright install --device phone -- -d <udid> # the agent of Mobilewright on a deviceEach adapter runs the CLI of the library it pins, so that the project needs no dependency on Playwright or
Mobilewright of its own, and the browsers are the ones the adapter launches. The arguments after -- go to the
adapters of the devices taken, which --device and --project choose.
Declare the devices in the projects of xwright.config.ts. Any option of Playwright Test other than use and
webServer goes as is.
import { mobilewright } from "@xwright/mobilewright";
import { playwright } from "@xwright/playwright";
import { defineConfig } from "xwright";
export default defineConfig({
testDir: "tests",
projects: [
{
name: "android",
devices: {
web: { adapter: playwright({ baseURL: "http://localhost:3000" }) },
phone: { adapter: mobilewright({ platform: "android", bundleId: "com.example.app" }) },
},
},
],
});Ask for the devices with use() at the top of a test file, and drive them one after another in a single test.
import type { MobilewrightHandle } from "@xwright/mobilewright";
import type { PlaywrightHandle } from "@xwright/playwright";
import { step, test, use } from "xwright";
declare module "xwright" {
interface Devices {
web: PlaywrightHandle;
phone: MobilewrightHandle;
}
}
const web = use("web");
const phone = use("phone");
test("signs in on the phone after signing up on the web", async () => {
await step("sign up on the web", async () => {
await web.page.goto("/signup");
await web.page.getByRole("button", { name: "Sign up" }).click();
});
await phone.screen.getByRole("button", { name: "Sign in" }).tap();
await phone.expect(phone.screen.getByText("Welcome")).toBeVisible();
});Then run the tests with xwright test, which takes the options of playwright test. A test runs in every project
that has all the devices it uses, and is skipped in the others.
See examples/web-and-mobile for a complete example, and
examples/laundry for an app tested on the web, a phone and a fake washer.
The tree reporter of Xwright prints each test with the steps of step() as a tree when the test ends, so that the
output of tests running in parallel does not interleave. The calls of the APIs and the assertions are shown only when
they failed, or always with showActions: true. On GitHub Actions, the steps of each test are in a group of the log, with
the errors after it. Statuses and errors are colored on a terminal and on GitHub Actions, unless NO_COLOR is set.
export default defineConfig({
reporter: [["tree"]],
// Or, to show every call and assertion:
// reporter: [["tree", { showActions: true }]],
});✓ [android] › washing.spec.ts:12 › starts a wash on the app (21.0s)
├─ pay on the app (3.4s)
└─ the cycle finishes (1.5s)
✘ [web] › washing.spec.ts:30 › shows the wash on the dashboard (8.2s)
└─ ✘ see the wash (5.1s)
└─ ✘ expect.toBeVisible() (5.0s)
It is also --reporter tree of xwright test, and can be used with the other reporters of Playwright Test.
Set trace on a device to record what it did: on, retain-on-failure (kept only when the test failed),
on-first-retry or off (the default).
devices: { phone: { adapter: mobilewright({ platform: "android" }), trace: "retain-on-failure" } }The trace of a test is written to xwright-trace in its output directory, and attached to the report as
xwright-trace:
trace.json: the test, the devices and the result, with the clock to convert the times of the events.test.ndjson: the steps ofstep(), the output and the errors of the test.<device>.ndjsonand<device>/resources/: the actions, snapshots, logs and communications of each device.
Passwords, tokens and other secrets are kept out of the traces: every event, trace.json and every text resource,
such as the bodies of the network, has them replaced with [redacted], also as written in URLs and JSON. List the
secrets known beforehand in the configuration, and wrap the ones known only during a test with secret(). The values
of Authorization, Proxy-Authorization, Cookie and Set-Cookie headers are always left out.
export default defineConfig({
redact: { secrets: [process.env.ADMIN_PASSWORD], headers: ["x-api-key"] },
});import { secret } from "xwright";
const token = secret(await issueToken());Screenshots are images, and are kept as they are; a secret shown on a screen, other than in a password field, stays in its screenshots.
xwright show-trace [path] shows the traces at the path in the browser: a trace, its directory or a ZIP file of it, a
directory with traces such as test-results (the default), or a ZIP file with traces such as the traces artifact of
CI. The tests of several traces are listed as in xwright test --ui, with the same filters, to show the trace of each.
Without traces, it opens a ZIP file dropped on the page. A trace still being written is followed until its test ends.
xwright test --ui lists the tests in the browser to run them, all, filtered by words, statuses, projects and tags,
or one at a time, and shows the trace of each test as it runs. Each run is a new xwright test with the same arguments, so the changes to the tests are picked up.
xwright export-trace writes each trace under test-results (or the paths given) into a ZIP file next to it, such as
xwright-trace.zip, to upload from CI, and deletes the directory of the trace. As with Playwright's trace.zip, each
trace is then in one ZIP file, which xwright show-trace and the UI open.
Events are appended as they happen, in the action model of Playwright's trace, with times of the monotonic clock of
the host. The creation and the disposal of devices go to xwright-worker-<index> in the output directory of the
project.
The options of an adapter can be a function that makes them for each worker, such as to give every worker running at
once its own account or port. It gets parallelIndex, from 0 to the number of workers - 1, which a worker recreated
after a failure keeps, and workerIndex, unique in the run.
web: {
adapter: playwright(({ parallelIndex }) => ({
baseURL: "http://localhost:3000",
httpCredentials: { username: `user${parallelIndex}`, password: process.env.PASSWORD! },
})),
},createOnce and disposeOnce of an adapter run in the runner, outside the workers, and get the options for the
indices 0.
-
playwright(options)launches a browser per worker. Every test gets a fresh browser context and page, orisolation: "file"shares them in a file. It takesbrowserName,launchOptionsand the options of a browser context such asbaseURL. The handle is{ browser, context, page, expect }. When traced, every call of Playwright's API is an action, with the logs, DOM snapshots, screenshots, console messages and network of Playwright's live trace. -
mobilewright(options)borrows a device from Mobilewright's device pool for each file, installsinstallAppsonce per device, and relaunchesbundleIdbefore each test. It takes the options of Mobilewright, such asplatformanddriver. The handle is{ device, screen, expect }. When traced, each operation such as a tap is an action, with a screenshot after it (screenshots), and the view hierarchy is recorded when a test failed (viewTree). -
appium(options)creates a session of an Appium server per worker through WebdriverIO, and launchesappIdagain before each test. It takes theurlof the server, thecapabilitiesof the session andremoteOptionsof WebdriverIO. The handle is{ driver, expect }. When traced, every command sent to the server is an action, with a screenshot after the ones that change the screen, and the source of the page is recorded when a test failed.
Any other device comes in through an adapter, written outside this repository with defineAdapter of
@xwright/core. An adapter depends on @xwright/core instead of xwright, so that it does not need a release for
every release of xwright; xwright exports the same, for an adapter written next to the tests. Every hook is
optional except create, and the hooks run outside the user's. createOnce and disposeOnce run once in the whole
run, in the runner, and the value createOnce returns reaches every worker as ctx.shared.
import { defineAdapter } from "@xwright/core";
export const band = defineAdapter<{ address: string }, BandHandle>({
name: "my-band",
create: ({ options }) => new BandHandle(options.address), // once per worker; what the handle points to
beforeAll: (band) => band.connect(), // for each file
beforeEach: (band) => band.reset(), // for each test
onFailure: (band, { testInfo }) => band.dumpLogs(testInfo), // when a test failed
afterAll: (band) => band.disconnect(),
dispose: (band) => band.close(),
});An adapter records what the device does on its track through ctx.trace, which every hook receives:
create: ({ trace }) => new BandHandle({
// Wrap each command of the driver, so that it shows up as an action on the timeline.
send: (command) => trace.action({ title: command.name, params: command.args }, () => driver.send(command)),
}),examples/laundry/fake-washer is a complete adapter of a fake device.
pnpm install
pnpm build
pnpm check
pnpm testLicensed under the Apache License, Version 2.0.