Skip to content
Open
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
109 changes: 109 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: E2E Tests

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

permissions:
contents: read

concurrency:
group: e2e-${{ 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/e2e.yml'
- 'appinfo/**'
- 'lib/**'
- 'src/**'
- 'templates/**'
- 'e2e/**'
- 'playwright.config.ts'
- 'package.json'
- 'package-lock.json'

e2e-tests:
runs-on: ubuntu-latest

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

name: Playwright E2E

steps:
- name: Checkout app
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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
run: npm ci

- name: Install Playwright browsers
run: npx playwright install chromium --with-deps

- name: Build assets
run: npm run build

- name: Run E2E tests
run: npm run test:e2e

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30

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

if: always()

name: e2e-summary

steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && needs.e2e-tests.result != 'success' }}; then exit 1; fi
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ vendor/
/.php-cs-fixer.cache
/tests/.phpunit.result.cache
.DS_Store

# Playwright e2e test artifacts
/playwright-report/
/test-results/
/blob-report/
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/firstrunwizard"

[[annotations]]
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"]
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"]
precedence = "aggregate"
SPDX-FileCopyrightText = "none"
SPDX-License-Identifier = "CC0-1.0"
Expand Down
131 changes: 131 additions & 0 deletions e2e/firstrunwizard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here and other files (year)

* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, test } from '@playwright/test'
import { createRandomUser, deleteUser, login, setUserPreference } from './support/utils.ts'

test.describe('First Run Wizard', () => {
test('opens automatically on first login', async ({ page }) => {
const user = await createRandomUser()

try {
await login(page, user.userId, user.password)

// The wizard is injected by first-run.ts and opened automatically
const wizard = page.locator('.first-run-wizard')
await expect(wizard).toBeVisible()

// For a brand-new user the intro animation (video) is shown first
await expect(wizard.locator('video')).toBeVisible()
} finally {
await deleteUser(user.userId)
}
})

test('opens when a new major version was shipped', async ({ page }) => {
const user = await createRandomUser()

try {
// Simulate a user who last saw wizard version 2.0.0.
// The stored version is greater than "1" (so changelogOnly = true)
// but less than the current CHANGELOG_VERSION (33.0.0),
// so the wizard is injected and opened again.
await setUserPreference(user.userId, 'firstrunwizard', 'show', '2.0.0')

await login(page, user.userId, user.password)

const wizard = page.locator('.first-run-wizard')
await expect(wizard).toBeVisible()

// The intro animation is always shown first; skip it to advance
// directly to the "What's new" page (changelog-only mode).
const skipButton = wizard.getByRole('button', { name: 'Skip' })
await expect(skipButton).toBeVisible({ timeout: 5_000 })
await skipButton.click()

// In changelog-only mode the wizard advances directly to the
// "What's new" page after the intro animation.
await expect(wizard).toContainText('New in Nextcloud Hub')
} finally {
await deleteUser(user.userId)
}
})

test('"About & What\'s new" menu entry reopens the wizard', async ({ page }) => {
const user = await createRandomUser()

try {
await login(page, user.userId, user.password)

// Close the first-run wizard that opens automatically on first login
const wizard = page.locator('.first-run-wizard')
await expect(wizard).toBeVisible()

// Skip the intro animation as soon as the Skip button appears (~2s)
const skipButton = wizard.getByRole('button', { name: 'Skip' })
await expect(skipButton).toBeVisible({ timeout: 5_000 })
await skipButton.click()

// Close the slideshow
const closeButton = wizard.getByRole('button', { name: 'Close' })
await expect(closeButton).toBeVisible()
await closeButton.click()
await expect(wizard).not.toBeVisible()

// Open the user settings menu to find the "About & What's new" entry
const userMenu = page.locator('[aria-controls="header-menu-user-menu"]')
await userMenu.click()

// Use the link role to avoid strict mode violation from the duplicate ID
// that Nextcloud renders on both the <li> and the inner <a> element
const aboutEntry = page.getByRole('link', { name: "About & What's new" })
await aboutEntry.click()

// The wizard should open again via the app-menu.ts handler
await expect(wizard).toBeVisible()
} finally {
await deleteUser(user.userId)
}
})

test('can be navigated and closed', async ({ page }) => {
const user = await createRandomUser()

try {
await login(page, user.userId, user.password)

const wizard = page.locator('.first-run-wizard')
await expect(wizard).toBeVisible()

// Skip the intro animation as soon as the Skip button appears (~2s)
const skipButton = wizard.getByRole('button', { name: 'Skip' })
await expect(skipButton).toBeVisible({ timeout: 5_000 })
await skipButton.click()

// The slideshow is now shown with the Close button always visible.
const closeButton = wizard.getByRole('button', { name: 'Close' })
await expect(closeButton).toBeVisible()

// Navigate through all pages by repeatedly clicking the last button
// (always the forward/primary navigation button) until the final
// page's "Get started!" button is reached.
const getStartedButton = wizard.getByRole('button', { name: 'Get started!' })

while (!(await getStartedButton.isVisible())) {
// The last button in DOM order is always the last navigation button
// in the button_wrapper (after the Close and Back buttons)
await wizard.getByRole('button').last().click()
}

// Clicking "Get started!" on the last page closes the wizard
await getStartedButton.click()

// The wizard should no longer be visible after closing
await expect(wizard).not.toBeVisible()
} finally {
await deleteUser(user.userId)
}
})
})
46 changes: 46 additions & 0 deletions e2e/settings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, test } from '@playwright/test'
import { createRandomUser, deleteUser, login } from './support/utils.ts'
import type { User } from './support/utils.ts'

