- Overview
- Prerequisites
- Running the Tests
- How the Tests Work
- Writing Tests
- Visual Testing
- Test Tags
- Supported Container Runtimes
Element Web contains two sets of Playwright tests:
- Element Web E2E Tests (
playwright/e2e/) - Full end-to-end tests of the Element Web application with real homeserver instances - Shared Components Tests (
packages/shared-components/) - Visual regression tests for the shared component library using Storybook
Both test suites run automatically in CI on every pull request and on every merge to develop & master.
Before running Playwright tests, ensure you have the following set up:
Follow the Playwright installation instructions:
- Browsers: https://playwright.dev/docs/browsers#install-browsers
- System dependencies: https://playwright.dev/docs/browsers#install-system-dependencies
pnpm playwright install --with-depsSee Supported Container Runtimes for details on supported container runtimes (Docker, Podman, Colima).
Element Web E2E tests require an instance running on http://localhost:8080 (configured in playwright.config.ts).
You can either:
- Run manually:
pnpm startin a separate terminal (not working for screenshot tests running in a docker environment). - Auto-start: Playwright will start the webserver automatically if it's not already running
Our main Playwright tests run against a full Element Web instance with Synapse/Dendrite homeservers.
Run all E2E tests:
pnpm run test:playwrightRun a specific test file:
pnpm run test:playwright playwright/e2e/register/register.spec.tsRun tests interactively with Playwright UI:
pnpm run test:playwright:openRun screenshot tests only:
Warning
This command run the playwright tests in a docker environment.
pnpm run test:playwright:screenshotsFor more information about visual testing, see Visual Testing.
Additional command line options: https://playwright.dev/docs/test-cli
See the Shared Components README for instructions on running the shared components Playwright tests.
By default, Playwright runs tests against all "Projects": Chrome, Firefox, "Safari" (Webkit), Dendrite and Picone.
- Chrome, Firefox, Safari run against Synapse
- Dendrite and Picone run against Chrome
Misc:
- Pull Request CI: Tests run only against Chrome
- Merge Queue: Tests run against all projects
- Some tests are excluded from certain browsers due to incompatibilities (see Test Tags)
Element Web tests are located in the playwright/ subdirectory:
playwright/e2e/- E2E test filesplaywright/testcontainers/- Testcontainers for Synapse/Dendrite instancesplaywright/snapshots/- Visual regression test screenshotsplaywright/pages/- Page object modelsplaywright/plugins/- Custom Playwright plugins
Homeservers (Synapse or Dendrite) are launched by Playwright workers and reused for all tests matching the worker configuration.
Configure Synapse options:
test.use({
synapseConfig: {
// Configuration options for the Synapse instance
},
});Important notes:
- Homeservers are reused between tests for efficiency
- Please use unique names for any rooms put into the room directory as they may be visible from other tests, the suggested approach is to use
testInfo.testIdwithin the name or lodash's uniqueId. - We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
- Homeserver logs are attached to Playwright test reports
We heavily leverage Playwright fixtures to provide:
- Homeserver instances (
homeserver) - Logged-in users (
user) - Bot users (
bot) - Application state (
app)
See Writing Tests for usage examples.
For general Playwright best practices, see:
- https://playwright.dev/docs/best-practices
- https://playwright.dev/docs/test-assertions#auto-retrying-assertions (recommended for avoiding flaky tests)
Use the homeserver fixture to acquire a Homeserver instance:
test("should do something", async ({ homeserver }) => {
// homeserver is a ready-to-use Synapse/Dendrite instance
});The fixture provides:
- Server port information
- Instance ID for shutdown
- Registration shared secret (
registrationSecret) for registering users via REST API
Homeserver instances are:
- Reasonably cheap to start (first run may be slow while pulling Docker image)
- Automatically cleaned up by the fixture
Use the user fixture to get a logged-in user:
test("should do something", async ({ user }) => {
// user is logged in and ready to use
});Customize the user:
test.use({
displayName: "Alice",
});
test("should do something", async ({ user }) => {
// user is logged in as "Alice"
});What the fixture does:
- Registers a random userId with the
registrationSecret - Generates a random password (or uses specified display name)
- Seeds localStorage with credentials
- Loads the app at path
/ - Provides user details for User-Interactive Auth if needed
To start with a user in a room:
test("should send a message", async ({ user, app, bot }) => {
// Use the bot client to create a room
const roomId = await bot.createRoom({
name: "Test Room",
invite: [user.userId],
});
// Accept the invite using the app client
await app.client.joinRoom(roomId);
// Now ready to test messaging
});Best practice: Use the REST API (via bot or app.client) to set up room state rather than driving the UI.
Due to CI constraints, use the matrix-js-sdk module exposed on window.matrixcs:
const matrixcs = window.matrixcs;Limitation: Only accessible when the app is loaded. This may be revisited in the future.
For more guidance, see the Playwright best practices guide.
Work with roles, labels, and accessible elements rather than CSS selectors:
// Good
await page.getByRole("button", { name: "Send" }).click();
// Avoid
await page.locator(".mx_MessageComposer_sendButton").click();See https://playwright.dev/docs/locators for more guidance.
- Focus on specific, well-defined units of functionality
- Easier to debug when tests fail
- More maintainable over time
- Each test should run successfully in isolation
- Don't depend on state from other tests
- Clean up after your test if needed
- Use REST APIs to set up test state when possible
- Only drive the UI for the functionality you're actually testing
Example:
// Testing reactions - good approach
test("should react to a message", async ({ page, app, bot }) => {
// Send message via API
const eventId = await bot.sendMessage(roomId, "Hello");
// Test the reaction UI
await page.getByText("Hello").hover();
await page.getByRole("button", { name: "React" }).click();
await page.getByLabel("π").click();
// Verify reaction was sent
await expect(page.getByLabel("π 1")).toBeVisible();
});Playwright locators and assertions automatically wait and retry:
// Good - implicit waiting
await expect(page.getByText("Message sent")).toBeVisible();
// Avoid - explicit waits
await page.waitForTimeout(1000);For dynamic content:
// Assert on the final state - Playwright will wait for it
await expect(page.getByRole("textbox")).toHaveValue("Edited message");
await expect(page.getByText("edited")).toBeVisible();When you do need to wait:
// Wait for network requests
await page.waitForResponse("**/messages");
// Wait for specific conditions
await page.waitForFunction(() => window.matrixcs !== undefined);Playwright has built-in support for visual comparison testing.
Screenshot location: playwright/snapshots/
Rendering environment: Linux Docker (for consistency across environments)
All screenshot tests must use the @screenshot tag:
test("should render message list", { tag: "@screenshot" }, async ({ page }) => {
await expect(page).toMatchScreenshot("message-list.png");
});Purpose of @screenshot tag:
- Allows running only screenshot tests via
test:playwright:screenshots - Speeds up screenshot test runs and updates
Use the custom toMatchScreenshot assertion (not the native toHaveScreenshot):
await expect(page).toMatchScreenshot("my-screenshot.png");Why a custom assertion? We inject custom CSS to stabilize dynamic UI elements (e.g., BaseAvatar color selection based on Matrix ID hash).
Always mask dynamic content that changes between runs:
await expect(page).toMatchScreenshot("chat.png", {
mask: [page.locator(".mx_MessageTimestamp"), page.locator(".mx_BaseAvatar")],
});Common elements to mask:
- Timestamps
- Avatars (when dynamic)
- Animated elements
- User-generated IDs
See Playwright masking docs for more details.
This command runs only tests tagged with @screenshot in the Docker environment.
When you need to update screenshot baselines (e.g., after intentional UI changes):
pnpm run test:playwright:screenshotsImportant: Always use this command to update screenshots rather than running tests locally with --update-snapshots.
Why? Screenshots must be rendered in a consistent Linux Docker environment because:
- Font rendering differs between operating systems (macOS, Windows, Linux)
- Subpixel rendering varies across systems
- Browser rendering engines have platform-specific differences
Using test:playwright:screenshots ensures screenshots are generated in the same Docker environment used in CI, preventing false failures due to rendering differences.
Test tags categorize tests for efficient subset execution.
-
@mergequeue: Slow or flaky tests covering rarely-updated app areas- Not run on every PR commit
- Run in the Merge Queue
-
@screenshot: Tests usingtoMatchScreenshotfor visual regression testing- See the Visual Testing section for detailed usage
-
@no-firefox: Tests unsupported in Firefox- Automatically skipped in Firefox project
- Common reason: Service worker required (disabled in Playwright Firefox for routing)
-
@no-webkit: Tests unsupported in Webkit- Automatically skipped in Webkit project
- Common reasons: Service worker required, microphone functionality unavailable
Add the X-Run-All-Tests label to your pull request to run all tests, including @mergequeue tests.
We use testcontainers to manage Synapse, Matrix Authentication Service, and other service instances.
Supported runtimes:
- Docker (default, recommended)
- Podman
- Colima See setup instructions: https://node.testcontainers.org/supported-container-runtimes/
Colima users:
If using Colima, you may need to set the TMPDIR environment variable to allow bind mounting temporary directories:
export TMPDIR=/tmp/colima
# or
export TMPDIR=$HOME/tmp