diff --git a/README.md b/README.md index c88668fa4a9..2f526984a6a 100644 --- a/README.md +++ b/README.md @@ -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 🛠 diff --git a/package.json b/package.json index 87d75fd531b..5d5c6fab43c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/playwright/README.md b/tests/playwright/README.md new file mode 100644 index 00000000000..355e33b0f5b --- /dev/null +++ b/tests/playwright/README.md @@ -0,0 +1,165 @@ + + +# 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//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//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 { + 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.