Skip to content

Commit 185870a

Browse files
Copilotszaimen
andcommitted
feat(e2e): add Playwright e2e tests for first-run wizard and settings page
Agent-Logs-Url: https://github.com/nextcloud/firstrunwizard/sessions/fead3c70-8ae9-4d07-9ab7-4a42f2bbeb6d fix(e2e): use nextcloud-version-matrix to determine correct server branch Agent-Logs-Url: https://github.com/nextcloud/firstrunwizard/sessions/c1e9236f-f665-4945-980b-77e53a8f025a refactor: use @nextcloud/e2e-test-server for Docker-based e2e testing Agent-Logs-Url: https://github.com/nextcloud/firstrunwizard/sessions/4928786f-c755-4bea-8c14-e5e1ae087ec3 fix(e2e): fix 3 failing test cases - Fix App.vue to skip intro animation in changelog-only mode - Fix 'can be navigated' test: NcButton primary variant has no CSS class - Fix settings test: duplicate ID selector + add Skip button click - Use --with-deps for Playwright install in CI Agent-Logs-Url: https://github.com/nextcloud/firstrunwizard/sessions/57316464-f4c4-4f0c-85e3-87160d3bcd03 fix(e2e): revert App.vue changes, fix tests without app-level workarounds - Revert src/views/App.vue to original state (always start at intro animation) - Fix 'opens when a new major version' test: click Skip to advance past intro - Move 'About & What's new' wizard test from settings.spec.ts to firstrunwizard.spec.ts - Remove 'About & What's new' describe block from settings.spec.ts Agent-Logs-Url: https://github.com/nextcloud/firstrunwizard/sessions/46619e35-ecf5-4089-a682-8830bd9abfe6 fix(e2e): fix setUserPreference to pass value as positional arg to occ user:setting occ user:setting takes the value as a positional argument, not --value=VALUE. The --value flag was silently ignored, so setUserPreference never actually set the 'show' preference, causing the 'opens when a new major version was shipped' test to always fail with the wizard landing on page 0 instead of the whats-new page. Agent-Logs-Url: https://github.com/nextcloud/firstrunwizard/sessions/784a92c7-5c87-4afc-b355-9a4633cbe01d Co-Authored-By: szaimen <42591237+szaimen@users.noreply.github.com>
1 parent d9e040b commit 185870a

11 files changed

Lines changed: 1276 additions & 189 deletions

