docs(tests): Add some general documentation for PlayWright

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-06-10 09:51:27 +02:00
parent 426cbeb192
commit b4bccb2402
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
3 changed files with 176 additions and 1 deletions

View file

@ -62,6 +62,16 @@ Several apps that are included by default in regular releases such as [First run
Otherwise, git checkouts can be handled the same as release archives, by using the `stable*` branches. Note they should never be used on production systems.
#### Testing your code
We use multiple test frameworks for specific areas of the code:
- PHPUnit for PHP unit tests
- Behat for PHP integration tests
- Vitest for Javascript / Typescript unit tests
- Playwright for end-to-end tests
For our end-to-end tests using Playwright you can refer [to our documentation](./tests/playwright/README.md)
on how to debug errors and to contribute new test cases.
### Tools we use 🛠

View file

@ -21,7 +21,7 @@
"cypress:version": "cypress version",
"dev": "build/demi.sh dev",
"postinstall": "build/demi.sh ci",
"lint": "eslint --suppressions-location build/eslint-baseline.json --no-error-on-unmatched-pattern ./cypress",
"lint": "eslint --suppressions-location build/eslint-baseline.json --no-error-on-unmatched-pattern ./cypress ./tests/playwright",
"postlint": "build/demi.sh lint",
"lint:fix": "build/demi.sh lint:fix",
"playwright": "playwright test",

165
tests/playwright/README.md Normal file
View file

@ -0,0 +1,165 @@
<!--
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# 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
```bash
# 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:
```bash
# 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:
```bash
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:
- [General Playwright documentation](https://playwright.dev/docs/writing-tests)
- [Page Object Models](https://playwright.dev/docs/pom)
### 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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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 are `async`.
- Scope child locators to `this.container()` so they stay inside the component boundary.
- Prefer accessible selectors (`getByRole`, `getByLabel`) over CSS classes. Fall back to `data-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.