From c8934f9bb0e09a5a9d6857c226eada23385ba184 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 14:27:50 +0200 Subject: [PATCH 01/11] test: add Playwright e2e tests with Text and Collabora Cover the overview, file list, search, filters, view toggle and creating from a template against a Nextcloud server with Text, richdocuments and a Collabora CODE container. Opening files in an editor is marked fixme until Text and richdocuments register with @nextcloud/viewer on server master. Refs #186 Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- .github/workflows/playwright.yml | 78 ++ .gitignore | 3 + package-lock.json | 923 +++++++++++++++++++++- package.json | 6 +- playwright.config.ts | 33 + playwright/e2e/create.spec.ts | 32 + playwright/e2e/files.spec.ts | 74 ++ playwright/e2e/open.spec.ts | 31 + playwright/e2e/overview.spec.ts | 27 + playwright/start-nextcloud-server.mjs | 142 ++++ playwright/support/files/document.odt | Bin 0 -> 8400 bytes playwright/support/files/note.md | 1 + playwright/support/files/presentation.odp | Bin 0 -> 11043 bytes playwright/support/files/report.docx | Bin 0 -> 5038 bytes playwright/support/files/spreadsheet.ods | Bin 0 -> 8359 bytes playwright/support/fixtures.ts | 46 ++ 16 files changed, 1381 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 playwright.config.ts create mode 100644 playwright/e2e/create.spec.ts create mode 100644 playwright/e2e/files.spec.ts create mode 100644 playwright/e2e/open.spec.ts create mode 100644 playwright/e2e/overview.spec.ts create mode 100644 playwright/start-nextcloud-server.mjs create mode 100644 playwright/support/files/document.odt create mode 100644 playwright/support/files/note.md create mode 100644 playwright/support/files/presentation.odp create mode 100644 playwright/support/files/report.docx create mode 100644 playwright/support/files/spreadsheet.ods create mode 100644 playwright/support/fixtures.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..cb67ff9 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Playwright + +on: pull_request + +permissions: + contents: read + +concurrency: + group: playwright-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + playwright: + runs-on: ubuntu-latest + timeout-minutes: 30 + + name: Playwright e2e + + steps: + - name: Checkout + 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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 & build app + run: | + npm ci + npm run build + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run Playwright tests + run: npm run test:e2e + env: + NEXTCLOUD_E2E_LOG_FILE: test-results/nextcloud.log + + - name: Upload results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 7 + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: playwright + + if: always() + + name: playwright-summary + + steps: + - name: Summary status + run: if ${{ needs.playwright.result != 'success' }}; then exit 1; fi diff --git a/.gitignore b/.gitignore index 8a76f18..9ce8645 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ /tests/.phpunit.cache /node_modules/ + +/test-results/ +/playwright-report/ diff --git a/package-lock.json b/package-lock.json index e62532d..50c339e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,9 +22,11 @@ }, "devDependencies": { "@nextcloud/browserslist-config": "^3.1.2", + "@nextcloud/e2e-test-server": "^0.6.1", "@nextcloud/eslint-config": "^8.4.2", "@nextcloud/stylelint-config": "^3.1.1", "@nextcloud/vite-config": "^2.5.4", + "@playwright/test": "^1.63.0", "@types/escape-html": "^1.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.0", @@ -370,6 +372,13 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1277,6 +1286,112 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.6.tgz", + "integrity": "sha512-SgC/B3/VZ8lTyOZ4Cwk6f1skWhmkNAaVfiX90qd5u1rRXGKk1//8FTUEN9GvOUq7rfWIqAYkA3CoyumYVpOS6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.7.tgz", + "integrity": "sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1389,6 +1504,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", @@ -1662,6 +1788,63 @@ "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, + "node_modules/@nextcloud/e2e-test-server": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@nextcloud/e2e-test-server/-/e2e-test-server-0.6.1.tgz", + "integrity": "sha512-qUnOJBubZMF3eJ1PE8nqGKZ/qt1WF/erAGolHPBxZpP0X8pCFIjBaSxI63ct7N9GGruoBiqbONgHJZI9yRwuhw==", + "dev": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nextcloud/paths": "^3.1.0", + "dockerode": "^5.0.0", + "fast-xml-parser": "^5.2.2", + "tar-stream": "^3.2.1", + "wait-on": "^9.0.1" + }, + "engines": { + "node": "^22.0.0 || ^24.0.0 || >=26.0.0" + } + }, + "node_modules/@nextcloud/e2e-test-server/node_modules/fast-xml-parser": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz", + "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.1", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@nextcloud/e2e-test-server/node_modules/strnum": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", + "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, "node_modules/@nextcloud/eslint-config": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/@nextcloud/eslint-config/-/eslint-config-8.4.2.tgz", @@ -2376,6 +2559,88 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", @@ -3125,6 +3390,16 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -4345,7 +4620,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4356,7 +4630,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4547,6 +4820,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -4691,6 +4974,21 @@ "axios": "0.x || 1.x" } }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -4707,6 +5005,91 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.4.tgz", + "integrity": "sha512-MPC2eyrpbk3A873sK4y6rk+Iv4o2enxTjHZ8zH3HO0mjrpm5XjnguY2XmfidIsDV6vnjpbMBKUAJpV0ISD2SsQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.5.tgz", + "integrity": "sha512-iM9qJCruvpt2rPaLsrumVDru9pogI7PHziHIa9kTNRFt8C8HuSa+kI9kFezK+NvHEFnnz71rKGZ1EAilq/3V/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base-64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", @@ -4747,6 +5130,16 @@ "node": ">=6.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -4766,6 +5159,18 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/blurhash": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", @@ -5050,6 +5455,16 @@ "dev": true, "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -5311,6 +5726,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", @@ -5326,6 +5748,36 @@ "node": ">= 0.10" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -5341,7 +5793,6 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5354,8 +5805,7 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/colord": { "version": "2.10.0", @@ -5527,6 +5977,21 @@ } } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -5964,6 +6429,40 @@ "node": ">=8" } }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-5.0.1.tgz", + "integrity": "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4" + }, + "engines": { + "node": ">= 14.17" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6196,8 +6695,17 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "once": "^1.4.0" + } }, "node_modules/entities": { "version": "4.5.0", @@ -7227,6 +7735,16 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -7267,6 +7785,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -7600,6 +8125,13 @@ "node": ">=12.20.0" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.5", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", @@ -7700,6 +8232,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", @@ -8737,7 +9279,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -9124,6 +9665,25 @@ "dev": true, "license": "MIT" }, + "node_modules/joi": { + "version": "18.2.9", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.9.tgz", + "integrity": "sha512-2mD929bUVKUhOLQQEVhlf6EZ0Mlo0DeRb5MO7cViR9AXLtBauuccEtB1py9Ocxpo/P7ucnh442iY/iOwrh3IQw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-beautify": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz", @@ -9514,6 +10074,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9530,6 +10097,13 @@ "license": "MIT", "peer": true }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -10443,6 +11017,13 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -10494,6 +11075,14 @@ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.29.0.tgz", + "integrity": "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", @@ -10866,7 +11455,6 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "wrappy": "1" } @@ -11275,6 +11863,35 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -11468,6 +12085,30 @@ "dev": true, "license": "ISC" }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -11499,6 +12140,17 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11805,6 +12457,16 @@ "unist-util-visit": "^5.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -12141,6 +12803,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -12219,6 +12891,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sass": { "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", @@ -12639,6 +13318,13 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, "node_modules/splitpanes": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-4.0.4.tgz", @@ -12658,6 +13344,24 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -12719,6 +13423,18 @@ "xtend": "^4.0.2" } }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12877,7 +13593,6 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -13528,6 +14243,69 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.2.tgz", + "integrity": "sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.9.0", + "bare-fs": "^4.8.2", + "fast-fifo": "^1.3.2", + "streamx": "^2.28.1" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13749,9 +14527,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true, - "peer": true + "license": "0BSD" }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -13760,6 +14536,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -13925,6 +14708,13 @@ "node": ">=22.19.0" } }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", @@ -15185,6 +15975,25 @@ "node": ">=18" } }, + "node_modules/wait-on": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.5.1.tgz", + "integrity": "sha512-bOtj1wRY6u+GPnNskZLcgZNHOaL5NC4+5hRCbEpVoVX6MbMrCLCqIBM2bjsAfrgst3ZC0siIo7vLb4LuYFfIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.20.0", + "joi": "^18.2.9", + "lodash": "^4.18.1", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -15448,13 +16257,45 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "7.0.1", @@ -15513,6 +16354,16 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -15521,6 +16372,50 @@ "license": "ISC", "peer": true }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 2126f6e..8ecdf49 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", "test:unit": "vitest run", "test:unit:watch": "vitest", - "watch": "vite --mode development build --watch" + "watch": "vite --mode development build --watch", + "test:e2e": "playwright test", + "start:nextcloud": "node playwright/start-nextcloud-server.mjs" }, "browserslist": [ "extends @nextcloud/browserslist-config" @@ -30,9 +32,11 @@ }, "devDependencies": { "@nextcloud/browserslist-config": "^3.1.2", + "@nextcloud/e2e-test-server": "^0.6.1", "@nextcloud/eslint-config": "^8.4.2", "@nextcloud/stylelint-config": "^3.1.1", "@nextcloud/vite-config": "^2.5.4", + "@playwright/test": "^1.63.0", "@types/escape-html": "^1.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..fdb973d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +/** + * 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: './playwright/e2e', + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + // One shared SQLite-backed server: parallel user creation races on it. + workers: 1, + timeout: 60_000, + reporter: process.env.CI ? [['github'], ['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: process.env.baseURL ?? 'http://localhost:8089/index.php/', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + ], + webServer: { + command: 'node playwright/start-nextcloud-server.mjs', + // Pulling and configuring Nextcloud and Collabora takes a while. + timeout: 10 * 60 * 1000, + stdout: 'pipe', + stderr: 'pipe', + wait: { stdout: /Office e2e environment is ready/ }, + gracefulShutdown: { signal: 'SIGTERM', timeout: 10_000 }, + }, +}) diff --git a/playwright/e2e/create.spec.ts b/playwright/e2e/create.spec.ts new file mode 100644 index 0000000..a00b87b --- /dev/null +++ b/playwright/e2e/create.spec.ts @@ -0,0 +1,32 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '../support/fixtures.ts' + +test('rejects an invalid filename', async ({ page }) => { + await page.goto('apps/office/documents') + await page.getByRole('button', { name: 'Blank' }).click() + + const dialog = page.getByRole('dialog') + await dialog.getByRole('textbox', { name: 'Filename' }).fill('in/valid.odt') + await dialog.getByRole('button', { name: 'Create' }).click() + + await expect(dialog).toBeVisible() + await expect(dialog.getByText('Filename contains invalid characters')).toBeVisible() +}) + +test('creates a blank document and opens it', async ({ page, account }) => { + await page.goto('apps/office/documents') + await page.getByRole('button', { name: 'Blank' }).click() + + const dialog = page.getByRole('dialog') + await dialog.getByRole('textbox', { name: 'Filename' }).fill('Created.odt') + await dialog.getByRole('button', { name: 'Create' }).click() + + // Hands off to the Files app, which opens the editor; see open.spec.ts. + await expect(page).toHaveURL(/apps\/files\/files\/\d+/) + const created = await page.request.fetch(`../remote.php/dav/files/${account.userId}/Created.odt`, { method: 'PROPFIND' }) + expect(created.status()).toBe(207) +}) diff --git a/playwright/e2e/files.spec.ts b/playwright/e2e/files.spec.ts new file mode 100644 index 0000000..0a2edda --- /dev/null +++ b/playwright/e2e/files.spec.ts @@ -0,0 +1,74 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '../support/fixtures.ts' + +test.beforeEach(async ({ upload }) => { + await upload('document.odt') + await upload('report.docx') + await upload('spreadsheet.ods') + await upload('presentation.odp') +}) + +test('shows each file in its category only', async ({ page }) => { + const files = page.locator('.office-overview__files') + + await page.goto('apps/office/documents') + await expect(files.getByText('document.odt')).toBeVisible() + await expect(files.getByText('report.docx')).toBeVisible() + await expect(files.getByText('spreadsheet.ods')).toHaveCount(0) + + await page.goto('apps/office/spreadsheets') + await expect(files.getByText('spreadsheet.ods')).toBeVisible() + await expect(files.getByText('document.odt')).toHaveCount(0) + + await page.goto('apps/office/presentations') + await expect(files.getByText('presentation.odp')).toBeVisible() +}) + +test('searches within the active category', async ({ page }) => { + await page.goto('apps/office/documents') + const files = page.locator('.office-overview__files') + await expect(files.getByText('document.odt')).toBeVisible() + + await page.getByRole('searchbox', { name: 'Search Documents' }).fill('report') + + await expect(files.getByText('report.docx')).toBeVisible() + await expect(files.getByText('document.odt')).toHaveCount(0) +}) + +test('filters by ownership and starred', async ({ page }) => { + await page.goto('apps/office/documents') + const files = page.locator('.office-overview__files') + const filters = page.getByRole('group', { name: 'Filter files' }) + + await expect(files.getByText('document.odt')).toBeVisible() + + await filters.getByRole('button', { name: 'Shared with me' }).click() + await expect(page.getByText('No Documents found')).toBeVisible() + + await filters.getByRole('button', { name: 'Starred' }).click() + await expect(page.getByRole('heading', { name: 'Starred Documents' })).toBeVisible() + await expect(page.getByText('No Documents found')).toBeVisible() + + await filters.getByRole('button', { name: 'All' }).click() + await expect(files.getByText('document.odt')).toBeVisible() +}) + +test('toggles between list and grid view and remembers it', async ({ page }) => { + await page.goto('apps/office/documents') + const files = page.locator('.office-overview__files') + await expect(files.getByText('document.odt')).toBeVisible() + + await page.getByRole('button', { name: 'Switch to grid view' }).click() + await expect(page.locator('.office-overview__grid')).toBeVisible() + await expect(page.getByRole('button', { name: 'Switch to list view' })).toBeVisible() + + await page.reload() + await expect(page.locator('.office-overview__grid')).toBeVisible() + + await page.getByRole('button', { name: 'Switch to list view' }).click() + await expect(page.locator('.office-overview__list')).toBeVisible() +}) diff --git a/playwright/e2e/open.spec.ts b/playwright/e2e/open.spec.ts new file mode 100644 index 0000000..9c30ada --- /dev/null +++ b/playwright/e2e/open.spec.ts @@ -0,0 +1,31 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '../support/fixtures.ts' + +// Server master dropped the bundled Viewer app for @nextcloud/viewer +// (nextcloud/server#63954); Text and richdocuments do not register with it yet, +// so Files falls back to downloading the file. Re-enable once they do. +test.fixme(true, 'Editors do not open on server master yet: nextcloud/server#63954') + +test('opens an office document in Collabora', async ({ page, upload }) => { + await upload('document.odt') + await page.goto('apps/office/documents') + + await page.locator('.office-overview__files').getByText('document.odt').click() + + const collabora = page.frameLocator('[data-cy="coolframe"]') + await expect(collabora.locator('#main-document-content')).toBeVisible({ timeout: 30_000 }) +}) + +test('opens a markdown file in Text', async ({ page, upload }) => { + await upload('note.md') + await page.goto('apps/office/') + + await page.getByRole('navigation').getByRole('link', { name: 'Text document' }).click() + await page.locator('.office-overview__files').getByText('note.md').click() + + await expect(page.locator('.editor').getByRole('textbox')).toContainText('Office e2e note') +}) diff --git a/playwright/e2e/overview.spec.ts b/playwright/e2e/overview.spec.ts new file mode 100644 index 0000000..0754905 --- /dev/null +++ b/playwright/e2e/overview.spec.ts @@ -0,0 +1,27 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '../support/fixtures.ts' + +test('lists a category per office file type and routes to it', async ({ page }) => { + await page.goto('apps/office/') + + const navigation = page.getByRole('navigation') + for (const category of ['Documents', 'Spreadsheets', 'Presentations', 'Diagrams']) { + await expect(navigation.getByRole('link', { name: category })).toBeVisible() + } + + await navigation.getByRole('link', { name: 'Spreadsheets' }).click() + await expect(page).toHaveURL(/apps\/office\/spreadsheets$/) + await expect(page.getByRole('heading', { name: 'Recent Spreadsheets' })).toBeVisible() +}) + +test('falls back to the first category for an unknown one', async ({ page }) => { + await page.goto('apps/office/does-not-exist') + + const first = page.getByRole('navigation').getByRole('link').first() + await expect(page).toHaveURL(new RegExp(`${await first.getAttribute('href')}$`)) + await expect(first).toHaveAttribute('aria-current', 'page') +}) diff --git a/playwright/start-nextcloud-server.mjs b/playwright/start-nextcloud-server.mjs new file mode 100644 index 0000000..48a426a --- /dev/null +++ b/playwright/start-nextcloud-server.mjs @@ -0,0 +1,142 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/* global process */ + +import { + configureNextcloud, + docker, + getContainer, + runOcc, + setSystemConfig, + startNextcloud, + stopNextcloud, + waitOnNextcloud, +} from '@nextcloud/e2e-test-server/docker' +import { resolve } from 'node:path' + +const NEXTCLOUD_PORT = 8089 +const COLLABORA_PORT = Number(process.env.COLLABORA_PORT ?? 9980) +const COLLABORA_IMAGE = 'collabora/code:latest' +const COLLABORA_CONTAINER = 'nextcloud-e2e-office-collabora' +// Set to a local richdocuments checkout (with built js/) to test against it +// instead of the app store release. +const RICHDOCUMENTS_PATH = process.env.RICHDOCUMENTS_PATH + +async function isServerRunning() { + try { + const res = await fetch(`http://127.0.0.1:${NEXTCLOUD_PORT}/status.php`) + return res.ok + } catch { + return false + } +} + +async function bridgeIp(container) { + const { NetworkSettings } = await container.inspect() + return NetworkSettings.Networks.bridge.IPAddress +} + +async function pull(image) { + const stream = await docker.pull(image) + await new Promise((resolve, reject) => docker.modem.followProgress(stream, (err) => err ? reject(err) : resolve())) +} + +async function removeCollabora() { + await docker.getContainer(COLLABORA_CONTAINER).remove({ force: true }).catch(() => {}) +} + +// Collabora calls back into Nextcloud for WOPI and Nextcloud fetches its +// discovery, so both talk over the docker bridge; the browser reaches Collabora +// through the published port. +async function startCollabora(nextcloudIp) { + console.log('Starting Collabora container… 🚀') + await pull(COLLABORA_IMAGE) + await removeCollabora() + const container = await docker.createContainer({ + Image: COLLABORA_IMAGE, + name: COLLABORA_CONTAINER, + Env: [ + `aliasgroup1=http://${nextcloudIp}`, + // The URL Collabora advertises in its discovery, which the browser loads. + `server_name=localhost:${COLLABORA_PORT}`, + 'extra_params=--o:ssl.enable=false --o:home_mode.enable=true', + ], + HostConfig: { + PortBindings: { '9980/tcp': [{ HostPort: String(COLLABORA_PORT) }] }, + }, + }) + await container.start() + + for (let tries = 0; tries < 60; tries++) { + try { + if ((await fetch(`http://127.0.0.1:${COLLABORA_PORT}/hosting/discovery`)).ok) { + console.log('└─ Collabora is ready') + return bridgeIp(container) + } + } catch {} + await new Promise((resolve) => setTimeout(resolve, 2000)) + } + throw new Error('Collabora did not become ready') +} + +async function configureRichdocuments(nextcloudIp, collaboraIp) { + if (!RICHDOCUMENTS_PATH) { + // No release targets the server's development branch yet, hence --force. + await runOcc(['app:install', '--allow-unstable', '--force', 'richdocuments'], { verbose: true }) + } + await runOcc(['app:enable', '--force', 'richdocuments'], { verbose: true }) + + await setSystemConfig('allow_local_remote_servers', 'true') + await runOcc(['config:system:set', 'trusted_domains', '1', `--value=${nextcloudIp}`]) + await runOcc(['config:app:set', 'richdocuments', 'wopi_allowlist', `--value=${collaboraIp}`]) + await runOcc([ + 'richdocuments:activate-config', + `--wopi-url=http://${collaboraIp}:9980`, + `--callback-url=http://${nextcloudIp}/`, + ], { verbose: true }) +} + +async function start() { + const mounts = RICHDOCUMENTS_PATH + ? { 'apps-writable/richdocuments': resolve(RICHDOCUMENTS_PATH) } + : {} + const ip = await startNextcloud('master', true, { exposePort: NEXTCLOUD_PORT, mounts }) + await waitOnNextcloud(ip) + await configureNextcloud(['text', 'office']) + + const nextcloudIp = await bridgeIp(getContainer()) + const collaboraIp = await startCollabora(nextcloudIp) + await configureRichdocuments(nextcloudIp, collaboraIp) + console.log('└─ Office e2e environment is ready') +} + +// Only tear down what this process started, so an already running server +// survives a test run that merely reused it. +let started = false + +async function stop() { + if (started) { + process.stderr.write('Stopping Nextcloud server…\n') + await removeCollabora() + await stopNextcloud() + } + process.exit(0) +} + +process.on('SIGTERM', stop) +process.on('SIGINT', stop) + +if (await isServerRunning()) { + console.log('└─ Office e2e environment is ready') +} else { + started = true + await start() +} + +// Idle to wait for shutdown +while (true) { + await new Promise((resolve) => setTimeout(resolve, 5000)) +} diff --git a/playwright/support/files/document.odt b/playwright/support/files/document.odt new file mode 100644 index 0000000000000000000000000000000000000000..0745d804e0abab77c5963a8136a647c990dd2f3c GIT binary patch literal 8400 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 iz#kIn582{b%RR?3q3L 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 literal 0 HcmV?d00001 diff --git a/playwright/support/files/spreadsheet.ods b/playwright/support/files/spreadsheet.ods new file mode 100644 index 0000000000000000000000000000000000000000..fdcf501eb8818acfb112d513ff0ea43d611e986d GIT binary patch literal 8359 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 Promise +} + +/** + * A fresh user per test, logged in, with a helper to upload files from + * `playwright/support/files/` into their root folder. + */ +export const test = base.extend({ + // eslint-disable-next-line no-empty-pattern + account: async ({}, use) => { + await use(await createRandomUser()) + }, + page: async ({ account, browser, baseURL }, use) => { + const page = await browser.newPage({ storageState: undefined, baseURL }) + await login(page.request, account) + // Cookie-authenticated DAV and OCS requests need the CSRF token. + const { token } = await (await page.request.get('./csrftoken', { failOnStatusCode: true })).json() + await page.context().setExtraHTTPHeaders({ requesttoken: token }) + await use(page) + await page.close() + }, + upload: async ({ account, page }, use) => { + await use(async (fixture, target = fixture) => { + const data = await readFile(new URL(`./files/${fixture}`, import.meta.url)) + await page.request.put(`../remote.php/dav/files/${account.userId}/${target}`, { + data, + failOnStatusCode: true, + }) + }) + }, +}) + +export { expect } from '@playwright/test' From ba45a8b7845c35c4a6bc967f6d4afa12e7a26ec5 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 14:27:51 +0200 Subject: [PATCH 02/11] docs: describe running the e2e tests Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 15f0a1f..e92f2a2 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,23 @@ every component needs outside a running NC page stub. `src/test-utils/fixtures.ts` has `makeNode()`/`makeCreator()` factories for building test data. CI runs the suite on PRs via `test-unit.yml`. +End-to-end tests use Playwright and need Docker: + +```bash +npx playwright install chromium +npm run build +npm run test:e2e +``` + +`playwright/start-nextcloud-server.mjs` starts a Nextcloud server (master) +with this app, Text and richdocuments, plus a Collabora CODE container on +port 9980. An already running server on port 8089 is reused, so +`npm run start:nextcloud` in a second terminal speeds up repeated runs. +`COLLABORA_PORT` changes the Collabora host port; `RICHDOCUMENTS_PATH` points +at a local richdocuments checkout (with built `js/`) instead of the app store +release. Example files live in `playwright/support/files/`. CI runs the suite +via `playwright.yml`. + ### 5. Committing changes Commit **source only** (`src/`, `lib/`, …) — do **not** commit the built From ed474ece41dbcbec3373b98f5ec634fad0fb00d4 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 15:26:24 +0200 Subject: [PATCH 03/11] test: allow running e2e against a local Text checkout Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- playwright/start-nextcloud-server.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/playwright/start-nextcloud-server.mjs b/playwright/start-nextcloud-server.mjs index 48a426a..9cbffe7 100644 --- a/playwright/start-nextcloud-server.mjs +++ b/playwright/start-nextcloud-server.mjs @@ -21,9 +21,10 @@ const NEXTCLOUD_PORT = 8089 const COLLABORA_PORT = Number(process.env.COLLABORA_PORT ?? 9980) const COLLABORA_IMAGE = 'collabora/code:latest' const COLLABORA_CONTAINER = 'nextcloud-e2e-office-collabora' -// Set to a local richdocuments checkout (with built js/) to test against it -// instead of the app store release. +// Set to a local checkout (with built js/ and vendor/) to test against it +// instead of the app store release or the Text main branch. const RICHDOCUMENTS_PATH = process.env.RICHDOCUMENTS_PATH +const TEXT_PATH = process.env.TEXT_PATH async function isServerRunning() { try { @@ -100,9 +101,10 @@ async function configureRichdocuments(nextcloudIp, collaboraIp) { } async function start() { - const mounts = RICHDOCUMENTS_PATH - ? { 'apps-writable/richdocuments': resolve(RICHDOCUMENTS_PATH) } - : {} + const mounts = { + ...(RICHDOCUMENTS_PATH && { 'apps-writable/richdocuments': resolve(RICHDOCUMENTS_PATH) }), + ...(TEXT_PATH && { 'apps-writable/text': resolve(TEXT_PATH) }), + } const ip = await startNextcloud('master', true, { exposePort: NEXTCLOUD_PORT, mounts }) await waitOnNextcloud(ip) await configureNextcloud(['text', 'office']) From 633eb32fe201eda93d064777ba7626e4a981c123 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:20:59 +0200 Subject: [PATCH 04/11] ci: run e2e against richdocuments with @nextcloud/viewer support Server master dropped the bundled Viewer app, so released richdocuments no longer opens files. Build nextcloud/richdocuments#6142, pinned to a commit, until it is released. Revert once a release supports @nextcloud/viewer. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- .github/workflows/playwright.yml | 16 ++++++++++++++++ playwright/e2e/create.spec.ts | 3 +-- playwright/e2e/open.spec.ts | 10 ++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index cb67ff9..265ebf6 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -45,6 +45,22 @@ jobs: npm ci npm run build + # Server master dropped the bundled Viewer app (nextcloud/server#63954); + # until a release supports @nextcloud/viewer, test against the PR that does. + - name: Build richdocuments with @nextcloud/viewer support + env: + # nextcloud/richdocuments#6142 (fix/6060-nextcloud-viewer-v2) + RICHDOCUMENTS_SHA: e101e6b3266cdb37e570137733bada08ca3f34af + run: | + git init -q "$RUNNER_TEMP/richdocuments" + cd "$RUNNER_TEMP/richdocuments" + git fetch -q --depth=1 https://github.com/nextcloud/richdocuments.git "$RICHDOCUMENTS_SHA" + git checkout -q FETCH_HEAD + composer install --no-dev + npm ci + npm run build + echo "RICHDOCUMENTS_PATH=$RUNNER_TEMP/richdocuments" >> "$GITHUB_ENV" + - name: Install Playwright browsers run: npx playwright install --with-deps chromium diff --git a/playwright/e2e/create.spec.ts b/playwright/e2e/create.spec.ts index a00b87b..15df4bb 100644 --- a/playwright/e2e/create.spec.ts +++ b/playwright/e2e/create.spec.ts @@ -25,8 +25,7 @@ test('creates a blank document and opens it', async ({ page, account }) => { await dialog.getByRole('textbox', { name: 'Filename' }).fill('Created.odt') await dialog.getByRole('button', { name: 'Create' }).click() - // Hands off to the Files app, which opens the editor; see open.spec.ts. - await expect(page).toHaveURL(/apps\/files\/files\/\d+/) + await expect(page.frameLocator('[data-cy="coolframe"]').locator('#main-document-content')).toBeVisible({ timeout: 30_000 }) const created = await page.request.fetch(`../remote.php/dav/files/${account.userId}/Created.odt`, { method: 'PROPFIND' }) expect(created.status()).toBe(207) }) diff --git a/playwright/e2e/open.spec.ts b/playwright/e2e/open.spec.ts index 9c30ada..813cc40 100644 --- a/playwright/e2e/open.spec.ts +++ b/playwright/e2e/open.spec.ts @@ -5,11 +5,6 @@ import { expect, test } from '../support/fixtures.ts' -// Server master dropped the bundled Viewer app for @nextcloud/viewer -// (nextcloud/server#63954); Text and richdocuments do not register with it yet, -// so Files falls back to downloading the file. Re-enable once they do. -test.fixme(true, 'Editors do not open on server master yet: nextcloud/server#63954') - test('opens an office document in Collabora', async ({ page, upload }) => { await upload('document.odt') await page.goto('apps/office/documents') @@ -20,7 +15,10 @@ test('opens an office document in Collabora', async ({ page, upload }) => { await expect(collabora.locator('#main-document-content')).toBeVisible({ timeout: 30_000 }) }) -test('opens a markdown file in Text', async ({ page, upload }) => { +// Server master dropped the bundled Viewer app for @nextcloud/viewer +// (nextcloud/server#63954) and Text does not register with it yet, so Files +// falls back to downloading the file. Re-enable with nextcloud/text#9235. +test.fixme('opens a markdown file in Text', async ({ page, upload }) => { await upload('note.md') await page.goto('apps/office/') From 9bedefdff7ff25295664e3b715da70abfdb30a64 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:21:09 +0200 Subject: [PATCH 05/11] ci: run e2e against Text with @nextcloud/viewer support Build nextcloud/text#9235, pinned to a commit, so Text registers with @nextcloud/viewer on server master. The Text spec stays fixme until opening a file works there. Revert once Text main supports @nextcloud/viewer. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- .github/workflows/playwright.yml | 15 +++++++++++++++ playwright/e2e/open.spec.ts | 5 ++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 265ebf6..4110d0b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -61,6 +61,21 @@ jobs: npm run build echo "RICHDOCUMENTS_PATH=$RUNNER_TEMP/richdocuments" >> "$GITHUB_ENV" + - name: Build Text with @nextcloud/viewer support + env: + # nextcloud/text#9235 (feature/migrate-new-viewer-api) + TEXT_SHA: 06f59d2bb2b4744ede66e7a60fabfe5cbde0c99c + CYPRESS_INSTALL_BINARY: 0 + run: | + git init -q "$RUNNER_TEMP/text" + cd "$RUNNER_TEMP/text" + git fetch -q --depth=1 https://github.com/nextcloud/text.git "$TEXT_SHA" + git checkout -q FETCH_HEAD + composer install --no-dev + npm ci + npm run build + echo "TEXT_PATH=$RUNNER_TEMP/text" >> "$GITHUB_ENV" + - name: Install Playwright browsers run: npx playwright install --with-deps chromium diff --git a/playwright/e2e/open.spec.ts b/playwright/e2e/open.spec.ts index 813cc40..d19986c 100644 --- a/playwright/e2e/open.spec.ts +++ b/playwright/e2e/open.spec.ts @@ -15,9 +15,8 @@ test('opens an office document in Collabora', async ({ page, upload }) => { await expect(collabora.locator('#main-document-content')).toBeVisible({ timeout: 30_000 }) }) -// Server master dropped the bundled Viewer app for @nextcloud/viewer -// (nextcloud/server#63954) and Text does not register with it yet, so Files -// falls back to downloading the file. Re-enable with nextcloud/text#9235. +// Text registers with @nextcloud/viewer in nextcloud/text#9235, but opening a +// file from Files does nothing yet there. Enable once that PR works. test.fixme('opens a markdown file in Text', async ({ page, upload }) => { await upload('note.md') await page.goto('apps/office/') From 41b6f9e4f41ad7fd66a9c462b38123711799dd1c Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 16:02:42 +0200 Subject: [PATCH 06/11] docs: mention TEXT_PATH for e2e tests Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e92f2a2..7903af2 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,9 @@ npm run test:e2e with this app, Text and richdocuments, plus a Collabora CODE container on port 9980. An already running server on port 8089 is reused, so `npm run start:nextcloud` in a second terminal speeds up repeated runs. -`COLLABORA_PORT` changes the Collabora host port; `RICHDOCUMENTS_PATH` points -at a local richdocuments checkout (with built `js/`) instead of the app store -release. Example files live in `playwright/support/files/`. CI runs the suite +`COLLABORA_PORT` changes the Collabora host port; `RICHDOCUMENTS_PATH` and +`TEXT_PATH` point at local checkouts (with built `js/` and `vendor/`) instead +of the app store release and Text main. Example files live in `playwright/support/files/`. CI runs the suite via `playwright.yml`. ### 5. Committing changes From ce3887b95ad204fc6e18d8a98d20bc2c48c9c4cf Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:21:52 +0200 Subject: [PATCH 07/11] test: scope the category fallback check to the app navigation The first link of any navigation landmark was matched, and its href was used unescaped in a RegExp. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- playwright/e2e/overview.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright/e2e/overview.spec.ts b/playwright/e2e/overview.spec.ts index 0754905..41ddc7a 100644 --- a/playwright/e2e/overview.spec.ts +++ b/playwright/e2e/overview.spec.ts @@ -21,7 +21,7 @@ test('lists a category per office file type and routes to it', async ({ page }) test('falls back to the first category for an unknown one', async ({ page }) => { await page.goto('apps/office/does-not-exist') - const first = page.getByRole('navigation').getByRole('link').first() - await expect(page).toHaveURL(new RegExp(`${await first.getAttribute('href')}$`)) + const first = page.locator('#app-navigation-vue').getByRole('link').first() + await expect(page).not.toHaveURL(/does-not-exist$/) await expect(first).toHaveAttribute('aria-current', 'page') }) From 86a219219ccccfa81df44e2de6554408be2450b6 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:25:06 +0200 Subject: [PATCH 08/11] test: only reuse a fully set up e2e server Reusing required just Nextcloud to answer, so a run after an interrupted setup got a server without Collabora. Require both containers, recreate Nextcloud on start and remove both again when setup fails. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- playwright/start-nextcloud-server.mjs | 38 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/playwright/start-nextcloud-server.mjs b/playwright/start-nextcloud-server.mjs index 9cbffe7..2ed70b1 100644 --- a/playwright/start-nextcloud-server.mjs +++ b/playwright/start-nextcloud-server.mjs @@ -26,15 +26,21 @@ const COLLABORA_CONTAINER = 'nextcloud-e2e-office-collabora' const RICHDOCUMENTS_PATH = process.env.RICHDOCUMENTS_PATH const TEXT_PATH = process.env.TEXT_PATH -async function isServerRunning() { +async function isUp(url) { try { - const res = await fetch(`http://127.0.0.1:${NEXTCLOUD_PORT}/status.php`) - return res.ok + return (await fetch(url)).ok } catch { return false } } +// Both containers only run together after a completed setup: a failed or +// interrupted one removes them again. +async function isServerRunning() { + return await isUp(`http://127.0.0.1:${NEXTCLOUD_PORT}/status.php`) + && await isUp(`http://127.0.0.1:${COLLABORA_PORT}/hosting/discovery`) +} + async function bridgeIp(container) { const { NetworkSettings } = await container.inspect() return NetworkSettings.Networks.bridge.IPAddress @@ -72,12 +78,10 @@ async function startCollabora(nextcloudIp) { await container.start() for (let tries = 0; tries < 60; tries++) { - try { - if ((await fetch(`http://127.0.0.1:${COLLABORA_PORT}/hosting/discovery`)).ok) { - console.log('└─ Collabora is ready') - return bridgeIp(container) - } - } catch {} + if (await isUp(`http://127.0.0.1:${COLLABORA_PORT}/hosting/discovery`)) { + console.log('└─ Collabora is ready') + return bridgeIp(container) + } await new Promise((resolve) => setTimeout(resolve, 2000)) } throw new Error('Collabora did not become ready') @@ -105,7 +109,8 @@ async function start() { ...(RICHDOCUMENTS_PATH && { 'apps-writable/richdocuments': resolve(RICHDOCUMENTS_PATH) }), ...(TEXT_PATH && { 'apps-writable/text': resolve(TEXT_PATH) }), } - const ip = await startNextcloud('master', true, { exposePort: NEXTCLOUD_PORT, mounts }) + // Never reuse a leftover Nextcloud container: it may be half configured. + const ip = await startNextcloud('master', true, { exposePort: NEXTCLOUD_PORT, mounts, forceRecreate: true }) await waitOnNextcloud(ip) await configureNextcloud(['text', 'office']) @@ -119,23 +124,26 @@ async function start() { // survives a test run that merely reused it. let started = false -async function stop() { +async function stop(exitCode = 0) { if (started) { process.stderr.write('Stopping Nextcloud server…\n') await removeCollabora() await stopNextcloud() } - process.exit(0) + process.exit(exitCode) } -process.on('SIGTERM', stop) -process.on('SIGINT', stop) +process.on('SIGTERM', () => stop()) +process.on('SIGINT', () => stop()) if (await isServerRunning()) { console.log('└─ Office e2e environment is ready') } else { started = true - await start() + await start().catch(async (error) => { + console.error(error) + await stop(1) + }) } // Idle to wait for shutdown From 0638e79bcc64ffbba3f1ac0228f572d5a1d645ff Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:25:27 +0200 Subject: [PATCH 09/11] ci: lint the e2e tests Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- .eslintrc.cjs | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2c91cb1..70c8b97 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -25,5 +25,14 @@ module.exports = { 'n/no-unpublished-import': 'off', }, }, + { + // The e2e suite and its server script run in Node, outside the bundle. + files: ['playwright/**', 'playwright.config.ts'], + rules: { + 'n/no-unpublished-import': 'off', + 'n/no-process-exit': 'off', + 'no-console': 'off', + }, + }, ], } diff --git a/package.json b/package.json index 8ecdf49..e55cd7a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "vite build", "dev": "vite --mode development build", - "lint": "eslint src", + "lint": "eslint src playwright playwright.config.ts", "prepare": "git config core.hooksPath .githooks || true", "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", "test:unit": "vitest run", From 2f761fa0be6312182b34230fd7eeeddf1b3021ee Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:25:36 +0200 Subject: [PATCH 10/11] docs: update e2e server reuse in the README Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7903af2..5a4fb67 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,12 @@ npm run test:e2e `playwright/start-nextcloud-server.mjs` starts a Nextcloud server (master) with this app, Text and richdocuments, plus a Collabora CODE container on -port 9980. An already running server on port 8089 is reused, so +port 9980. When both are already running they are reused, so `npm run start:nextcloud` in a second terminal speeds up repeated runs. `COLLABORA_PORT` changes the Collabora host port; `RICHDOCUMENTS_PATH` and `TEXT_PATH` point at local checkouts (with built `js/` and `vendor/`) instead -of the app store release and Text main. Example files live in `playwright/support/files/`. CI runs the suite -via `playwright.yml`. +of the app store release and Text main. Example files live in +`playwright/support/files/`. CI runs the suite via `playwright.yml`. ### 5. Committing changes From fdb157c53f566b3021da6f6dc54fa1ac6522f553 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Mon, 5 Oct 2026 20:30:13 +0200 Subject: [PATCH 11/11] test: follow the grid view default in the view toggle spec Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Julius Knorr --- playwright/e2e/files.spec.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/playwright/e2e/files.spec.ts b/playwright/e2e/files.spec.ts index 0a2edda..d289c68 100644 --- a/playwright/e2e/files.spec.ts +++ b/playwright/e2e/files.spec.ts @@ -57,18 +57,19 @@ test('filters by ownership and starred', async ({ page }) => { await expect(files.getByText('document.odt')).toBeVisible() }) -test('toggles between list and grid view and remembers it', async ({ page }) => { +test('defaults to grid view and remembers switching to list', async ({ page }) => { await page.goto('apps/office/documents') const files = page.locator('.office-overview__files') await expect(files.getByText('document.odt')).toBeVisible() - - await page.getByRole('button', { name: 'Switch to grid view' }).click() - await expect(page.locator('.office-overview__grid')).toBeVisible() - await expect(page.getByRole('button', { name: 'Switch to list view' })).toBeVisible() - - await page.reload() await expect(page.locator('.office-overview__grid')).toBeVisible() await page.getByRole('button', { name: 'Switch to list view' }).click() await expect(page.locator('.office-overview__list')).toBeVisible() + await expect(page.getByRole('button', { name: 'Switch to grid view' })).toBeVisible() + + await page.reload() + await expect(page.locator('.office-overview__list')).toBeVisible() + + await page.getByRole('button', { name: 'Switch to grid view' }).click() + await expect(page.locator('.office-overview__grid')).toBeVisible() })