5.8 KiB
Playwright end-to-end tests
Playwright tests for the Nextcloud server core and bundled apps. The test runner starts a Nextcloud instance inside Docker automatically — no manual setup is needed.
Running the tests
# Install the browser binary once
npm run playwright:install
# Run all tests (starts the server automatically)
npm run playwright
# Run a single spec file
npx playwright test tests/playwright/e2e/files/files-sidebar.spec.ts
# Run with the interactive UI (recommended for local development)
npx playwright test --ui
The dev server is reused between runs locally (reuseExistingServer: true) so subsequent runs start faster.
Viewing test traces
Traces are captured on the first retry of a failing test (trace: 'on-first-retry' in playwright.config.ts). After a run that produced trace files, open the Playwright trace viewer:
# Open the HTML report — includes a "Traces" link for each failing test
npx playwright show-report
# Open a specific trace archive directly
npx playwright show-trace test-results/<test-name>/trace.zip
The trace viewer shows a timeline of every action, a DOM snapshot at each step, network requests, and console output. Use it to pinpoint exactly where a test diverged from expected behavior.
For an even faster loop while writing tests, run in headed mode so you can watch the browser live:
npx playwright test --headed --project=chrome tests/playwright/e2e/files/files-sidebar.spec.ts
Viewing test traces from CI
When a test failed on the CI it is also possible to review the full test run locally.
For this download the "HTML report" archive from the CI summary of the Playwright tests.
Then extract it and use playwright show-trace as described above.
Directory layout
tests/playwright/
├── e2e/ # Test specs, one directory per feature area
│ ├── dav/
│ ├── files/
│ ├── systemtags/
│ └── theming/
└── support/
├── fixtures/ # Playwright fixture extensions (auth, page objects)
├── matchers.ts # Custom expect matchers
├── sections/ # Page Object Model classes
└── utils/ # Shared helpers (DAV, theming, …)
Adding a new test
We use Page Object Models to abstract the Nextcloud UI and make tests reusable to easier create new tests and ease maintenance. You can find more general information here:
1. Pick or create a fixture
Fixtures in support/fixtures/ extend Playwright's test with auth and page objects. Use an existing one when the test area is already covered:
| Fixture file | When to use |
|---|---|
files-page.ts |
Tests that need a random user with filesListPage and filesSidebar |
random-user-session.ts |
Any test needing a fresh random user, no page objects |
admin-session.ts |
Admin-only tests |
admin-theming-page.ts |
Theming admin settings |
admin-appstore-page.ts |
Appstore admin settings |
If no existing fixture fits, extend the closest one:
import { test as baseTest } from './random-user-session.ts'
import { MyPage } from '../sections/MyPage.ts'
export const test = baseTest.extend<{ myPage: MyPage }>({
myPage: async ({ page }, use) => {
await use(new MyPage(page))
},
})
export { expect } from '../matchers.ts'
2. Write the spec
Create e2e/<area>/my-feature.spec.ts. Import test and expect from the fixture:
import { test, expect } from '../../support/fixtures/files-page.ts'
import { uploadContent } from '../../support/utils/dav.ts'
test.describe('Files: my feature', () => {
test.beforeEach(async ({ user, page, filesListPage }) => {
await uploadContent(page.request, user, Buffer.from('hello'), 'text/plain', '/hello.txt')
await filesListPage.open()
})
test('does something', async ({ filesListPage }) => {
await expect(filesListPage.getRowForFile('hello.txt')).toBeVisible()
})
})
Always set up waitForResponse before the action that triggers the request, otherwise there is a race condition:
const saved = page.waitForResponse(r => r.url().includes('/endpoint'))
await page.getByRole('button', { name: 'Save' }).click()
await saved
3. Page Object Models
Page objects live in support/sections/. Each class wraps a Page or a scoped Locator and exposes named locators and action methods. This keeps selectors out of the specs and makes them easy to update when the UI changes.
A minimal page object:
import type { Locator, Page } from '@playwright/test'
export class MyFeaturePage {
constructor(private readonly page: Page) {}
// Locators — return Locator, never await
container(): Locator {
return this.page.locator('[data-cy-my-feature]')
}
submitButton(): Locator {
return this.container().getByRole('button', { name: 'Submit' })
}
// Actions — async, orchestrate one user interaction
async open(): Promise<void> {
await this.page.goto('apps/myapp')
await this.container().waitFor({ state: 'visible' })
}
}
Guidelines:
- Locator methods are synchronous and return
Locator. Only actions areasync. - Scope child locators to
this.container()so they stay inside the component boundary. - Prefer accessible selectors (
getByRole,getByLabel) over CSS classes. Fall back todata-cy-*attributes for elements that have no stable accessible name. - Add the page object to the appropriate fixture so tests receive it as a parameter — do not instantiate page objects inside specs.