test.describe('Settings page', () => {
let user: User

test.beforeAll(async () => {
user = await createRandomUser()
})

test.afterAll(async () => {
await deleteUser(user.userId)
})

test.beforeEach(async ({ page }) => {
await login(page, user.userId, user.password)
// Navigate to the personal settings page for sync clients
await page.goto('/settings/user/sync-clients')
})

test('shows the sync clients section', async ({ page }) => {
// The SettingsClients section heading should be visible
await expect(
page.getByRole('heading', { name: 'Get the apps to sync your files' }),
).toBeVisible()
})

test('shows the connected apps section', async ({ page }) => {
// The SettingsApps section should be visible
const heading = page.getByRole('heading', { name: /Connect other apps to/i })
await expect(heading).toBeVisible()
})

test('shows the server address section', async ({ page }) => {
// The SettingsServer section should be visible
await expect(
page.getByRole('heading', { name: 'Server address' }),
).toBeVisible()
})
})
56 changes: 56 additions & 0 deletions e2e/start-nextcloud-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {
configureNextcloud,
startNextcloud,
stopNextcloud,
waitOnNextcloud,
} from '@nextcloud/e2e-test-server/docker'
import { readFileSync } from 'fs'
import { execSync } from 'node:child_process'

async function start() {
const appinfo = readFileSync('appinfo/info.xml').toString()
const maxVersion = appinfo.match(
/<nextcloud min-version="\d+" max-version="(\d\d+)" \/>/,
)?.[1]

let branch = 'master'
if (maxVersion) {
try {
const refs = execSync('git ls-remote --refs').toString('utf-8')
branch = refs.includes(`refs/heads/stable${maxVersion}`)
? `stable${maxVersion}`
: branch
} catch {
// If git command fails, fall back to 'master'
}
}

return await startNextcloud(branch, true, {
exposePort: 8089,
})
}

async function stop() {
process.stderr.write('Stopping Nextcloud server…\n')
await stopNextcloud()
process.exit(0)
}

process.on('SIGTERM', stop)
process.on('SIGINT', stop)

// Start the Nextcloud docker container
const ip = await start()
await waitOnNextcloud(ip)
await configureNextcloud(['firstrunwizard'])

// Idle to keep the process alive until a SIGTERM/SIGINT signal is received
// (sent by Playwright's gracefulShutdown when tests finish)
while (true) {
await new Promise((resolve) => setTimeout(resolve, 5000))
}
Loading
Loading