Skip to content

Commit cdc6cdc

Browse files
authored
Merge pull request #2562 from nextcloud/feature/noid/frontend-test-scaffolding
test: scaffold front-end testing with Vitest and Playwright
2 parents 7257eab + 58c83f0 commit cdc6cdc

17 files changed

Lines changed: 3180 additions & 46 deletions

.github/workflows/node-test.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# This workflow is provided via the organization template repository
2+
#
3+
# https://github.com/nextcloud/.github
4+
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
5+
#
6+
# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
7+
# SPDX-License-Identifier: MIT
8+
9+
name: Node tests
10+
11+
on:
12+
pull_request:
13+
push:
14+
branches:
15+
- main
16+
- master
17+
- stable*
18+
19+
permissions:
20+
contents: read
21+
22+
concurrency:
23+
group: node-tests-${{ github.head_ref || github.run_id }}
24+
cancel-in-progress: true
25+
26+
jobs:
27+
changes:
28+
runs-on: ubuntu-latest-low
29+
permissions:
30+
contents: read
31+
pull-requests: read
32+
33+
outputs:
34+
src: ${{ steps.changes.outputs.src}}
35+
36+
steps:
37+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
38+
id: changes
39+
continue-on-error: true
40+
with:
41+
filters: |
42+
src:
43+
- '.github/workflows/**'
44+
- '__tests__/**'
45+
- '__mocks__/**'
46+
- 'src/**'
47+
- 'appinfo/info.xml'
48+
- 'package.json'
49+
- 'package-lock.json'
50+
- 'tsconfig.json'
51+
- '**.js'
52+
- '**.ts'
53+
- '**.vue'
54+
55+
test:
56+
runs-on: ubuntu-latest
57+
58+
needs: changes
59+
if: needs.changes.outputs.src != 'false'
60+
61+
steps:
62+
- name: Checkout
63+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
64+
with:
65+
persist-credentials: false
66+
67+
- name: Read package.json node and npm engines version
68+
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
69+
id: versions
70+
with:
71+
fallbackNode: '^24'
72+
fallbackNpm: '^11.3'
73+
74+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
75+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
76+
with:
77+
node-version: ${{ steps.versions.outputs.nodeVersion }}
78+
79+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
80+
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
81+
82+
- name: Install dependencies & build
83+
env:
84+
CYPRESS_INSTALL_BINARY: 0
85+
run: |
86+
npm ci
87+
npm run build --if-present
88+
89+
- name: Test
90+
run: npm run test --if-present
91+
92+
- name: Test and process coverage
93+
run: npm run test:coverage --if-present
94+
95+
- name: Collect coverage
96+
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
97+
with:
98+
files: ./coverage/lcov.info
99+
100+
summary:
101+
permissions:
102+
contents: none
103+
runs-on: ubuntu-latest-low
104+
needs: [changes, test]
105+
106+
if: always()
107+
108+
name: test-summary
109+
110+
steps:
111+
- name: Summary status
112+
run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi

