Skip to content

Commit 0520fdb

Browse files
committed
test(viewer): vitest unit/component tests and playwright e2e
Replace the Cypress suite with Vitest (unit + component) and a Playwright end-to-end suite that runs against a live Nextcloud container, and wire the corresponding CI workflows. Signed-off-by: skjnldsv <skjnldsv@protonmail.com> Assisted-by: ClaudeCode:claude-opus-4-8
1 parent e4d2d8b commit 0520fdb

91 files changed

Lines changed: 3334 additions & 2460 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/playwright.yml

Lines changed: 69 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,102 @@
1-
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
22
# SPDX-License-Identifier: MIT
33

4-
name: Playwright
5-
6-
on: pull_request
7-
8-
concurrency:
9-
group: playwright-${{ github.head_ref || github.run_id }}
10-
cancel-in-progress: true
11-
12-
permissions:
13-
contents: read
14-
15-
env:
16-
APP_NAME: ${{ github.event.repository.name }}
17-
BRANCH: ${{ github.base_ref || github.ref_name }}
4+
name: Playwright Tests
5+
on:
6+
pull_request:
7+
branches: [master]
188

199
jobs:
20-
playwright:
10+
playwright-tests:
11+
timeout-minutes: 60
2112
runs-on: ubuntu-latest
22-
23-
name: Playwright E2E tests
24-
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
shardIndex: [1, 2]
17+
shardTotal: [2]
2518
steps:
26-
- name: Disabled on forks
27-
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
28-
run: |
29-
echo 'Can not run Playwright on forks'
30-
exit 1
31-
3219
- name: Checkout app
33-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
34-
with:
35-
persist-credentials: false
20+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
3621

3722
- name: Check composer.json
3823
id: check_composer
39-
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
24+
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v2
4025
with:
4126
files: 'composer.json'
4227

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

47-
- name: Read package.json node and npm engines version
48-
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
32+
- name: Read package.json
33+
uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0
4934
id: versions
50-
with:
51-
fallbackNode: '^24'
52-
fallbackNpm: '^11'
5335

54-
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
55-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
36+
- name: Set up node
37+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
5638
with:
57-
node-version: ${{ steps.versions.outputs.nodeVersion }}
39+
node-version: ${{ steps.versions.outputs.node-version }}
5840

59-
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
60-
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
41+
- name: Set up npm
42+
run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}'
6143

62-
- name: Install dependencies & build app
44+
- name: Install node dependencies & build app
6345
run: |
6446
npm ci
65-
TESTING=true npm run build --if-present
47+
npm run build --if-present
6648
67-
- name: Install Playwright browsers
68-
run: npx playwright install chromium --with-deps
49+
- name: Install Playwright Browsers
50+
run: npx playwright install --with-deps
6951

7052
- name: Run Playwright tests
71-
run: npx playwright test
72-
env:
73-
BRANCH: ${{ env.BRANCH }}
74-
CI: true
53+
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
7554

76-
- name: Show Nextcloud logs on failure
77-
if: failure()
78-
run: |
79-
docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} cat data/nextcloud.log || true
80-
81-
- name: Upload Playwright report
82-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
55+
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
8356
if: ${{ !cancelled() }}
8457
with:
85-
name: playwright-report
86-
path: playwright-report/
87-
retention-days: 7
88-
if-no-files-found: ignore
89-
90-
- name: Upload Playwright traces
91-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
92-
if: failure()
93-
with:
94-
name: playwright-traces
95-
path: test-results/
96-
retention-days: 7
97-
if-no-files-found: ignore
58+
name: blob-report-${{ matrix.shardIndex }}
59+
path: blob-report
60+
retention-days: 1
61+
62+
merge-reports:
63+
# Merge reports after playwright-tests, even if some shards have failed
64+
if: ${{ !cancelled() }}
65+
needs: [playwright-tests]
9866

99-
summary:
10067
runs-on: ubuntu-latest-low
101-
needs: playwright
102-
if: always()
103-
name: playwright-summary
10468
steps:
105-
- name: Summary status
106-
run: if ${{ needs.playwright.result != 'success' && needs.playwright.result != 'skipped' }}; then exit 1; fi
69+
- name: Checkout app
70+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
71+
72+
- name: Read package.json
73+
uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0
74+
id: versions
75+
76+
- name: Set up node
77+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
78+
with:
79+
node-version: ${{ steps.versions.outputs.node-version }}
80+
81+
- name: Set up npm
82+
run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}'
83+
84+
- name: Install dependencies
85+
run: npm ci
86+
87+
- name: Download blob reports from GitHub Actions Artifacts
88+
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
89+
with:
90+
path: all-blob-reports
91+
pattern: blob-report-*
92+
merge-multiple: true
93+
94+
- name: Merge into HTML Report
95+
run: npx playwright merge-reports --config playwright/merge.config.ts ./all-blob-reports
96+
97+
- name: Upload HTML report
98+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
99+
with:
100+
name: html-report--attempt-${{ github.run_attempt }}
101+
path: playwright-report
102+
retention-days: 14

.github/workflows/vitest.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
4+
name: Vitest
5+
6+
on:
7+
pull_request:
8+
branches: [master]
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: vitest-${{ github.head_ref || github.run_id }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
vitest:
19+
runs-on: ubuntu-latest
20+
name: Unit & component tests
21+
22+
steps:
23+
- name: Checkout app
24+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
25+
with:
26+
persist-credentials: false
27+
28+
- name: Read package.json
29+
uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0
30+
id: versions
31+
32+
- name: Set up node
33+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
34+
with:
35+
node-version: ${{ steps.versions.outputs.node-version }}
36+
37+
- name: Set up npm
38+
run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}'
39+
40+
- name: Install dependencies
41+
run: npm ci
42+
43+
- name: Run vitest
44+
run: npm run test:coverage

