mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
test: create basic Playwright test infrastructure
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
6c9c739d54
commit
c45a5d4809
11 changed files with 385 additions and 2 deletions
4
.github/workflows/cypress.yml
vendored
4
.github/workflows/cypress.yml
vendored
|
|
@ -105,10 +105,10 @@ jobs:
|
|||
matrix:
|
||||
# Run multiple copies of the current job in parallel
|
||||
# Please increase the number or runners as your tests suite grows (0 based index for e2e tests)
|
||||
containers: ['setup', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
||||
containers: ['setup', '0', '1', '2', '3', '4', '5', '6', '7', '8']
|
||||
# Hack as strategy.job-total includes the "setup" and GitHub does not allow math expressions
|
||||
# Always align this number with the total of e2e runners (max. index + 1)
|
||||
total-containers: [10]
|
||||
total-containers: [9]
|
||||
|
||||
services:
|
||||
mysql:
|
||||
|
|
|
|||
130
.github/workflows/playwright.yml
vendored
Normal file
130
.github/workflows/playwright.yml
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
playwright-tests:
|
||||
timeout-minutes: 60
|
||||
name: Playwright tests ${{ matrix.shardIndex }} / ${{ matrix.shardTotal }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3]
|
||||
shardTotal: [3]
|
||||
outputs:
|
||||
node-version: ${{ steps.versions.outputs.node-version }}
|
||||
package-manager-version: ${{ steps.versions.outputs.package-manager-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true # for 3rdparty
|
||||
|
||||
- name: Read package.json
|
||||
uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0
|
||||
id: versions
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.node-version }}
|
||||
|
||||
- name: Set up npm
|
||||
run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}'
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run playwright -- --shard='${{ matrix.shardIndex }}/${{ matrix.shardTotal }}'
|
||||
|
||||
- name: Show logs
|
||||
if: failure()
|
||||
run: |
|
||||
for id in $(docker ps -aq); do
|
||||
docker container inspect "$id" --format '=== Logs for container {{.Name}} ==='
|
||||
docker logs "$id" >> nextcloud.log
|
||||
done
|
||||
echo '=== Nextcloud server logs ==='
|
||||
docker exec nextcloud-e2e-test-server_server cat data/nextcloud.log
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
|
||||
merge-reports:
|
||||
# Merge reports after playwright-tests, even if some shards have failed
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [playwright-tests]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ needs.playwright-tests.outputs.node-version }}
|
||||
|
||||
- name: Set up npm
|
||||
run: npm i -g 'npm@${{ needs.playwright-tests.outputs.package-manager-version }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --config tests/playwright/merge.config.ts --reporter html,github ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 7
|
||||
|
||||
- name: Show the logs
|
||||
run: |
|
||||
echo 'To view the report:'
|
||||
echo ' 1. Extract the folder from the zip file'
|
||||
echo ' 2. run "npx playwright show-report name-of-my-extracted-playwright-report"'
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [playwright-tests]
|
||||
|
||||
if: always()
|
||||
|
||||
name: playwright-test-summary
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.playwright-tests.result != 'success' }}; then exit 1; fi
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -145,7 +145,9 @@ Vagrantfile
|
|||
|
||||
# Tests - auto-generated files
|
||||
/data-autotest
|
||||
/playwright-report
|
||||
/results.sarif
|
||||
/test-results
|
||||
/tests/.phpunit.cache
|
||||
/tests/.phpunit.result.cache
|
||||
/tests/coverage*
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ $expectedFiles = [
|
|||
'openapi.json',
|
||||
'package-lock.json',
|
||||
'package.json',
|
||||
'playwright.config.ts',
|
||||
'psalm-ncu.xml',
|
||||
'psalm-ocp.xml',
|
||||
'psalm-strict.xml',
|
||||
|
|
|
|||
64
package-lock.json
generated
64
package-lock.json
generated
|
|
@ -47,6 +47,7 @@
|
|||
"@nextcloud/stylelint-config": "^3.2.2",
|
||||
"@nextcloud/typings": "^1.10.0",
|
||||
"@nextcloud/vite-config": "^2.5.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@testing-library/cypress": "^10.1.3",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/vue": "^8.1.0",
|
||||
|
|
@ -3032,6 +3033,22 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
|
|
@ -13409,6 +13426,53 @@
|
|||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
"lint": "eslint --suppressions-location build/eslint-baseline.json --no-error-on-unmatched-pattern ./cypress",
|
||||
"postlint": "build/demi.sh lint",
|
||||
"lint:fix": "build/demi.sh lint:fix",
|
||||
"playwright": "playwright test",
|
||||
"playwright:install": "playwright install chromium-headless-shell",
|
||||
"sass": "sass --style compressed --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then printf \"$cssdir \"; fi; done)",
|
||||
"sass:icons": "node build/icons.mjs",
|
||||
"sass:watch": "sass --watch --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then printf \"$cssdir \"; fi; done)",
|
||||
|
|
@ -76,6 +78,7 @@
|
|||
"@nextcloud/stylelint-config": "^3.2.2",
|
||||
"@nextcloud/typings": "^1.10.0",
|
||||
"@nextcloud/vite-config": "^2.5.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@testing-library/cypress": "^10.1.3",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/vue": "^8.1.0",
|
||||
|
|
|
|||
55
playwright.config.ts
Normal file
55
playwright.config.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/playwright/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? [['blob'], ['dot'], ['github']] : 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:8042/index.php/',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'admin-settings',
|
||||
fullyParallel: false,
|
||||
workers: 1, // only one admin setting test can run at a time due to shared state
|
||||
testMatch: '**/admin-settings*.spec.ts',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'chrome',
|
||||
testMatch: /\/(?!admin-settings)[^/]*\.spec\.ts$/,
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'node tests/playwright/start-nextcloud-server.js',
|
||||
env: {
|
||||
NEXTCLOUD_PORT: '8042',
|
||||
},
|
||||
stderr: 'pipe',
|
||||
stdout: 'pipe',
|
||||
gracefulShutdown: {
|
||||
signal: 'SIGTERM',
|
||||
timeout: 10000,
|
||||
},
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 5 * 60 * 1000,
|
||||
wait: {
|
||||
stdout: /Nextcloud container ready to run Playwright tests/,
|
||||
},
|
||||
},
|
||||
})
|
||||
11
tests/playwright/merge.config.ts
Normal file
11
tests/playwright/merge.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// Needed to merge multiple Playwright reports
|
||||
// when they are ran on self-hosted and github runners (different test directories are used)
|
||||
export default {
|
||||
testDir: 'tests/playwright/e2e',
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
}
|
||||
75
tests/playwright/start-nextcloud-server.js
Normal file
75
tests/playwright/start-nextcloud-server.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { configureNextcloud, runExec, runOcc, startNextcloud, stopNextcloud, waitOnNextcloud } from '@nextcloud/e2e-test-server/docker'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
|
||||
function getMounts() {
|
||||
const mounts = {
|
||||
'3rdparty': resolve(rootDir, '3rdparty'),
|
||||
apps: resolve(rootDir, 'apps'),
|
||||
core: resolve(rootDir, 'core'),
|
||||
dist: resolve(rootDir, 'dist'),
|
||||
lib: resolve(rootDir, 'lib'),
|
||||
ocs: resolve(rootDir, 'ocs'),
|
||||
'ocs-provider': resolve(rootDir, 'ocs-provider'),
|
||||
resources: resolve(rootDir, 'resources'),
|
||||
tests: resolve(rootDir, 'tests'),
|
||||
'console.php': resolve(rootDir, 'console.php'),
|
||||
'cron.php': resolve(rootDir, 'cron.php'),
|
||||
'index.php': resolve(rootDir, 'index.php'),
|
||||
occ: resolve(rootDir, 'occ'),
|
||||
'public.php': resolve(rootDir, 'public.php'),
|
||||
'remote.php': resolve(rootDir, 'remote.php'),
|
||||
'status.php': resolve(rootDir, 'status.php'),
|
||||
'version.php': resolve(rootDir, 'version.php'),
|
||||
}
|
||||
|
||||
return Object.fromEntries(Object.entries(mounts).filter(([, path]) => existsSync(path)))
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const port = Number.parseInt(process.env.NEXTCLOUD_PORT ?? '8042', 10)
|
||||
const ip = await startNextcloud(process.env.BRANCH, false, {
|
||||
mounts: getMounts(),
|
||||
exposePort: port,
|
||||
forceRecreate: true,
|
||||
})
|
||||
|
||||
await runExec(['mkdir', '-p', 'apps-cypress'])
|
||||
await runExec(['cp', 'cypress/fixtures/app.config.php', 'config'])
|
||||
|
||||
await waitOnNextcloud(ip)
|
||||
await configureNextcloud()
|
||||
|
||||
process.stdout.write('\nApply custom configuration for Playwright tests\n')
|
||||
await runOcc(['config:system:set', 'appstoreenabled', '--value', 'false', '--type', 'boolean'])
|
||||
process.stdout.write('├─ Disabled app store\n')
|
||||
await runExec(['php', '-r', '$db = new SQLite3("data/owncloud.db");$db->busyTimeout(5000);$db->exec("PRAGMA journal_mode = wal;");'])
|
||||
process.stdout.write('├─ Enabled SQLite WAL mode for better performance\n')
|
||||
process.stdout.write('├─ Initialize cron job...\n')
|
||||
await runExec(['php', 'cron.php'])
|
||||
process.stdout.write('│ └─ OK !\n')
|
||||
process.stdout.write('└─ Nextcloud container ready to run Playwright tests\n')
|
||||
}
|
||||
|
||||
async function stop() {
|
||||
process.stderr.write('Stopping Nextcloud server…\n')
|
||||
await stopNextcloud()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on('SIGTERM', stop)
|
||||
process.on('SIGINT', stop)
|
||||
|
||||
await start()
|
||||
|
||||
while (true) {
|
||||
await new Promise((resolvePromise) => setTimeout(resolvePromise, 5000))
|
||||
}
|
||||
23
tests/playwright/support/fixtures/admin-session.ts
Normal file
23
tests/playwright/support/fixtures/admin-session.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server'
|
||||
import { login } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { test as baseTest } from '@playwright/test'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
export const test = baseTest.extend({
|
||||
page: async ({ page, context }, use) => {
|
||||
try {
|
||||
await login(context.request, admin)
|
||||
} catch (error) {
|
||||
console.info('Failed to authenticate as admin, retrying', error)
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
await login(context.request, admin)
|
||||
}
|
||||
await use(page)
|
||||
},
|
||||
})
|
||||
19
tests/playwright/support/fixtures/random-user-session.ts
Normal file
19
tests/playwright/support/fixtures/random-user-session.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { test as baseTest } from '@playwright/test'
|
||||
|
||||
export const test = baseTest.extend({
|
||||
page: async ({ page, context }, use) => {
|
||||
const user = await createRandomUser()
|
||||
await login(context.request, user)
|
||||
|
||||
await use(page)
|
||||
|
||||
await runOcc(['user:delete', user.userId])
|
||||
},
|
||||
})
|
||||
Loading…
Reference in a new issue