diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6988a457c3..7664de084d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -227,12 +227,95 @@ jobs: env: CI: true + matrix: + runs-on: ubuntu-latest-low + outputs: + matrix: ${{ steps.versions.outputs.sparse-matrix }} + steps: + - name: Checkout app + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 + with: + matrix: '{"node-versions": ["20"]}' + + frontend-e2e-tests: + runs-on: ubuntu-latest + name: Front-end E2E tests + needs: matrix + strategy: + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + steps: + - name: Set up Nextcloud env + uses: ChristophWurst/setup-nextcloud@fc0790385c175d97e88a7cb0933490de6e990374 # v0.3.2 + with: + nextcloud-version: ${{ matrix.server-versions }} + php-version: ${{ matrix.php-versions }} + node-version: ${{ matrix.node-versions }} + install: true + - name: Configure Nextcloud for testing + run: | + php -f nextcloud/occ config:system:set debug --type=bool --value=true + php -f nextcloud/occ config:system:set overwriteprotocol --value=https + php -f nextcloud/occ config:system:set overwritehost --value=localhost + php -f nextcloud/occ config:system:set overwrite.cli.url --value=https://localhost + php -f nextcloud/occ config:system:set app.mail.debug --type=bool --value=true + php -f nextcloud/occ config:system:set app.mail.verify-tls-peer --type=bool --value=false + - name: Check out the app + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + with: + path: nextcloud/apps/mail + - name: Install php dependencies + working-directory: nextcloud/apps/mail + run: composer install + - name: Install the app + run: php -f nextcloud/occ app:enable mail + - name: Set up node ${{ matrix.node-version }} + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 + with: + node-version: ${{ matrix.node-versions }} + - name: Install npm dependencies + working-directory: nextcloud/apps/mail + run: npm ci + - name: Build frontend + working-directory: nextcloud/apps/mail + run: npm run build + - name: Install stunnel (tiny https proxy) + run: sudo apt-get install -y stunnel + - name: Start php server and https proxy + working-directory: nextcloud + run: | + openssl req -new -x509 -days 365 -nodes -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" -out stunnel.pem -keyout stunnel.pem + php -S 127.0.0.1:8080 & + sudo stunnel3 -p stunnel.pem -d 443 -r 8080 + - name: Test https access + run: curl --insecure -Li https://localhost + - name: Install Playwright browsers + working-directory: nextcloud/apps/mail + run: npx playwright install --with-deps chromium + - name: Run Playwright tests + working-directory: nextcloud/apps/mail + run: DEBUG=pw:api npx playwright test + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: playwright-report-${{ github.event.number }}-nc${{ matrix.server-versions }}-php${{ matrix.php-versions }}-node${{ matrix.node-versions }} + path: nextcloud/apps/mail/playwright-report/ + retention-days: 14 + - name: Print server logs + if: always() + run: cat nextcloud/data/nextcloud.log* + env: + CI: true + summary: runs-on: ubuntu-latest needs: - unit-tests - integration-tests - frontend-unit-test + - frontend-e2e-tests if: always() @@ -245,3 +328,5 @@ jobs: run: if ${{ needs.integration-tests.result != 'success' && needs.integration-tests.result != 'skipped' }}; then exit 1; fi - name: Frontend unit test status run: if ${{ needs.frontend-unit-test.result != 'success' && needs.frontend-unit-test.result != 'skipped' }}; then exit 1; fi + - name: Frontend E2E test status + run: if ${{ needs.frontend-e2e-tests.result != 'success' && needs.frontend-e2e-tests.result != 'skipped' }}; then exit 1; fi diff --git a/.gitignore b/.gitignore index fa403cf86a..91c74878f8 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,8 @@ tests/.phpunit.result.cache # PhpStorm .idea + +# Playwright +/playwright +/playwright-report +/test-results diff --git a/package-lock.json b/package-lock.json index 395dbfa258..1e4f76a61c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/eslint-config": "^8.4.2", "@nextcloud/eslint-plugin": "^2.2.1", + "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", "@vue/test-utils": "^1.3.6", "@vue/vue2-jest": "^29.2.6", @@ -6207,6 +6208,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@riophae/vue-treeselect": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@riophae/vue-treeselect/-/vue-treeselect-0.4.0.tgz", @@ -17382,6 +17399,53 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "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/pofile": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/pofile/-/pofile-1.1.4.tgz", diff --git a/package.json b/package.json index d9bf05e1ce..666f32b1d8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "lint": "eslint --ext .js,.vue --ignore-pattern tests src", "lint:fix": "eslint --ext .js,.vue --ignore-pattern tests src --fix", "test:unit": "jest src/tests/unit", - "test:unit:watch": "jest --watch src/tests/unit" + "test:unit:watch": "jest --watch src/tests/unit", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@ckeditor/ckeditor5-alignment": "44.3.0", @@ -108,6 +110,7 @@ "@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/eslint-config": "^8.4.2", "@nextcloud/eslint-plugin": "^2.2.1", + "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", "@vue/test-utils": "^1.3.6", "@vue/vue2-jest": "^29.2.6", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000000..ee7f05ab79 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,36 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +const { defineConfig, devices } = require('@playwright/test') +const path = require('path') + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './src/tests/e2e', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [ + ['list'], + ['html'], + ], + use: { + baseURL: 'https://localhost', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + testMatch: '**/*.spec.js', + use: { + ...devices['Desktop Chrome'], + ignoreHTTPSErrors: true, + }, + }, + ], +}) diff --git a/src/tests/e2e/login.js b/src/tests/e2e/login.js new file mode 100644 index 0000000000..84fe088605 --- /dev/null +++ b/src/tests/e2e/login.js @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Log in as the admin user. + * This method is best used in a beforeEach hook. + * + * @param {import("@playwright/test").Page} page The page object to use + * @return {Promise} + */ +export async function login(page) { + await page.goto('./index.php/login') + await page.locator('#user').fill('admin') + await page.locator('#password').fill('admin') + await page.locator('#password').press('Enter') + + // Wait for login to finish + await page.waitForURL('**/apps/**') +} diff --git a/src/tests/e2e/mail-e2e.spec.js b/src/tests/e2e/mail-e2e.spec.js new file mode 100644 index 0000000000..409825fe92 --- /dev/null +++ b/src/tests/e2e/mail-e2e.spec.js @@ -0,0 +1,40 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { test, expect } from '@playwright/test' +import { login } from './login.js' + +test.beforeEach(async ({ page }) => { + await login(page) +}) + +// we might need these in the future + +/* test('open Mail app and load inbox', async ({ page }) => { + await page.goto('./index.php/apps/mail') + + // Assert that the mail interface is rendered + await expect(page.getByRole('complementary', { name: 'New message' }).first()).toBeVisible() + await expect(page.getByRole('link', { name: 'Priority inbox' })).toBeVisible() + await expect(page.getByRole('link', { name: 'All inboxes' })).toBeVisible() +}) */ + +/* test('compose and send an email', async ({ page }) => { + await page.goto('./index.php/apps/mail') + + // Open compose modal + await page.getByRole('complementary', { name: 'New message' }).first().click() + + // Fill in email fields + await page.getByRole('combobox', { name: 'Select recipient' }).fill('test@example.com') + await page.getByRole('textbox', { name: 'Subject' }).fill('Test Subject') + await page.getByRole('textbox', { name: 'Rich Text Editor' }).fill('This is a test message.') + + // Click send + await page.getByRole('button', { name: 'Send' }).click() + + // Assert that a confirmation or notification appears + await expect(page.getByText('Message sent')).toBeVisible() +}) */