.github/workflows/playwright.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
name: Playwright Tests
4+
5+
on:
6+
pull_request:
7+
branches: [master]
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: playwright-${{ github.head_ref || github.run_id }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
test:
18+
timeout-minutes: 60
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout app
22+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
23+
with:
24+
persist-credentials: false
25+
26+
- name: Check composer.json
27+
id: check_composer
28+
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v2
29+
with:
30+
files: 'composer.json'
31+
32+
- name: Install composer dependencies
33+
if: steps.check_composer.outputs.files_exists == 'true'
34+
run: composer install --no-dev
35+
36+
- name: Read package.json node and npm engines version
37+
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
38+
id: versions
39+
with:
40+
fallbackNode: '^22'
41+
fallbackNpm: '^10'
42+
43+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
44+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
45+
with:
46+
node-version: ${{ steps.versions.outputs.nodeVersion }}
47+
48+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
49+
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
50+
51+
- name: Install node dependencies & build app
52+
run: |
53+
npm ci
54+
npm run build --if-present
55+
56+
- name: Install Playwright Browsers
57+
run: npx playwright install chromium --only-shell
58+
59+
- name: Run Playwright tests
60+
run: npx playwright test
61+
62+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
63+
if: always()
64+
with:
65+
name: playwright-report
66+
path: playwright-report/
67+
retention-days: 30

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
/tests/coverage*
88
/tests/clover.xml
99

10+
# front-end test artifacts
11+
/coverage
12+
/playwright-report
13+
/test-results
14+
/playwright/.cache
15+
1016
# 3rdparty
1117
node_modules/
1218
/vendor

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ The *Teams* app is installed and enabled by default.
1818

1919
# Documentation
2020
You can use the `\Psr\Container\ContainerInterface`, see [dependency injection](https://docs.nextcloud.com/server/stable/developer_manual/basics/dependency_injection.html), to get the `\OCA\Circles\CirclesManager` class, see our [API documentation](https://nextcloud.github.io/circles/) for its interface.
21+
22+
# Testing
23+
Front-end tests (Vitest for unit/component, Playwright for end-to-end) are documented in [TESTING.md](TESTING.md).

TESTING.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
# Front-end tests
7+
8+
The app has two layers of front-end tests:
9+
10+
- **Vitest** for unit and component tests. Fast, runs in jsdom, no server needed.
11+
- **Playwright** for end-to-end tests. Starts a throwaway Nextcloud instance in Docker and drives a real browser.
12+
13+
PHP tests (PHPUnit, Psalm) live under `tests/` and are documented separately.
14+
15+
## Vitest (unit & component)
16+
17+
### Run
18+
19+
```bash
20+
npm test # watch mode
21+
npx vitest run # single run (for CI / pre-push)
22+
npm run test:coverage # single run with a coverage report
23+
```
24+
25+
The `test` scripts set `LANG=C` so assertions on formatted strings are stable.
26+
27+
### Where tests live
28+
29+
Specs sit next to the code they cover, as `*.spec.ts` (or `*.test.ts`):
30+
31+
```
32+
src/components/TeamsListItem.vue
33+
src/components/TeamsListItem.spec.ts ← test for the component above
34+
```
35+
36+
Config is in [`vitest.config.ts`](vitest.config.ts). Global setup (a `matchMedia`
37+
stub, plus the place to add l10n / `window.OC` mocks as the app grows) is in
38+
[`src/test-setup.ts`](src/test-setup.ts).
39+
40+
### Writing a component test
41+
42+
[`src/components/TeamsListItem.spec.ts`](src/components/TeamsListItem.spec.ts) is the
43+
reference. Use `shallowMount` to isolate the component under test from its children:
44+
45+
```typescript
46+
import { shallowMount } from '@vue/test-utils'
47+
import { describe, expect, it } from 'vitest'
48+
import MyComponent from './MyComponent.vue'
49+
50+
describe('MyComponent', () => {
51+
it('renders the label', () => {
52+
const wrapper = shallowMount(MyComponent, { props: { label: 'Hello' } })
53+
expect(wrapper.text()).toContain('Hello')
54+
})
55+
})
56+
```
57+
58+
- `shallowMount` stubs child components so the test asserts only this component's behaviour. Use `mount` when you need real child output.
59+
- Import `describe` / `it` / `expect` from `vitest`; there are no globals.
60+
61+
## Playwright (end-to-end)
62+
63+
### Requirements
64+
65+
- Docker running locally (the test runner creates the Nextcloud container).
66+
- The Chromium binary, installed once:
67+
68+
```bash
69+
npx playwright install chromium
70+
```
71+
72+
### Run
73+
74+
```bash
75+
npm run test:e2e # all specs (boots the server automatically)
76+
npx playwright test --ui # interactive UI, best for local development
77+
npx playwright test --headed # watch the browser live
78+
npx playwright test playwright/e2e/admin-settings.spec.ts # a single spec
79+
npx playwright show-report # open the HTML report after a run
80+
```
81+
82+
The container is reused between runs locally (`reuseExistingServer: true` in
83+
[`playwright.config.ts`](playwright.config.ts)), so repeat runs start faster. The
84+
first run pulls the image and can take a few minutes.
85+
86+
### How it works
87+
88+
[`start-nextcloud-server.mjs`](playwright/start-nextcloud-server.mjs) boots a throwaway
89+
Nextcloud container (on the `stable*` branch matching `appinfo/info.xml`) with this app
90+
bind-mounted, exposed on port `8089`. The `setup` project then enables the app via
91+
[`support/setup.ts`](playwright/support/setup.ts) before the test project runs. All of
92+
this is wired through `@nextcloud/e2e-test-server`, the same harness the Forms app uses.
93+
94+
### Directory layout
95+
96+
```
97+
playwright/
98+
├── e2e/
99+
│ ├── admin-settings.spec.ts # working smoke test: the Federated Teams admin section
100+
│ └── app-page.spec.ts # skeleton for the Teams SPA (circles#2561), see below
101+
├── support/
102+
│ ├── fixtures.ts # authenticated test fixtures (see table)
103+
│ ├── helpers.ts # waitForApiResponse()
104+
│ └── setup.ts # enables the app in the container (runs once)
105+
└── start-nextcloud-server.mjs # boots the throwaway Nextcloud container
106+
```
107+
108+
### Fixtures
109+
110+
Import `test` from [`support/fixtures.ts`](playwright/support/fixtures.ts) so the page
111+
arrives already authenticated:
112+
113+
| Fixture | Logs in as | Use for |
114+
|---|---|---|
115+
| `adminTest` | the default admin | admin-settings flows |
116+
| `userTest` | a fresh random user | regular end-user flows (e.g. the Teams page) |
117+
118+
### The Teams page skeleton
119+
120+
[`app-page.spec.ts`](playwright/e2e/app-page.spec.ts) targets the in-app Teams page at
121+
`/apps/circles/teams` (the SPA from [circles#2561](https://github.com/nextcloud/circles/pull/2561)).
122+
It is marked `test.fixme` so it does not fail the suite until that page lands. Once it
123+
does:
124+
125+
1. remove the `test.fixme(...)` line,
126+
2. replace the placeholder selectors with real roles and labels.
127+
128+
### Writing a spec
129+
130+
```typescript
131+
import { expect } from '@playwright/test'
132+
import { userTest as test } from '../support/fixtures.ts'
133+
134+
test.describe('Teams page', () => {
135+
test('creates a team', async ({ page }) => {
136+
await page.goto('apps/circles/teams', { waitUntil: 'networkidle' })
137+
await expect(page.getByRole('button', { name: 'Create team' })).toBeVisible()
138+
})
139+
})
140+
```
141+
142+
Selector rules (full list in the `nextcloud-testing` conventions):
143+
144+
- Prefer `getByRole()` with an accessible name. Never select by CSS class, especially third-party ones.
145+
- Call `waitForApiResponse()` from [`support/helpers.ts`](playwright/support/helpers.ts) **before** the action that triggers the request, then await it after, to avoid a race.
146+
- For `NcCheckboxRadioSwitch`, click with `{ force: true }` (the native input is visually hidden).
147+
148+
### Traces
149+
150+
Traces are captured on the first retry of a failing test. Open one with:
151+
152+
```bash
153+
npx playwright show-trace test-results/<test-name>/trace.zip
154+
```
155+
156+
## CI
157+
158+
Both layers run automatically on pull requests, so there's nothing to set up before writing tests:
159+
160+
- **Vitest**`.github/workflows/node-test.yml` runs `npm run test` and `npm run test:coverage` and uploads coverage. This is the org workflow template (synced from `nextcloud/.github`); don't hand-edit it, the template sync would overwrite your changes.
161+
- **Playwright**`.github/workflows/playwright.yml` builds the app, installs Chromium, and runs `npx 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.

0 commit comments

Comments
 (0)