Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .github/workflows/node-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT

name: Node tests

on:
pull_request:
push:
branches:
- main
- master
- stable*

permissions:
contents: read

concurrency:
group: node-tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
changes:
runs-on: ubuntu-latest-low
permissions:
contents: read
pull-requests: read

outputs:
src: ${{ steps.changes.outputs.src}}

steps:
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
continue-on-error: true
with:
filters: |
src:
- '.github/workflows/**'
- '__tests__/**'
- '__mocks__/**'
- 'src/**'
- 'appinfo/info.xml'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- '**.js'
- '**.ts'
- '**.vue'

test:
runs-on: ubuntu-latest

needs: changes
if: needs.changes.outputs.src != 'false'

steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
fallbackNode: '^24'
fallbackNpm: '^11.3'

- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}

- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'

- name: Install dependencies & build
env:
CYPRESS_INSTALL_BINARY: 0
run: |
npm ci
npm run build --if-present

- name: Test
run: npm run test --if-present

- name: Test and process coverage
run: npm run test:coverage --if-present

- name: Collect coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
files: ./coverage/lcov.info

summary:
permissions:
contents: none
runs-on: ubuntu-latest-low
needs: [changes, test]

if: always()

name: test-summary

steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi
67 changes: 67 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Playwright Tests

on:
pull_request:
branches: [master]

permissions:
contents: read

concurrency:
group: playwright-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Checkout app
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Check composer.json
id: check_composer
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v2
with:
files: 'composer.json'

- name: Install composer dependencies
if: steps.check_composer.outputs.files_exists == 'true'
run: composer install --no-dev

- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
fallbackNode: '^22'
fallbackNpm: '^10'

- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}

- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"

- name: Install node dependencies & build app
run: |
npm ci
npm run build --if-present

- name: Install Playwright Browsers
run: npx playwright install chromium --only-shell

- name: Run Playwright tests
run: npx playwright test

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
/tests/coverage*
/tests/clover.xml

# front-end test artifacts
/coverage
/playwright-report
/test-results
/playwright/.cache

# 3rdparty
node_modules/
/vendor
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ The *Teams* app is installed and enabled by default.

# Documentation
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.

# Testing
Front-end tests (Vitest for unit/component, Playwright for end-to-end) are documented in [TESTING.md](TESTING.md).
161 changes: 161 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<!--
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->

# Front-end tests

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

```bash
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`](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`](src/test-setup.ts).

### Writing a component test

[`src/components/TeamsListItem.spec.ts`](src/components/TeamsListItem.spec.ts) is the
reference. Use `shallowMount` to isolate the component under test from its children:

```typescript
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')
})
})
```

- `shallowMount` stubs child components so the test asserts only this component's behaviour. Use `mount` when you need real child output.
- Import `describe` / `it` / `expect` from `vitest`; there are no globals.

## Playwright (end-to-end)

### Requirements

- Docker running locally (the test runner creates the Nextcloud container).
- The Chromium binary, installed once:

```bash
npx playwright install chromium
```

### Run

```bash
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`](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`](playwright/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`](playwright/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`](playwright/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`](playwright/e2e/app-page.spec.ts) targets the in-app Teams page at
`/apps/circles/teams` (the SPA from [circles#2561](https://github.com/nextcloud/circles/pull/2561)).
It is marked `test.fixme` so it does not fail the suite until that page lands. Once it
does:

1. remove the `test.fixme(...)` line,
2. replace the placeholder selectors with real roles and labels.

### Writing a spec

```typescript
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()` from [`support/helpers.ts`](playwright/support/helpers.ts) **before** 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:

```bash
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.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.
- **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.
Loading
Loading