Testing
Write end-to-end tests that drive your application with Playwright.
Overview
MōBrowser projects come with end-to-end tests written with Playwright. A test drives the built application the way a user does: it clicks, types, and checks what the windows show. Every test starts its own instance of the application with a new profile and a hidden window, so tests don’t depend on each other or on your data.
Creating a project with tests
Projects created with npm create mobrowser-app include the testing setup:
.agents/
├── skills/
├── mobrowser-testing/
.claude/
├── skills/
├── mobrowser-testing/
.github/
├── workflows/
├── test.yml
test/
├── e2e/
├── app.test.ts
playwright.config.tstest/e2e/contains the tests, starting with an example.playwright.config.tsconfigures Playwright. See Configuration..github/workflows/test.ymlruns the tests for every pull request. See Continuous integration..agents/skills/mobrowser-testing/tells AI agents how to write and run the tests. See Testing with AI agents.
package.json includes the test and test:e2e scripts and the @playwright/test dev dependency.
Adding tests to an existing project
To add the testing setup to an existing project, run:
npm run add testing
Note: Projects created with an earlier version of create-mobrowser-app may not have the add script in package.json. In such projects, run npm run mobrowser add testing instead.
The command:
- Installs
@playwright/test. - Adds the
testandtest:e2escripts, keeping any existing ones. - Creates
playwright.config.ts, the testing skill, and, in projects with a renderer, an example test. - Creates
.github/workflows/test.ymlif the project has GitHub Actions workflows.
If the project already has a Playwright configuration file, the command changes nothing.
Running tests
To run the tests, use the following command:
npm test
npm test builds the application and runs the tests in test/e2e/.
Pass Playwright options after --. For example, to run only the tests whose names match a pattern, or the tests in one file:
npm run test:e2e -- -g "greets the user"
npm run test:e2e -- test/e2e/app.test.ts
To watch the tests, show the application window:
npm run test:e2e -- --show-window
Linux
On Linux without a display, such as a CI agent, the CLI runs the tests with xvfb-run. Install Xvfb if it’s missing:
sudo apt install xvfb
On Ubuntu 24.04 and later, the CLI may offer to create an AppArmor profile, as npm run dev does. See AppArmor Restrictions.
Writing tests
Put the tests in test/e2e/ as .test.ts or .test.js files. Import test and expect from @mobrowser/cli/e2e, not @playwright/test:
import { expect, test } from "@mobrowser/cli/e2e";
test("greets the user by name", async ({ page }) => {
await page.getByPlaceholder("Your name").fill("Ada");
await page.getByRole("button", { name: "Greet" }).click();
await expect(page.getByText("Hello, Ada!", { exact: true })).toBeVisible();
});
A test fails if the application doesn’t open, crashes, or exits with an error.
A test can use the following fixtures:
| Fixture | Description |
|---|---|
page | The application’s first window. If the application opens several windows at startup, pick one from context.pages() by its URL or title. |
context | The browser context of the application. context.pages() lists all its windows. |
app | Answers native dialogs. See Native dialogs. |
The rest is regular Playwright. Locators, actions, and assertions work as described in the Playwright documentation.
Native dialogs
Playwright can’t reach native message and file dialogs. MōBrowser intercepts them during tests, and the app fixture answers them.
Queue an answer with app.answerDialog() before the action that opens the dialog:
test("deletes the conversation after confirmation", async ({ page, app }) => {
await app.answerDialog({ button: "Delete" });
await page.getByRole("button", { name: "Delete conversation" }).click();
await expect(page.getByRole("listitem")).toHaveCount(0);
const { dialogs } = await app.dialogs();
expect(dialogs[0].dialog.message).toBe("Delete this conversation?");
});
Queued answers are used in order. An answer can have the following properties:
| Property | Description |
|---|---|
button | The label of the button to press, or its index as a string, such as "0". If no button matches, the default button is pressed. |
textValues | The values of the message dialog’s text fields. |
checkbox | The state of the message dialog’s checkbox. |
paths | The paths an open dialog returns. A save dialog returns the first one. |
canceled | Cancels a file dialog. Message dialogs ignore it. |
Without a queued answer, a message dialog presses its default button and a file dialog is canceled.
app.dialogs() returns the dialogs the application has shown and how they were answered.
Limitations
- Playwright’s context options under
use, such asviewportandlocale, don’t apply: the tests use the application’s own browser context. Thebrowserfixture is a separate Chromium, not your application. - Every test starts the application with a new profile, so a test can’t restart the application and check that data survives the restart. In tests,
app.restart()quits the application instead of restarting it.
Test results
Playwright prints the results to the console. For each failed test, it keeps the following files in build/test-results/e2e/<test>/:
| File | Description |
|---|---|
test-failed-<n>.png | A screenshot of the window at the end of the test. |
trace.zip | A Playwright trace with the actions, console messages, network requests, and a screencast. |
error-context.md | The error and an accessibility snapshot of the page. |
app.log | The output of the application. Start here if the application crashed or exited. |
Each run replaces the results of the previous one.
To open a trace, run:
npx playwright show-trace build/test-results/e2e/<test>/trace.zip
To add Chromium logs to app.log, run the tests with --verbose:
npm run test:e2e -- --verbose
Configuration
playwright.config.ts configures Playwright:
import { defineConfig } from "@playwright/test"
export default defineConfig({
testDir: "test/e2e",
outputDir: "build/test-results/e2e",
preserveOutput: "failures-only",
reporter: "list",
use: { trace: "retain-on-failure", screenshot: "only-on-failure" },
})
You can change it as described in the Playwright documentation. Keep the results in build/test-results/: the test workflow uploads this directory.
Continuous integration
.github/workflows/test.yml runs the tests on macOS, Windows, and Linux for every pull request. Failed tests’ results are uploaded as test-results-<OS> artifacts.
The workflow installs dependencies with npm ci, so commit package-lock.json.
To add the workflow to a project that has the test:e2e script, run npm run add github-actions. See GitHub CI/CD.
Testing with AI agents
Projects include the mobrowser-testing skill in .agents/skills/ and, for Claude Code, in .claude/skills/. The skill makes an agent keep the checks it makes with the automation commands as regression tests, run the tests, and investigate failures.