From 0fc50c46f7847b451f9f6b932744d3afa515836d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:41:07 -0300 Subject: [PATCH 01/19] chore: add @playwright/test dependency and e2e scripts Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- package-lock.json | 64 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 7f89142e5e..43b638a11d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "@nextcloud/stylelint-config": "^3.2.1", "@nextcloud/vite-config": "^2.5.2", "@pinia/testing": "^1.0.3", + "@playwright/test": "^1.58.1", "@testing-library/dom": "^10.4.1", "@testing-library/vue": "^8.1.0", "@vitejs/plugin-vue": "^6.0.3", @@ -3256,6 +3257,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@redocly/ajv": { "version": "8.17.2", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.2.tgz", @@ -12648,6 +12665,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "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", diff --git a/package.json b/package.json index 3c15427c25..b6feac9890 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "stylelint:fix": "stylelint src/**/*.scss src/**/*.vue --fix", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui --ui-host=0.0.0.0 --ui-port=9323", + "test:e2e:report": "playwright show-report" }, "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -75,6 +78,7 @@ "npm": "^11.3.0" }, "devDependencies": { + "@playwright/test": "^1.58.1", "@nextcloud/browserslist-config": "^3.1.2", "@nextcloud/eslint-config": "^8.4.2", "@nextcloud/stylelint-config": "^3.2.1", From 0eeaf857e204992cb159acf6fb58e17e9bf7820d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:41:17 -0300 Subject: [PATCH 02/19] chore: add playwright configuration Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright.config.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 playwright.config.ts diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..55e760738b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { defineConfig, devices } from '@playwright/test' + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './playwright', + + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'github' : 'list', + /* Default timeout for each test (60 seconds) */ + timeout: 60000, + + /* Shared settings for all the projects below. */ + use: { + /* Base URL to use in actions like `await page.goto('./apps/libresign')`. */ + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'https://localhost', + + /* Ignore HTTPS errors on local self-signed certificates */ + ignoreHTTPSErrors: true, + + /* Collect trace when retrying the failed test. */ + trace: 'on-first-retry', + + /* Screenshot on failure */ + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}) From 6e65c61d0d8330ef87f242e548ad78cc9f1c5c65 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:42:12 -0300 Subject: [PATCH 03/19] test(e2e): add Nextcloud login helper for Playwright Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/support/nc-login.ts | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 playwright/support/nc-login.ts diff --git a/playwright/support/nc-login.ts b/playwright/support/nc-login.ts new file mode 100644 index 0000000000..f26b58494e --- /dev/null +++ b/playwright/support/nc-login.ts @@ -0,0 +1,62 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { APIRequestContext } from '@playwright/test' + +/** + * Login to Nextcloud via API (no browser form involved). + * + * This mirrors the approach used by @nextcloud/e2e-test-server: + * 1. GET /csrftoken → obtain a CSRF token + * 2. POST /login → authenticate using form data + Origin header + * 3. GET /apps/files → validate the session is active + * + * Using page.request keeps the cookies on the browser context, so every + * subsequent page.goto() will already be authenticated. + * + * @param request - The Playwright APIRequestContext (use `page.request`) + * @param user - Account name / login + * @param password - Account password + */ +export async function login( + request: APIRequestContext, + user: string, + password: string, +): Promise { + const tokenResponse = await request.get('./csrftoken', { + failOnStatusCode: true, + }) + + const { token: requesttoken } = await tokenResponse.json() as { token: string } + + // Strip everything from "index.php" onward so we get the bare origin + const origin = tokenResponse.url().replace(/index\.php.*/, '') + + const loginResponse = await request.post('./login', { + form: { + user, + password, + requesttoken, + }, + headers: { + Origin: origin, + }, + // The 303 redirect points to the configured public URL (e.g. https://localhost) + // which may not be reachable inside the container. We don't need to follow it — + // the session cookies are already set at this point. + maxRedirects: 0, + failOnStatusCode: false, + }) + + // The Nextcloud login sets x-user-id on success (even on the 303 response). + if (!loginResponse.headers()['x-user-id']) { + throw new Error(`Login failed for user "${user}": no x-user-id header in response (status ${loginResponse.status()})`) + } + + // Confirm the session is valid + await request.get('./apps/files', { + failOnStatusCode: true, + }) +} From 778fd0ef3086dc6a8bb9e3c2c38d817bbba8e2c2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:42:17 -0300 Subject: [PATCH 04/19] test(e2e): add OCS provisioning helpers for Playwright Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/support/nc-provisioning.ts | 172 ++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 playwright/support/nc-provisioning.ts diff --git a/playwright/support/nc-provisioning.ts b/playwright/support/nc-provisioning.ts new file mode 100644 index 0000000000..05d225c396 --- /dev/null +++ b/playwright/support/nc-provisioning.ts @@ -0,0 +1,172 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * Helpers for configuring the Nextcloud environment from Playwright tests, + * equivalent to Behat's OCC/OCS helpers. + * + * All operations go through the Nextcloud OCS Provisioning API and are + * performed as admin. No Docker or OCC CLI access is needed. + */ + +import type { APIRequestContext } from '@playwright/test' + +type OcsResponse = { + ocs: { + meta: { status: string; statuscode: number; message: string } + data: T + } +} + +async function ocsRequest( + request: APIRequestContext, + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + path: string, + adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', + adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', + body?: Record, + jsonBody?: unknown, +): Promise { + const url = `./ocs/v2.php${path}` + const auth = 'Basic ' + Buffer.from(`${adminUser}:${adminPassword}`).toString('base64') + const headers: Record = { + 'OCS-ApiRequest': 'true', + Accept: 'application/json', + Authorization: auth, + } + if (jsonBody !== undefined) { + headers['Content-Type'] = 'application/json' + } + const response = await request[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'](url, { + headers, + ...(jsonBody !== undefined + ? { data: JSON.stringify(jsonBody) } + : body !== undefined ? { form: body } : {}), + failOnStatusCode: false, + }) + + if (!response.ok() && response.status() !== 404) { + throw new Error(`OCS request failed: ${method} ${path} → ${response.status()} ${await response.text()}`) + } + + return response.json() as Promise +} + +// --------------------------------------------------------------------------- +// Users +// --------------------------------------------------------------------------- + +/** + * Creates a user if it doesn't exist yet. + * Equivalent to Behat: `user :user exists` + */ +export async function ensureUserExists( + request: APIRequestContext, + userId: string, + password = '123456', +): Promise { + const check = await ocsRequest(request, 'GET', `/cloud/users/${userId}`) + if (check.ocs.meta.statuscode === 200) { + return + } + const create = await ocsRequest(request, 'POST', '/cloud/users', undefined, undefined, { + userid: userId, + password, + }) + if (create.ocs.meta.statuscode !== 200) { + throw new Error(`Failed to create user "${userId}": ${create.ocs.meta.message}`) + } +} + +/** + * Deletes a user. Silently succeeds if the user doesn't exist. + */ +export async function deleteUser( + request: APIRequestContext, + userId: string, +): Promise { + await ocsRequest(request, 'DELETE', `/cloud/users/${userId}`) +} + +// --------------------------------------------------------------------------- +// App config (equivalent to `occ config:app:set`) +// --------------------------------------------------------------------------- + +/** + * Sets an app config value. + * Equivalent to: `occ config:app:set --value=` + */ +export async function setAppConfig( + request: APIRequestContext, + appId: string, + key: string, + value: string, +): Promise { + const result = await ocsRequest( + request, + 'POST', + `/apps/provisioning_api/api/v1/config/apps/${appId}/${key}`, + undefined, + undefined, + { value }, + ) + if (result.ocs.meta.statuscode !== 200) { + throw new Error(`Failed to set app config ${appId}/${key}: ${result.ocs.meta.message}`) + } +} + +/** + * Deletes an app config value. + * Equivalent to: `occ config:app:delete ` + */ +export async function deleteAppConfig( + request: APIRequestContext, + appId: string, + key: string, +): Promise { + await ocsRequest(request, 'DELETE', `/apps/provisioning_api/api/v1/config/apps/${appId}/${key}`) +} + +// --------------------------------------------------------------------------- +// LibreSign-specific helpers +// --------------------------------------------------------------------------- + +type OpenSslCertNames = { + OU?: string | string[] + O?: string + C?: string + ST?: string + L?: string +} + +/** + * Configures the OpenSSL certificate engine. + * Equivalent to: `occ libresign:configure:openssl --cn=... --c=... ...` + */ +export async function configureOpenSsl( + request: APIRequestContext, + commonName: string, + names: OpenSslCertNames = {}, +): Promise { + const normalised: OpenSslCertNames = { ...names } + if (typeof normalised.OU === 'string') { + normalised.OU = [normalised.OU] + } + + const namesArray = (Object.entries(normalised) as [string, string | string[] | undefined][]) + .filter(([, val]) => val !== undefined) + .map(([id, value]) => ({ id, value })) + + const result = await ocsRequest( + request, + 'POST', + '/apps/libresign/api/v1/admin/certificate/openssl', + undefined, + undefined, + undefined, + { rootCert: { commonName, names: namesArray } }, + ) + if (result.ocs.meta.statuscode !== 200) { + throw new Error(`Failed to configure OpenSSL: ${result.ocs.meta.message}`) + } +} From d7db93886b0428efb13e21a897edac8755ab07c6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:42:23 -0300 Subject: [PATCH 05/19] test(e2e): add sign herself with click-to-sign spec Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../sign-herself-with-click-to-sign.spec.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 playwright/e2e/sign-herself-with-click-to-sign.spec.ts diff --git a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts new file mode 100644 index 0000000000..2d27edf44c --- /dev/null +++ b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts @@ -0,0 +1,55 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '@playwright/test' +import { login } from '../support/nc-login' +import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning' + +test('sign herself with click to sign', async ({ page }) => { + await login( + page.request, + process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', + process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', + ) + + await configureOpenSsl(page.request, 'LibreSign Test', { + C: 'BR', + OU: ['Organization Unit'], + ST: 'Rio de Janeiro', + O: 'LibreSign', + L: 'Rio de Janeiro', + }) + + await setAppConfig( + page.request, + 'libresign', + 'identify_methods', + JSON.stringify([ + { name: 'account', enabled: true, mandatory: true, signatureMethods: { clickToSign: { enabled: true } } }, + { name: 'email', enabled: false, mandatory: false }, + ]), + ) + + await page.goto('./apps/libresign'); + await page.getByRole('button', { name: 'Upload from URL' }).click(); + await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf'); + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByRole('button', { name: 'Add signer' }).click(); + await page.getByPlaceholder('Account').fill('admin'); + await page.getByText('admin@email.tld').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Request signatures' }).click(); + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByRole('button', { name: 'Sign document' }).click(); + await page.getByRole('button', { name: 'Sign the document.' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.waitForURL('**/validation/**'); + await expect(page.getByText('This document is valid')).toBeVisible(); + await page.getByRole('button', { name: 'Expand details' }).click(); + await page.getByRole('button', { name: 'Expand validation status', exact: true }).click(); + await expect(page.getByRole('link', { name: 'Document integrity verified' })).toBeVisible(); + await page.getByRole('button', { name: 'Expand document certification', exact: true }).click(); + await expect(page.getByRole('link', { name: 'Document has not been' })).toBeVisible(); +}); From 048f16b8d956c27f3c556a8ddf38fd695d30b2eb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:42:28 -0300 Subject: [PATCH 06/19] ci: add Playwright E2E workflow Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 219 +++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 .github/workflows/playwright.yml diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000..cf2ea81536 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Playwright Tests + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: playwright-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest + + outputs: + src: ${{ steps.changes.outputs.src }} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/playwright.yml' + - 'appinfo/**' + - 'lib/**' + - 'src/**' + - 'templates/**' + - 'playwright/**' + - 'playwright.config.ts' + - 'package.json' + - 'package-lock.json' + + playwright: + runs-on: ubuntu-latest + timeout-minutes: 60 + + needs: [changes] + if: needs.changes.outputs.src != 'false' + + name: Playwright E2E Tests + + services: + mailpit: + image: axllent/mailpit + ports: + - 8025:8025/tcp + - 1025:1025/tcp + + steps: + - name: Set app env + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Install system dependencies + run: sudo apt update && sudo apt install poppler-utils + + - name: Checkout server + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: main + + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: true + path: apps/${{ env.APP_NAME }} + + - name: Get php version + id: php_versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 + + - name: Set up php ${{ steps.php_versions.outputs.php-min }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 + with: + php-version: ${{ steps.php_versions.outputs.php-min }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, sqlite, pdo_sqlite, xmlreader, xmlwriter, zip, zlib + coverage: none + ini-file: development + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check composer file existence + id: check_composer + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 + with: + files: apps/${{ env.APP_NAME }}/composer.json + + - name: Set up composer dependencies + if: steps.check_composer.outputs.files_exists == 'true' + working-directory: apps/${{ env.APP_NAME }} + run: | + composer remove nextcloud/ocp --dev --no-scripts + composer install --no-dev + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + path: apps/${{ env.APP_NAME }} + fallbackNode: '^24' + fallbackNpm: '^11' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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 node dependencies & build app + working-directory: apps/${{ env.APP_NAME }} + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + npm ci + TESTING=true npm run build --if-present + + - name: Set up Nextcloud + run: | + sudo echo "127.0.0.1 mailpit" | sudo tee -a /etc/hosts + mkdir data + ./occ maintenance:install \ + --verbose \ + --database=sqlite \ + --admin-user admin \ + --admin-pass admin + ./occ --version + ./occ app:enable --force ${{ env.APP_NAME }} + git clone --depth 1 -b main https://github.com/nextcloud/notifications apps/notifications + composer --working-dir=apps/notifications install --no-dev + ./occ app:enable --force notifications + git clone --depth 1 -b main https://github.com/nextcloud/activity apps/activity + composer --working-dir=apps/activity install --no-dev + ./occ app:enable --force activity + ./occ config:system:set allow_local_remote_servers --value true --type boolean + ./occ config:system:set auth.bruteforce.protection.enabled --value false --type boolean + ./occ config:system:set ratelimit.protection.enabled --value false --type boolean + ./occ config:system:set mail_smtphost --value mailpit + ./occ config:system:set mail_smtpport --value 1025 --type integer + ./occ config:system:set overwrite.cli.url --value 'http://localhost:8080' + ./occ config:system:set overwritehost --value 'localhost:8080' + ./occ libresign:install --use-local-cert --java + ./occ libresign:install --use-local-cert --jsignpdf + ./occ libresign:install --use-local-cert --pdftk + ./occ libresign:configure:openssl \ + --cn="Common Name" \ + --c=BR \ + --ou="Organization Unit" \ + --st="Rio de Janeiro" \ + --o=LibreSign \ + --l="Rio de Janeiro" + + - name: Start PHP built-in server + run: | + php -S localhost:8080 & + # Wait for server to become available + timeout 30 bash -c 'until curl -s http://localhost:8080/status.php > /dev/null; do sleep 1; done' + echo "Nextcloud is ready at http://localhost:8080" + + - name: Install Playwright browsers + working-directory: apps/${{ env.APP_NAME }} + run: npx playwright install chromium --with-deps + + - name: Run Playwright tests + working-directory: apps/${{ env.APP_NAME }} + env: + PLAYWRIGHT_BASE_URL: http://localhost:8080 + NEXTCLOUD_ADMIN_USER: admin + NEXTCLOUD_ADMIN_PASSWORD: admin + run: npx playwright test + + - name: Upload Playwright report + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + if: always() + with: + name: playwright-report + path: apps/${{ env.APP_NAME }}/playwright-report/ + retention-days: 30 + + - name: Upload test results + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + if: always() + with: + name: playwright-test-results + path: apps/${{ env.APP_NAME }}/test-results/ + retention-days: 30 + + - name: Print Nextcloud logs + if: always() + run: cat data/nextcloud.log 2>/dev/null || echo "No Nextcloud logs found" + + summary: + permissions: + contents: none + runs-on: ubuntu-latest + needs: [changes, playwright] + + if: always() + + name: playwright-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.playwright.result != 'success' }}; then exit 1; fi From 584e26ce796deb764555782f7fc0c7f0c1a8cb6f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:44:10 -0300 Subject: [PATCH 07/19] chore: remove unecessary comment Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/support/nc-login.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/playwright/support/nc-login.ts b/playwright/support/nc-login.ts index f26b58494e..f97f681ac6 100644 --- a/playwright/support/nc-login.ts +++ b/playwright/support/nc-login.ts @@ -43,9 +43,6 @@ export async function login( headers: { Origin: origin, }, - // The 303 redirect points to the configured public URL (e.g. https://localhost) - // which may not be reachable inside the container. We don't need to follow it — - // the session cookies are already set at this point. maxRedirects: 0, failOnStatusCode: false, }) From ceb79c21fe5cddace4205296e059b955b73eece2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:49:04 -0300 Subject: [PATCH 08/19] fix: use matrix to identify server version Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index cf2ea81536..adc79cb5d1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -15,6 +15,20 @@ concurrency: cancel-in-progress: true jobs: + matrix: + runs-on: ubuntu-latest + outputs: + server-max: ${{ steps.versions.outputs.branches-max-list }} + steps: + - name: Checkout app + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 + changes: runs-on: ubuntu-latest @@ -42,9 +56,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 - needs: [changes] + needs: [matrix, changes] if: needs.changes.outputs.src != 'false' + strategy: + matrix: + server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + name: Playwright E2E Tests services: @@ -69,7 +87,7 @@ jobs: persist-credentials: false submodules: true repository: nextcloud/server - ref: main + ref: ${{ matrix.server-versions }} - name: Checkout app uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -208,7 +226,7 @@ jobs: permissions: contents: none runs-on: ubuntu-latest - needs: [changes, playwright] + needs: [matrix, changes, playwright] if: always() From 6071264ea41539ee05ff1428d0df1e4f02c68c92 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:51:15 -0300 Subject: [PATCH 09/19] fix(ci): pass app path to nextcloud-version-matrix action Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index adc79cb5d1..b68746ed4e 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -99,6 +99,8 @@ jobs: - name: Get php version id: php_versions uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 + with: + filename: apps/${{ env.APP_NAME }}/appinfo/info.xml - name: Set up php ${{ steps.php_versions.outputs.php-min }} uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 From 426b69a7b98443f27d777749164b46cee4c01d37 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:54:24 -0300 Subject: [PATCH 10/19] fix(ci): use matrix server-versions branch for notifications and activity clone Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index b68746ed4e..c063f92227 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -161,10 +161,10 @@ jobs: --admin-pass admin ./occ --version ./occ app:enable --force ${{ env.APP_NAME }} - git clone --depth 1 -b main https://github.com/nextcloud/notifications apps/notifications + git clone --depth 1 -b ${{ matrix.server-versions }} https://github.com/nextcloud/notifications apps/notifications composer --working-dir=apps/notifications install --no-dev ./occ app:enable --force notifications - git clone --depth 1 -b main https://github.com/nextcloud/activity apps/activity + git clone --depth 1 -b ${{ matrix.server-versions }} https://github.com/nextcloud/activity apps/activity composer --working-dir=apps/activity install --no-dev ./occ app:enable --force activity ./occ config:system:set allow_local_remote_servers --value true --type boolean From ad84fa8621ac89750af3f28f4b19d0a7bc2a14e2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:58:34 -0300 Subject: [PATCH 11/19] fix(ci): enable debug mode to allow --use-local-cert on libresign:install Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> fix(ci): use index.php as router for PHP built-in server Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index c063f92227..0aa8370336 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -174,6 +174,7 @@ jobs: ./occ config:system:set mail_smtpport --value 1025 --type integer ./occ config:system:set overwrite.cli.url --value 'http://localhost:8080' ./occ config:system:set overwritehost --value 'localhost:8080' + ./occ config:system:set debug --value true --type boolean ./occ libresign:install --use-local-cert --java ./occ libresign:install --use-local-cert --jsignpdf ./occ libresign:install --use-local-cert --pdftk @@ -187,7 +188,7 @@ jobs: - name: Start PHP built-in server run: | - php -S localhost:8080 & + php -S localhost:8080 index.php & # Wait for server to become available timeout 30 bash -c 'until curl -s http://localhost:8080/status.php > /dev/null; do sleep 1; done' echo "Nextcloud is ready at http://localhost:8080" From 8564886b751b8591c626c6b2bb61d45ca928345b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:12:53 -0300 Subject: [PATCH 12/19] fix(e2e): add direct LibreSign API helper and safe JSON parsing Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/support/nc-provisioning.ts | 44 +++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/playwright/support/nc-provisioning.ts b/playwright/support/nc-provisioning.ts index 05d225c396..0a3abef00d 100644 --- a/playwright/support/nc-provisioning.ts +++ b/playwright/support/nc-provisioning.ts @@ -49,7 +49,43 @@ async function ocsRequest( throw new Error(`OCS request failed: ${method} ${path} → ${response.status()} ${await response.text()}`) } - return response.json() as Promise + const text = await response.text() + if (!text) { + return { ocs: { meta: { status: 'ok', statuscode: response.status(), message: '' }, data: {} } } as OcsResponse + } + return JSON.parse(text) as OcsResponse +} + +async function libresignRequest( + request: APIRequestContext, + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + path: string, + adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', + adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', + jsonBody?: unknown, +): Promise { + const url = `.${path}` + const auth = 'Basic ' + Buffer.from(`${adminUser}:${adminPassword}`).toString('base64') + const headers: Record = { + Accept: 'application/json', + Authorization: auth, + } + if (jsonBody !== undefined) { + headers['Content-Type'] = 'application/json' + } + const response = await request[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'](url, { + headers, + ...(jsonBody !== undefined ? { data: JSON.stringify(jsonBody) } : {}), + failOnStatusCode: false, + }) + + if (!response.ok()) { + throw new Error(`LibreSign API request failed: ${method} ${path} → ${response.status()} ${await response.text()}`) + } + + const text = await response.text() + if (!text) return {} + return JSON.parse(text) } // --------------------------------------------------------------------------- @@ -157,16 +193,12 @@ export async function configureOpenSsl( .filter(([, val]) => val !== undefined) .map(([id, value]) => ({ id, value })) - const result = await ocsRequest( + await libresignRequest( request, 'POST', '/apps/libresign/api/v1/admin/certificate/openssl', undefined, undefined, - undefined, { rootCert: { commonName, names: namesArray } }, ) - if (result.ocs.meta.statuscode !== 200) { - throw new Error(`Failed to configure OpenSSL: ${result.ocs.meta.message}`) - } } From 438e7e267c22fbd7ce29c5919280364ebff26934 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:21:21 -0300 Subject: [PATCH 13/19] fix(e2e): use ocsRequest for openssl endpoint via ocs/v2.php prefix Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/support/nc-provisioning.ts | 38 ++++----------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/playwright/support/nc-provisioning.ts b/playwright/support/nc-provisioning.ts index 0a3abef00d..794b2ceb6b 100644 --- a/playwright/support/nc-provisioning.ts +++ b/playwright/support/nc-provisioning.ts @@ -56,38 +56,6 @@ async function ocsRequest( return JSON.parse(text) as OcsResponse } -async function libresignRequest( - request: APIRequestContext, - method: 'GET' | 'POST' | 'PUT' | 'DELETE', - path: string, - adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', - adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', - jsonBody?: unknown, -): Promise { - const url = `.${path}` - const auth = 'Basic ' + Buffer.from(`${adminUser}:${adminPassword}`).toString('base64') - const headers: Record = { - Accept: 'application/json', - Authorization: auth, - } - if (jsonBody !== undefined) { - headers['Content-Type'] = 'application/json' - } - const response = await request[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete'](url, { - headers, - ...(jsonBody !== undefined ? { data: JSON.stringify(jsonBody) } : {}), - failOnStatusCode: false, - }) - - if (!response.ok()) { - throw new Error(`LibreSign API request failed: ${method} ${path} → ${response.status()} ${await response.text()}`) - } - - const text = await response.text() - if (!text) return {} - return JSON.parse(text) -} - // --------------------------------------------------------------------------- // Users // --------------------------------------------------------------------------- @@ -193,12 +161,16 @@ export async function configureOpenSsl( .filter(([, val]) => val !== undefined) .map(([id, value]) => ({ id, value })) - await libresignRequest( + const result = await ocsRequest( request, 'POST', '/apps/libresign/api/v1/admin/certificate/openssl', undefined, undefined, + undefined, { rootCert: { commonName, names: namesArray } }, ) + if (result.ocs.meta.statuscode !== 200) { + throw new Error(`Failed to configure OpenSSL: ${result.ocs.meta.message}`) + } } From bbed687e8fc4544ad74a443ecd59813e404a8d57 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:08:06 -0300 Subject: [PATCH 14/19] fix: run into GitHub Action test: add router script for E2E server ci: use router script for PHP built-in server with correct document root fix: cs build: ignore playwright test artifacts test(e2e): wait for Vue app to load before running tests ci(e2e): add debug steps to check build artifacts and server status test(e2e): remove redundant wait - rely on Playwright auto-waiting ci(e2e): add more debug checks for app page and enablement status - Check what HTML is returned when curling /apps/libresign - Verify if app is enabled with occ app:list test(e2e): add debug logging to investigate CI timeout - Capture browser console logs - Log page URL, title, and HTML before button click - Take screenshot for visual debugging - Remove workaround navigation to /apps/files (works fine locally) Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 19 ++++++++- .gitignore | 1 + .../sign-herself-with-click-to-sign.spec.ts | 14 ++++++- playwright/router.php | 42 +++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 playwright/router.php diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 0aa8370336..213fa3a051 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -188,11 +188,28 @@ jobs: - name: Start PHP built-in server run: | - php -S localhost:8080 index.php & + php -S localhost:8080 -t . apps/${{ env.APP_NAME }}/playwright/router.php & # Wait for server to become available timeout 30 bash -c 'until curl -s http://localhost:8080/status.php > /dev/null; do sleep 1; done' echo "Nextcloud is ready at http://localhost:8080" + - name: Debug - Check if build artifacts exist + run: | + echo "Checking for LibreSign JS build artifacts..." + ls -lah apps/${{ env.APP_NAME }}/js/ || echo "js/ directory not found" + echo "---" + echo "Testing status endpoint:" + curl -v http://localhost:8080/status.php + echo "---" + echo "Testing index endpoint:" + curl -I http://localhost:8080/index.php + echo "---" + echo "Testing app endpoint (should load HTML with JS):" + curl -s http://localhost:8080/apps/libresign | head -100 + echo "---" + echo "Checking if app is enabled:" + php occ app:list | grep libresign + - name: Install Playwright browsers working-directory: apps/${{ env.APP_NAME }} run: npx playwright install chromium --with-deps diff --git a/.gitignore b/.gitignore index 24e5eff10f..4ee4c89951 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ node_modules/ /lib/Vendor/ /coverage /dist/ +/test-results/ diff --git a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts index 2d27edf44c..3729536cfb 100644 --- a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts +++ b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts @@ -32,7 +32,19 @@ test('sign herself with click to sign', async ({ page }) => { ]), ) - await page.goto('./apps/libresign'); + // Capture console logs to debug + page.on('console', msg => console.log('BROWSER CONSOLE:', msg.type(), msg.text())) + + await page.goto('./apps/libresign') + + // Debug: capture page state before looking for button + console.log('Page URL:', page.url()) + console.log('Page title:', await page.title()) + await page.screenshot({ path: 'debug-before-click.png', fullPage: true }) + const bodyHTML = await page.evaluate(() => document.body.innerHTML) + console.log('Body HTML length:', bodyHTML.length) + console.log('Body HTML preview:', bodyHTML.substring(0, 500)) + await page.getByRole('button', { name: 'Upload from URL' }).click(); await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf'); await page.getByRole('button', { name: 'Send' }).click(); diff --git a/playwright/router.php b/playwright/router.php new file mode 100644 index 0000000000..616eec675b --- /dev/null +++ b/playwright/router.php @@ -0,0 +1,42 @@ + Date: Thu, 26 Feb 2026 12:33:41 -0300 Subject: [PATCH 15/19] ci(e2e): add debug logging to router.php to diagnose path issues Log REQUEST_URI, URI, and dispatch info to stderr to understand why paths are being duplicated or misrouted in CI environment. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/router.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/playwright/router.php b/playwright/router.php index 616eec675b..e590870c16 100644 --- a/playwright/router.php +++ b/playwright/router.php @@ -16,6 +16,15 @@ $uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); $file = $rootDir . $uri; +// Debug: log all requests (to stderr so it shows in PHP output) +error_log(sprintf( + '[ROUTER] REQUEST_URI=%s | URI=%s | SCRIPT_NAME=%s | file_exists=%d', + $_SERVER['REQUEST_URI'], + $uri, + $_SERVER['SCRIPT_NAME'] ?? 'N/A', + file_exists($file) ? 1 : 0 +)); + // Serve static files as-is (except PHP files) if ($uri !== '/' && file_exists($file) && !is_dir($file) && pathinfo($uri, PATHINFO_EXTENSION) !== 'php') { return false; @@ -26,6 +35,12 @@ $_SERVER['SCRIPT_FILENAME'] = $rootDir . $script; $_SERVER['PHP_SELF'] = $script; $_SERVER['PATH_INFO'] = substr($uri, strlen($script)) ?: ''; + error_log(sprintf( + '[ROUTER DISPATCH] SCRIPT=%s | PATH_INFO=%s | SCRIPT_FILENAME=%s', + $script, + $_SERVER['PATH_INFO'] ?? 'empty', + $_SERVER['SCRIPT_FILENAME'] + )); require $rootDir . $script; }; From 850d4ef124d8d41006a1cb209572369bf4252b17 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:47:09 -0300 Subject: [PATCH 16/19] test(e2e): debug: log URL being constructed before page.goto Add explicit URL construction logging to see what path is being used. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/e2e/sign-herself-with-click-to-sign.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts index 3729536cfb..9d34286f79 100644 --- a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts +++ b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts @@ -34,17 +34,17 @@ test('sign herself with click to sign', async ({ page }) => { // Capture console logs to debug page.on('console', msg => console.log('BROWSER CONSOLE:', msg.type(), msg.text())) - - await page.goto('./apps/libresign') - - // Debug: capture page state before looking for button + + // Debug: log the URL being constructed before goto + const testUrl = new URL('./apps/libresign', 'http://localhost:8080').toString() + console.log('Test URL constructed:', testUrl) console.log('Page URL:', page.url()) console.log('Page title:', await page.title()) await page.screenshot({ path: 'debug-before-click.png', fullPage: true }) const bodyHTML = await page.evaluate(() => document.body.innerHTML) console.log('Body HTML length:', bodyHTML.length) console.log('Body HTML preview:', bodyHTML.substring(0, 500)) - + await page.getByRole('button', { name: 'Upload from URL' }).click(); await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf'); await page.getByRole('button', { name: 'Send' }).click(); From f889aedb0daca75767d1ee93eb07bf3c3b4f6436 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:08:08 -0300 Subject: [PATCH 17/19] fix(e2e): set front_controller_active so Nextcloud generates clean URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without Apache/mod_rewrite, Nextcloud redirects /apps/libresign to /index.php/apps/libresign. The Vue Router then builds relative paths from /index.php/, causing URL duplication and a broken app state. PHP's built-in server reads the front_controller_active env variable (see lib/private/URLGenerator.php). When true, Nextcloud generates clean URLs without the index.php prefix — the same behavior that Apache's .htaccess RewriteRule provides — and our router.php already handles routing those clean URLs to index.php. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 21 +++---------------- .../sign-herself-with-click-to-sign.spec.ts | 14 +------------ playwright/router.php | 15 ------------- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 213fa3a051..7feaaa6e7f 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -188,28 +188,13 @@ jobs: - name: Start PHP built-in server run: | - php -S localhost:8080 -t . apps/${{ env.APP_NAME }}/playwright/router.php & + # front_controller_active tells Nextcloud to generate clean URLs (without index.php prefix) + # This mirrors what Apache mod_rewrite does via .htaccess RewriteRule + front_controller_active=true php -S localhost:8080 -t . apps/${{ env.APP_NAME }}/playwright/router.php & # Wait for server to become available timeout 30 bash -c 'until curl -s http://localhost:8080/status.php > /dev/null; do sleep 1; done' echo "Nextcloud is ready at http://localhost:8080" - - name: Debug - Check if build artifacts exist - run: | - echo "Checking for LibreSign JS build artifacts..." - ls -lah apps/${{ env.APP_NAME }}/js/ || echo "js/ directory not found" - echo "---" - echo "Testing status endpoint:" - curl -v http://localhost:8080/status.php - echo "---" - echo "Testing index endpoint:" - curl -I http://localhost:8080/index.php - echo "---" - echo "Testing app endpoint (should load HTML with JS):" - curl -s http://localhost:8080/apps/libresign | head -100 - echo "---" - echo "Checking if app is enabled:" - php occ app:list | grep libresign - - name: Install Playwright browsers working-directory: apps/${{ env.APP_NAME }} run: npx playwright install chromium --with-deps diff --git a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts index 9d34286f79..58d1042c92 100644 --- a/playwright/e2e/sign-herself-with-click-to-sign.spec.ts +++ b/playwright/e2e/sign-herself-with-click-to-sign.spec.ts @@ -32,19 +32,7 @@ test('sign herself with click to sign', async ({ page }) => { ]), ) - // Capture console logs to debug - page.on('console', msg => console.log('BROWSER CONSOLE:', msg.type(), msg.text())) - - // Debug: log the URL being constructed before goto - const testUrl = new URL('./apps/libresign', 'http://localhost:8080').toString() - console.log('Test URL constructed:', testUrl) - console.log('Page URL:', page.url()) - console.log('Page title:', await page.title()) - await page.screenshot({ path: 'debug-before-click.png', fullPage: true }) - const bodyHTML = await page.evaluate(() => document.body.innerHTML) - console.log('Body HTML length:', bodyHTML.length) - console.log('Body HTML preview:', bodyHTML.substring(0, 500)) - + await page.goto('./apps/libresign') await page.getByRole('button', { name: 'Upload from URL' }).click(); await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf'); await page.getByRole('button', { name: 'Send' }).click(); diff --git a/playwright/router.php b/playwright/router.php index e590870c16..616eec675b 100644 --- a/playwright/router.php +++ b/playwright/router.php @@ -16,15 +16,6 @@ $uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); $file = $rootDir . $uri; -// Debug: log all requests (to stderr so it shows in PHP output) -error_log(sprintf( - '[ROUTER] REQUEST_URI=%s | URI=%s | SCRIPT_NAME=%s | file_exists=%d', - $_SERVER['REQUEST_URI'], - $uri, - $_SERVER['SCRIPT_NAME'] ?? 'N/A', - file_exists($file) ? 1 : 0 -)); - // Serve static files as-is (except PHP files) if ($uri !== '/' && file_exists($file) && !is_dir($file) && pathinfo($uri, PATHINFO_EXTENSION) !== 'php') { return false; @@ -35,12 +26,6 @@ $_SERVER['SCRIPT_FILENAME'] = $rootDir . $script; $_SERVER['PHP_SELF'] = $script; $_SERVER['PATH_INFO'] = substr($uri, strlen($script)) ?: ''; - error_log(sprintf( - '[ROUTER DISPATCH] SCRIPT=%s | PATH_INFO=%s | SCRIPT_FILENAME=%s', - $script, - $_SERVER['PATH_INFO'] ?? 'empty', - $_SERVER['SCRIPT_FILENAME'] - )); require $rootDir . $script; }; From e947c83f5a850a0ce2009e4c8be4365ad9f30a51 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:24:53 -0300 Subject: [PATCH 18/19] fix(e2e): set admin email so account autocomplete resolves admin@email.tld Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7feaaa6e7f..ce109d2180 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -185,6 +185,7 @@ jobs: --st="Rio de Janeiro" \ --o=LibreSign \ --l="Rio de Janeiro" + ./occ user:setting admin settings email admin@email.tld - name: Start PHP built-in server run: | From 675d181aa5fe13a6cb49606b199f96b97b04481a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:30:42 -0300 Subject: [PATCH 19/19] ci(e2e): split setup into nextcloud, app dependencies and libresign steps Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/playwright.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index ce109d2180..3678c02702 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -160,13 +160,19 @@ jobs: --admin-user admin \ --admin-pass admin ./occ --version - ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Install app dependencies + run: | git clone --depth 1 -b ${{ matrix.server-versions }} https://github.com/nextcloud/notifications apps/notifications composer --working-dir=apps/notifications install --no-dev ./occ app:enable --force notifications git clone --depth 1 -b ${{ matrix.server-versions }} https://github.com/nextcloud/activity apps/activity composer --working-dir=apps/activity install --no-dev ./occ app:enable --force activity + + - name: Set up LibreSign + run: | + ./occ app:enable --force ${{ env.APP_NAME }} ./occ config:system:set allow_local_remote_servers --value true --type boolean ./occ config:system:set auth.bruteforce.protection.enabled --value false --type boolean ./occ config:system:set ratelimit.protection.enabled --value false --type boolean