Front-end tests
June 26, 2026 · View on GitHub
The app has two layers of front-end tests:
- Vitest for unit and component tests. Fast, runs in jsdom, no server needed.
- Playwright for end-to-end tests. Starts a throwaway Nextcloud instance in Docker and drives a real browser.
PHP tests (PHPUnit, Psalm) live under tests/ and are documented separately.
Vitest (unit & component)
Run
npm test # watch mode
npx vitest run # single run (for CI / pre-push)
npm run test:coverage # single run with a coverage report
The test scripts set LANG=C so assertions on formatted strings are stable.
Where tests live
Specs sit next to the code they cover, as *.spec.ts (or *.test.ts):
src/components/TeamsListItem.vue
src/components/TeamsListItem.spec.ts ← test for the component above
Config is in vitest.config.ts. Global setup (a matchMedia
stub, plus the place to add l10n / window.OC mocks as the app grows) is in
src/test-setup.ts.
Writing a component test
src/components/TeamsListItem.spec.ts is the
reference. Use shallowMount to isolate the component under test from its children:
import { shallowMount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import MyComponent from './MyComponent.vue'
describe('MyComponent', () => {
it('renders the label', () => {
const wrapper = shallowMount(MyComponent, { props: { label: 'Hello' } })
expect(wrapper.text()).toContain('Hello')
})
})
shallowMountstubs child components so the test asserts only this component's behaviour. Usemountwhen you need real child output.- Import
describe/it/expectfromvitest; there are no globals.
Playwright (end-to-end)
Requirements
- Docker running locally (the test runner creates the Nextcloud container).
- The Chromium binary, installed once:
npx playwright install chromium
Run
npm run test:e2e # all specs (boots the server automatically)
npx playwright test --ui # interactive UI, best for local development
npx playwright test --headed # watch the browser live
npx playwright test playwright/e2e/admin-settings.spec.ts # a single spec
npx playwright show-report # open the HTML report after a run
The container is reused between runs locally (reuseExistingServer: true in
playwright.config.ts), so repeat runs start faster. The
first run pulls the image and can take a few minutes.
How it works
start-nextcloud-server.mjs boots a throwaway
Nextcloud container (on the stable* branch matching appinfo/info.xml) with this app
bind-mounted, exposed on port 8089. The setup project then enables the app via
support/setup.ts before the test project runs. All of
this is wired through @nextcloud/e2e-test-server, the same harness the Forms app uses.
Directory layout
playwright/
├── e2e/
│ ├── admin-settings.spec.ts # working smoke test: the Federated Teams admin section
│ └── app-page.spec.ts # skeleton for the Teams SPA (circles#2561), see below
├── support/
│ ├── fixtures.ts # authenticated test fixtures (see table)
│ ├── helpers.ts # waitForApiResponse()
│ └── setup.ts # enables the app in the container (runs once)
└── start-nextcloud-server.mjs # boots the throwaway Nextcloud container
Fixtures
Import test from support/fixtures.ts so the page
arrives already authenticated:
| Fixture | Logs in as | Use for |
|---|---|---|
adminTest | the default admin | admin-settings flows |
userTest | a fresh random user | regular end-user flows (e.g. the Teams page) |
The Teams page skeleton
app-page.spec.ts targets the in-app Teams page at
/apps/circles/teams (the SPA from circles#2561).
It is marked test.fixme so it does not fail the suite until that page lands. Once it
does:
- remove the
test.fixme(...)line, - replace the placeholder selectors with real roles and labels.
Writing a spec
import { expect } from '@playwright/test'
import { userTest as test } from '../support/fixtures.ts'
test.describe('Teams page', () => {
test('creates a team', async ({ page }) => {
await page.goto('apps/circles/teams', { waitUntil: 'networkidle' })
await expect(page.getByRole('button', { name: 'Create team' })).toBeVisible()
})
})
Selector rules (full list in the nextcloud-testing conventions):
- Prefer
getByRole()with an accessible name. Never select by CSS class, especially third-party ones. - Call
waitForApiResponse()fromsupport/helpers.tsbefore the action that triggers the request, then await it after, to avoid a race. - For
NcCheckboxRadioSwitch, click with{ force: true }(the native input is visually hidden).
Traces
Traces are captured on the first retry of a failing test. Open one with:
npx playwright show-trace test-results/<test-name>/trace.zip
CI
Both layers run automatically on pull requests, so there's nothing to set up before writing tests:
- Vitest —
.github/workflows/node-test.ymlrunsnpm run testandnpm run test:coverageand uploads coverage. This is the org workflow template (synced fromnextcloud/.github); don't hand-edit it, the template sync would overwrite your changes. - Playwright —
.github/workflows/playwright.ymlbuilds the app, installs Chromium, and runsnpx playwright test. The HTML report is uploaded as a build artifact (playwright-report, kept 30 days). This one is app-specific, so edit it here as the suite grows.