playwright/e2e/a11y.spec.ts

Lines changed: 22 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,32 @@
1-
/**
2-
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import type { Page } from '@playwright/test'
6+
import { expect } from '@playwright/test'
7+
import { test } from '../support/fixtures/viewer.ts'
78

8-
import { expect, setupFilesPage, test } from '../support/fixtures.ts'
9-
import { getRowForFile, openFile } from '../support/filesUtils.ts'
10-
import {
11-
expectNavArrows,
12-
expectViewerLoaded,
13-
getMediaInContainer,
14-
getViewer,
15-
getViewerName,
16-
} from '../support/viewerUtils.ts'
17-
18-
/** Media element of an inactive (neighbour) slide. */
19-
const inactiveMedia = (page: Page, type: 'img' | 'video') =>
20-
getViewer(page).locator(`.modal-container .viewer__file:not(.viewer__file--active) ${type}`)
21-
22-
test.describe.serial('A11y tests', () => {
23-
let page: Page
24-
25-
test.beforeAll(async ({ browser }) => {
26-
({ page } = await setupFilesPage(browser, [
27-
{ fixture: 'image1.jpg', mimeType: 'image/jpeg' },
28-
{ fixture: 'image2.jpg', mimeType: 'image/jpeg' },
29-
{ fixture: 'video1.mp4', mimeType: 'video/mp4' },
30-
]))
31-
})
32-
33-
test.afterAll(async () => {
34-
await page.close()
35-
})
36-
37-
test('See files in the list', async () => {
38-
await expect(getRowForFile(page, 'image1.jpg')).toContainText('image1 .jpg')
39-
await expect(getRowForFile(page, 'image2.jpg')).toContainText('image2 .jpg')
40-
await expect(getRowForFile(page, 'video1.mp4')).toContainText('video1 .mp4')
9+
test.describe('Viewer accessibility', () => {
10+
test.beforeEach(async ({ filesApp, uploadMedia }) => {
11+
await uploadMedia('image1.jpg', 'image1.jpg', 'image/jpeg')
12+
await uploadMedia('image2.jpg', 'image2.jpg', 'image/jpeg')
13+
await uploadMedia('video1.mp4', 'video1.mp4', 'video/mp4')
14+
await filesApp.openFilesApp()
15+
await expect(filesApp.getRowByName('image1.jpg')).toBeVisible()
4116
})
4217

43-
test('Open the viewer on file click', async () => {
44-
await openFile(page, 'image2.jpg')
45-
await expect(getViewer(page)).toBeVisible()
46-
})
18+
test('exposes the viewer as a labelled dialog with accessible controls', async ({ page, filesApp, viewer }) => {
19+
await filesApp.openFile('image2.jpg')
20+
await viewer.waitForOpen()
4721

48-
test('Does not see a loading animation', async () => {
49-
await expectViewerLoaded(page)
50-
})
51-
52-
test('See the title on the viewer header', async () => {
53-
await expect(getViewerName(page)).toContainText('image2.jpg')
54-
})
55-
56-
test('Should have rendered the previous video and the next image', async () => {
57-
await expectNavArrows(page)
58-
// The previous and the next image slides are preloaded around the active one.
59-
await expect(getMediaInContainer(page, 'img')).toHaveCount(2)
60-
await expect(getMediaInContainer(page, 'video')).toHaveCount(1)
61-
})
62-
63-
test('Should make the previous and the next slides hidden for assistive technologies', async () => {
64-
for (const type of ['video', 'img'] as const) {
65-
const media = inactiveMedia(page, type)
66-
await expect(media.locator('xpath=ancestor::*[@aria-hidden="true"]')).not.toHaveCount(0)
67-
await expect(media.locator('xpath=ancestor::*[@inert]')).not.toHaveCount(0)
68-
}
69-
})
22+
// The modal is a dialog labelled by the current file name.
23+
const dialog = page.locator('.viewer__modal[role="dialog"]')
24+
await expect(dialog).toBeVisible()
25+
expect(await viewer.currentName()).toBe('image2.jpg')
7026

71-
test('Should make video controls on the next slide not focusable', async () => {
72-
const control = getViewer(page)
73-
.locator('.modal-container .viewer__file:not(.viewer__file--active):has(video) button')
74-
.first()
75-
// Force a focus attempt via the DOM; the [inert] ancestor must reject it.
76-
await control.evaluate((el: HTMLElement) => el.focus())
77-
await expect(control).not.toBeFocused()
27+
// Navigation and close controls have accessible names.
28+
await expect(viewer.nextButton).toBeVisible()
29+
await expect(viewer.previousButton).toBeVisible()
30+
await expect(viewer.closeButton).toBeVisible()
7831
})
7932
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test } from '../../support/fixtures/viewer.ts'
7+
8+
test.describe('Viewer close on click outside', () => {
9+
test.beforeEach(async ({ filesApp, uploadMedia }) => {
10+
await uploadMedia('image1.jpg', 'image1.jpg', 'image/jpeg')
11+
await filesApp.openFilesApp()
12+
})
13+
14+
// Regression for nextcloud/viewer#2166: clicking outside the image closes it.
15+
test('closes when clicking outside the media', async ({ filesApp, viewer }) => {
16+
await filesApp.openFile('image1.jpg')
17+
await viewer.waitForOpen()
18+
19+
await viewer.clickOutside()
20+
21+
await viewer.waitForClosed()
22+
})
23+
})

0 commit comments

Comments
 (0)