.github/workflows/e2e.yml

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
name: E2E Tests
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches:
10+
- main
11+
- master
12+
- stable*
13+
14+
permissions:
15+
contents: read
16+
17+
concurrency:
18+
group: e2e-${{ github.head_ref || github.run_id }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
changes:
23+
runs-on: ubuntu-latest-low
24+
permissions:
25+
contents: read
26+
pull-requests: read
27+
28+
outputs:
29+
src: ${{ steps.changes.outputs.src }}
30+
31+
steps:
32+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
33+
id: changes
34+
continue-on-error: true
35+
with:
36+
filters: |
37+
src:
38+
- '.github/workflows/e2e.yml'
39+
- 'appinfo/**'
40+
- 'lib/**'
41+
- 'src/**'
42+
- 'templates/**'
43+
- 'e2e/**'
44+
- 'playwright.config.ts'
45+
- 'package.json'
46+
- 'package-lock.json'
47+
48+
e2e-tests:
49+
runs-on: ubuntu-latest
50+
51+
needs: [changes]
52+
if: needs.changes.outputs.src != 'false'
53+
54+
name: Playwright E2E
55+
56+
steps:
57+
- name: Checkout app
58+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
persist-credentials: false
61+
62+
- name: Read package.json node and npm engines version
63+
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
64+
id: versions
65+
with:
66+
fallbackNode: '^24'
67+
fallbackNpm: '^11.3'
68+
69+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
70+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
71+
with:
72+
node-version: ${{ steps.versions.outputs.nodeVersion }}
73+
74+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
75+
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
76+
77+
- name: Install dependencies
78+
run: npm ci
79+
80+
- name: Install Playwright browsers
81+
run: npx playwright install chromium --with-deps
82+
83+
- name: Build assets
84+
run: npm run build
85+
86+
- name: Run E2E tests
87+
run: npm run test:e2e
88+
89+
- name: Upload Playwright report
90+
if: always()
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: playwright-report
94+
path: playwright-report/
95+
retention-days: 30
96+
97+
summary:
98+
permissions:
99+
contents: none
100+
runs-on: ubuntu-latest-low
101+
needs: [changes, e2e-tests]
102+
103+
if: always()
104+
105+
name: e2e-summary
106+
107+
steps:
108+
- name: Summary status
109+
run: if ${{ needs.changes.outputs.src != 'false' && needs.e2e-tests.result != 'success' }}; then exit 1; fi

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ vendor/
1313
/.php-cs-fixer.cache
1414
/tests/.phpunit.result.cache
1515
.DS_Store
16+
17+
# Playwright e2e test artifacts
18+
/playwright-report/
19+
/test-results/
20+
/blob-report/

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
66
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/firstrunwizard"
77

88
[[annotations]]
9-
path = [".gitattributes", ".github/issue_template.md", ".github/CODEOWNERS", ".editorconfig", "package-lock.json", "package.json", "composer.json", "composer.lock", "**/composer.json", "**/composer.lock", ".l10nignore", "cypress/tsconfig.json", "vendor-bin/**/composer.json", "vendor-bin/**/composer.lock", ".tx/config", "tsconfig.json", "krankerl.toml", ".npmignore", ".nextcloudignore"]
9+
path = [".gitattributes", ".github/issue_template.md", ".github/CODEOWNERS", ".editorconfig", "package-lock.json", "package.json", "composer.json", "composer.lock", "**/composer.json", "**/composer.lock", ".l10nignore", "cypress/tsconfig.json", "e2e/tsconfig.json", "vendor-bin/**/composer.json", "vendor-bin/**/composer.lock", ".tx/config", "tsconfig.json", "krankerl.toml", ".npmignore", ".nextcloudignore"]
1010
precedence = "aggregate"
1111
SPDX-FileCopyrightText = "none"
1212
SPDX-License-Identifier = "CC0-1.0"

e2e/firstrunwizard.spec.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from '@playwright/test'
7+
import { createRandomUser, deleteUser, login, setUserPreference } from './support/utils.ts'
8+
9+
test.describe('First Run Wizard', () => {
10+
test('opens automatically on first login', async ({ page }) => {
11+
const user = await createRandomUser()
12+
13+
try {
14+
await login(page, user.userId, user.password)
15+
16+
// The wizard is injected by first-run.ts and opened automatically
17+
const wizard = page.locator('.first-run-wizard')
18+
await expect(wizard).toBeVisible()
19+
20+
// For a brand-new user the intro animation (video) is shown first
21+
await expect(wizard.locator('video')).toBeVisible()
22+
} finally {
23+
await deleteUser(user.userId)
24+
}
25+
})
26+
27+
test('opens when a new major version was shipped', async ({ page }) => {
28+
const user = await createRandomUser()
29+
30+
try {
31+
// Simulate a user who last saw wizard version 2.0.0.
32+
// The stored version is greater than "1" (so changelogOnly = true)
33+
// but less than the current CHANGELOG_VERSION (33.0.0),
34+
// so the wizard is injected and opened again.
35+
await setUserPreference(user.userId, 'firstrunwizard', 'show', '2.0.0')
36+
37+
await login(page, user.userId, user.password)
38+
39+
const wizard = page.locator('.first-run-wizard')
40+
await expect(wizard).toBeVisible()
41+
42+
// The intro animation is always shown first; skip it to advance
43+
// directly to the "What's new" page (changelog-only mode).
44+
const skipButton = wizard.getByRole('button', { name: 'Skip' })
45+
await expect(skipButton).toBeVisible({ timeout: 5_000 })
46+
await skipButton.click()
47+
48+
// In changelog-only mode the wizard advances directly to the
49+
// "What's new" page after the intro animation.
50+
await expect(wizard).toContainText('New in Nextcloud Hub')
51+
} finally {
52+
await deleteUser(user.userId)
53+
}
54+
})
55+
56+
test('"About & What\'s new" menu entry reopens the wizard', async ({ page }) => {
57+
const user = await createRandomUser()
58+
59+
try {
60+
await login(page, user.userId, user.password)
61+
62+
// Close the first-run wizard that opens automatically on first login
63+
const wizard = page.locator('.first-run-wizard')
64+
await expect(wizard).toBeVisible()
65+
66+
// Skip the intro animation as soon as the Skip button appears (~2s)
67+
const skipButton = wizard.getByRole('button', { name: 'Skip' })
68+
await expect(skipButton).toBeVisible({ timeout: 5_000 })
69+
await skipButton.click()
70+
71+
// Close the slideshow
72+
const closeButton = wizard.getByRole('button', { name: 'Close' })
73+
await expect(closeButton).toBeVisible()
74+
await closeButton.click()
75+
await expect(wizard).not.toBeVisible()
76+
77+
// Open the user settings menu to find the "About & What's new" entry
78+
const userMenu = page.locator('[aria-controls="header-menu-user-menu"]')
79+
await userMenu.click()
80+
81+
// Use the link role to avoid strict mode violation from the duplicate ID
82+
// that Nextcloud renders on both the <li> and the inner <a> element
83+
const aboutEntry = page.getByRole('link', { name: "About & What's new" })
84+
await aboutEntry.click()
85+
86+
// The wizard should open again via the app-menu.ts handler
87+
await expect(wizard).toBeVisible()
88+
} finally {
89+
await deleteUser(user.userId)
90+
}
91+
})
92+
93+
test('can be navigated and closed', async ({ page }) => {
94+
const user = await createRandomUser()
95+
96+
try {
97+
await login(page, user.userId, user.password)
98+
99+
const wizard = page.locator('.first-run-wizard')
100+
await expect(wizard).toBeVisible()
101+
102+
// Skip the intro animation as soon as the Skip button appears (~2s)
103+
const skipButton = wizard.getByRole('button', { name: 'Skip' })
104+
await expect(skipButton).toBeVisible({ timeout: 5_000 })
105+
await skipButton.click()
106+
107+
// The slideshow is now shown with the Close button always visible.
108+
const closeButton = wizard.getByRole('button', { name: 'Close' })
109+
await expect(closeButton).toBeVisible()
110+
111+
// Navigate through all pages by repeatedly clicking the last button
112+
// (always the forward/primary navigation button) until the final
113+
// page's "Get started!" button is reached.
114+
const getStartedButton = wizard.getByRole('button', { name: 'Get started!' })
115+
116+
while (!(await getStartedButton.isVisible())) {
117+
// The last button in DOM order is always the last navigation button
118+
// in the button_wrapper (after the Close and Back buttons)
119+
await wizard.getByRole('button').last().click()
120+
}
121+
122+
// Clicking "Get started!" on the last page closes the wizard
123+
await getStartedButton.click()
124+
125+
// The wizard should no longer be visible after closing
126+
await expect(wizard).not.toBeVisible()
127+
} finally {
128+
await deleteUser(user.userId)
129+
}
130+
})
131+
})

e2e/settings.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from '@playwright/test'
7+
import { createRandomUser, deleteUser, login } from './support/utils.ts'
8+
import type { User } from './support/utils.ts'
9+
10+
test.describe('Settings page', () => {
11+
let user: User
12+
13+
test.beforeAll(async () => {
14+
user = await createRandomUser()
15+
})
16+
17+
test.afterAll(async () => {
18+
await deleteUser(user.userId)
19+
})
20+
21+
test.beforeEach(async ({ page }) => {
22+
await login(page, user.userId, user.password)
23+
// Navigate to the personal settings page for sync clients
24+
await page.goto('/settings/user/sync-clients')
25+
})
26+
27+
test('shows the sync clients section', async ({ page }) => {
28+
// The SettingsClients section heading should be visible
29+
await expect(
30+
page.getByRole('heading', { name: 'Get the apps to sync your files' }),
31+
).toBeVisible()
32+
})
33+
34+
test('shows the connected apps section', async ({ page }) => {
35+
// The SettingsApps section should be visible
36+
const heading = page.getByRole('heading', { name: /Connect other apps to/i })
37+
await expect(heading).toBeVisible()
38+
})
39+
40+
test('shows the server address section', async ({ page }) => {
41+
// The SettingsServer section should be visible
42+
await expect(
43+
page.getByRole('heading', { name: 'Server address' }),
44+
).toBeVisible()
45+
})
46+
})

e2e/start-nextcloud-server.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import {
7+
configureNextcloud,
8+
startNextcloud,
9+
stopNextcloud,
10+
waitOnNextcloud,
11+
} from '@nextcloud/e2e-test-server/docker'
12+
import { readFileSync } from 'fs'
13+
import { execSync } from 'node:child_process'
14+
15+
async function start() {
16+
const appinfo = readFileSync('appinfo/info.xml').toString()
17+
const maxVersion = appinfo.match(
18+
/<nextcloud min-version="\d+" max-version="(\d\d+)" \/>/,
19+
)?.[1]
20+
21+
let branch = 'master'
22+
if (maxVersion) {
23+
try {
24+
const refs = execSync('git ls-remote --refs').toString('utf-8')
25+
branch = refs.includes(`refs/heads/stable${maxVersion}`)
26+
? `stable${maxVersion}`
27+
: branch
28+
} catch {
29+
// If git command fails, fall back to 'master'
30+
}
31+
}
32+
33+
return await startNextcloud(branch, true, {
34+
exposePort: 8089,
35+
})
36+
}
37+
38+
async function stop() {
39+
process.stderr.write('Stopping Nextcloud server…\n')
40+
await stopNextcloud()
41+
process.exit(0)
42+
}
43+
44+
process.on('SIGTERM', stop)
45+
process.on('SIGINT', stop)
46+
47+
// Start the Nextcloud docker container
48+
const ip = await start()
49+
await waitOnNextcloud(ip)
50+
await configureNextcloud(['firstrunwizard'])
51+
52+
// Idle to keep the process alive until a SIGTERM/SIGINT signal is received
53+
// (sent by Playwright's gracefulShutdown when tests finish)
54+
while (true) {
55+
await new Promise((resolve) => setTimeout(resolve, 5000))
56+
}

0 commit comments

Comments
 (0)