diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 47bf020aee706..f3f8e1df41688 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -10,7 +10,7 @@
/__mocks__ @nextcloud/server-frontend
/__tests__ @nextcloud/server-frontend
/dist @nextcloud/server-frontend
-/cypress @nextcloud/server-frontend
+/tests/playwright @nextcloud/server-frontend
**/css @nextcloud/server-frontend
**/js @nextcloud/server-frontend
**/src @nextcloud/server-frontend
diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml
deleted file mode 100644
index 9af0b0cea5cf6..0000000000000
--- a/.github/workflows/cypress.yml
+++ /dev/null
@@ -1,269 +0,0 @@
-# This workflow is provided via the organization template repository
-#
-# https://github.com/nextcloud/.github
-# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
-#
-# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
-# SPDX-License-Identifier: MIT
-
-name: Cypress
-
-on:
- pull_request:
- types:
- - opened
- - synchronize
- - reopened
- - ready_for_review
- - labeled
-
-concurrency:
- group: cypress-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-env:
- # Adjust APP_NAME if your repository name is different
- APP_NAME: ${{ github.event.repository.name }}
-
- # This represents the server branch to checkout.
- # Usually it's the base branch of the PR, but for pushes it's the branch itself.
- # e.g. 'main', 'stable27' or 'feature/my-feature'
- # n.b. server will use head_ref, as we want to test the PR branch.
- BRANCH: ${{ github.base_ref || github.ref_name }}
-
-permissions:
- contents: read
- pull-requests: read
-
-jobs:
- gate:
- runs-on: ubuntu-latest-low
- steps:
- - name: Evaluate e2e tests execution conditions
- id: gate-e2e
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v8.0.0
- with:
- script: |
- const pr = context.payload.pull_request
-
- const hasForceLabel = pr.labels.some((label) => label.name === 'force-e2e-tests')
- const hasToReviewLabel = pr.labels.some((label) => label.name === '3. to review')
- const hasToReleaseLabel = pr.labels.some((label) => label.name === '4. to release')
-
- const files = await github.paginate(github.rest.pulls.listFiles, {
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: pr.number,
- per_page: 100,
- })
- const cypressTouched = files.some((file) => file.filename.startsWith('cypress'))
-
- if (hasForceLabel || hasToReviewLabel || hasToReleaseLabel || cypressTouched) {
- return
- } else {
- core.setFailed('Skipping Cypress: draft state, missing labels or no cypress path changes.')
- }
-
- init:
- runs-on: ubuntu-latest
- needs: gate
- outputs:
- nodeVersion: ${{ steps.versions.outputs.nodeVersion }}
- npmVersion: ${{ steps.versions.outputs.npmVersion }}
-
- env:
- # We'll install cypress in the cypress job
- CYPRESS_INSTALL_BINARY: 0
- PUPPETEER_SKIP_DOWNLOAD: true
-
- steps:
- - name: Checkout server
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- with:
- persist-credentials: false
- # We need to checkout submodules for 3rdparty
- submodules: true
-
- - name: Check composer.json
- id: check_composer
- uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
- with:
- files: 'composer.json'
-
- - name: Install composer dependencies
- if: steps.check_composer.outputs.files_exists == 'true'
- run: 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:
- 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: Restore npm cache
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~/.npm
- key: node-${{ steps.versions.outputs.nodeVersion }}-npm-${{ steps.versions.outputs.npmVersion }}-${{ hashFiles('**/package-lock.json') }}
-
- - name: Install node dependencies & build app
- run: |
- npm ci
- TESTING=true npm run build --if-present
-
- - name: Save npm cache
- uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~/.npm
- key: node-${{ steps.versions.outputs.nodeVersion }}-npm-${{ steps.versions.outputs.npmVersion }}-${{ hashFiles('**/package-lock.json') }}
-
- - name: Save context
- uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- key: cypress-context-${{ github.run_id }}
- path: ./
-
- cypress:
- runs-on: ubuntu-latest
- needs: [gate, init]
-
- strategy:
- fail-fast: false
- matrix:
- # Run multiple copies of the current job in parallel
- # Please increase the number or runners as your tests suite grows (0 based index for e2e tests)
- containers: ['0', '1']
- # Always align this number with the total of e2e runners (max. index + 1)
- total-containers: [2]
-
- name: runner ${{ matrix.containers }}
-
- steps:
- - name: Restore context
- id: cache
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- key: cypress-context-${{ github.run_id }}
- path: ./
-
- - name: Checkout server
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- if: steps.cache.outputs.cache-hit != 'true'
- with:
- persist-credentials: false
- # We need to checkout submodules for 3rdparty
- submodules: true
-
- - name: Check composer.json
- id: check_composer
- if: steps.cache.outputs.cache-hit != 'true'
- uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
- with:
- files: 'composer.json'
-
- - name: Install composer dependencies
- if: steps.check_composer.outputs.files_exists == 'true' && steps.cache.outputs.cache-hit != 'true'
- run: composer install --no-dev
-
- - name: Set up node ${{ needs.init.outputs.nodeVersion }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: ${{ needs.init.outputs.nodeVersion }}
-
- - name: Set up npm ${{ needs.init.outputs.npmVersion }}
- run: npm i -g 'npm@${{ needs.init.outputs.npmVersion }}'
-
- - name: Install node dependencies & build app
- if: steps.cache.outputs.cache-hit != 'true'
- run: |
- npm ci
- TESTING=true npm run build --if-present
-
- - name: Install cypress
- run: ./node_modules/cypress/bin/cypress install
-
- - name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
- uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
- with:
- # We already installed the dependencies in the init job
- install: false
- component: ${{ matrix.containers == 'component' }}
- env:
- # Needs to be prefixed with CYPRESS_
- CYPRESS_BRANCH: ${{ env.BRANCH }}
- # https://github.com/cypress-io/github-action/issues/124
- COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
- # Needed for some specific code workarounds
- TESTING: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SPLIT: ${{ matrix.total-containers }}
- SPLIT_INDEX: ${{ matrix.containers == 'component' && 0 || matrix.containers }}
- SPLIT_RANDOM_SEED: ${{ github.run_id }}
-
- - name: Upload snapshots and videos
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- if: always()
- with:
- name: snapshots_${{ matrix.containers }}
- path: |
- cypress/snapshots
- cypress/videos
-
- - name: Show logs
- if: failure() && matrix.containers != 'component'
- run: |
- for id in $(docker ps -aq); do
- docker container inspect "$id" --format '=== Logs for container {{.Name}} ==='
- docker logs "$id" >> nextcloud.log
- done
- echo '=== Nextcloud server logs ==='
- docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} cat data/nextcloud.log
-
- - name: Create data dir archive
- if: failure() && matrix.containers != 'component'
- run: docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} tar -cvjf - data > data.tar
-
- - name: Upload data archive
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- if: failure() && matrix.containers != 'component'
- with:
- name: nc_data_${{ matrix.containers }}
- path: data.tar
-
- summary:
- runs-on: ubuntu-latest-low
- needs: [init, cypress]
-
- if: always()
-
- name: cypress-summary
-
- permissions:
- # `actions:write` permission is required to delete caches
- # See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
- actions: write
- contents: read
-
- steps:
- - name: Summary status
- run: if ${{ needs.init.result != 'success' || ( needs.cypress.result != 'success' && needs.cypress.result != 'skipped' ) }}; then exit 1; fi
-
- - name: Delete cache on success
- run: |
- ## Setting this to not fail the workflow while deleting cache keys.
- set +e
- echo "Deleting cache..."
- gh cache delete 'cypress-context-${{ github.run_id }}'
- echo "Done"
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GH_REPO: ${{ github.repository }}
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index fc5aac80f9017..1ff86fad47828 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -82,8 +82,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- shardIndex: [1, 2, 3, 4, 5, 6, 7]
- shardTotal: [7]
+ shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
+ shardTotal: [8]
outputs:
node-version: ${{ steps.versions.outputs.node-version }}
package-manager-version: ${{ steps.versions.outputs.package-manager-version }}
diff --git a/.gitignore b/.gitignore
index 3836a0ac18e29..46a6155c8485c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,9 +179,7 @@ composer.phar
./.htaccess
core/js/mimetypelist.js
-# Tests - cypress
-cypress/downloads
-cypress/snapshots
-cypress/videos
+# Tests - Legacy cypress (to not clobber the directory when changing branches)
+/cypress
/.direnv
diff --git a/.nextcloudignore b/.nextcloudignore
index 67b7649665a47..f554eebc1869a 100644
--- a/.nextcloudignore
+++ b/.nextcloudignore
@@ -36,8 +36,6 @@ codecov.yml
cs-fixer
csfixer
custom.d.ts
-cypress
-cypress.config.ts
eslint.config.js
flake.lock
flake.nix
diff --git a/REUSE.toml b/REUSE.toml
index e9117789e80f7..6e155a2e1d39a 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -16,7 +16,7 @@ SPDX-FileCopyrightText = "2016-2024 Nextcloud translators"
SPDX-License-Identifier = "AGPL-3.0-or-later"
[[annotations]]
-path = ["lib/l10n/zh_TW.js", "lib/l10n/zh_TW.json", "core/l10n/zh_TW.js", "core/l10n/zh_TW.json", "apps/admin_audit/l10n/zh_TW.js", "apps/admin_audit/l10n/zh_TW.json", "apps/comments/l10n/zh_TW.js", "apps/comments/l10n/zh_TW.json", "apps/dav/l10n/zh_TW.js", "apps/dav/l10n/zh_TW.json", "apps/encryption/l10n/zh_TW.js", "apps/encryption/l10n/zh_TW.json", "apps/federatedfilesharing/l10n/zh_TW.js", "apps/federatedfilesharing/l10n/zh_TW.json", "apps/federation/l10n/zh_TW.js", "apps/federation/l10n/zh_TW.json", "apps/files/l10n/zh_TW.js", "apps/files/l10n/zh_TW.json", "apps/files_external/l10n/zh_TW.js", "apps/files_external/l10n/zh_TW.json", "apps/files_sharing/l10n/zh_TW.js", "apps/files_sharing/l10n/zh_TW.json", "apps/files_trashbin/l10n/zh_TW.js", "apps/files_trashbin/l10n/zh_TW.json", "apps/files_versions/l10n/zh_TW.js", "apps/files_versions/l10n/zh_TW.json", "apps/provisioning_api/l10n/zh_TW.js", "apps/provisioning_api/l10n/zh_TW.json", "apps/settings/l10n/zh_TW.js", "apps/settings/l10n/zh_TW.json", "apps/systemtags/l10n/zh_TW.js", "apps/systemtags/l10n/zh_TW.json", "apps/testing/l10n/zh_TW.js", "apps/testing/l10n/zh_TW.json", "apps/updatenotification/l10n/zh_TW.js", "apps/updatenotification/l10n/zh_TW.json", "apps/user_ldap/l10n/zh_TW.js", "apps/user_ldap/l10n/zh_TW.json"]
+path = ["lib/l10n/zh_TW.js", "lib/l10n/zh_TW.json", "core/l10n/zh_TW.js", "core/l10n/zh_TW.json", "apps/admin_audit/l10n/zh_TW.js", "apps/admin_audit/l10n/zh_TW.json", "apps/comments/l10n/zh_TW.js", "apps/comments/l10n/zh_TW.json", "apps/dav/l10n/zh_TW.js", "apps/dav/l10n/zh_TW.json", "apps/encryption/l10n/zh_TW.js", "apps/encryption/l10n/zh_TW.json", "apps/federatedfilesharing/l10n/zh_TW.js", "apps/federatedfilesharing/l10n/zh_TW.json", "apps/federation/l10n/zh_TW.js", "apps/federation/l10n/zh_TW.json", "apps/files/l10n/zh_TW.js", "apps/files/l10n/zh_TW.json", "apps/files_external/l10n/zh_TW.js", "apps/files_external/l10n/zh_TW.json", "apps/files_sharing/l10n/zh_TW.js", "apps/files_sharing/l10n/zh_TW.json", "apps/files_trashbin/l10n/zh_TW.js", "apps/files_trashbin/l10n/zh_TW.json", "apps/files_versions/l10n/zh_TW.js", "apps/files_versions/l10n/zh_TW.json", "apps/provisioning_api/l10n/zh_TW.js", "apps/provisioning_api/l10n/zh_TW.json", "apps/settings/l10n/zh_TW.js", "apps/settings/l10n/zh_TW.json", "apps/systemtags/l10n/zh_TW.js", "apps/systemtags/l10n/zh_TW.json", "apps/updatenotification/l10n/zh_TW.js", "apps/updatenotification/l10n/zh_TW.json", "apps/user_ldap/l10n/zh_TW.js", "apps/user_ldap/l10n/zh_TW.json"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2016-2024 Nextcloud translators, 2024 moda-l10n "
SPDX-License-Identifier = "AGPL-3.0-only OR AGPL-3.0-or-later"
@@ -28,7 +28,7 @@ SPDX-FileCopyrightText = "2016-2024 Nextcloud translators, 2024 moda-l10n , 2022 OpenJS Foundation and other contributors "
-SPDX-License-Identifier = "MIT"
-
[[annotations]]
path = "dist/preview-service-worker.js"
precedence = "aggregate"
@@ -393,12 +327,6 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2016 Andrew Nayenko "
SPDX-License-Identifier = "CC-BY-SA-3.0 OR GPL-3.0-or-later"
-[[annotations]]
-path = "cypress/fixtures/image.jpg"
-precedence = "aggregate"
-SPDX-FileCopyrightText = "2019 Tom Gainor "
-SPDX-License-Identifier = "LicenseRef-Unsplash"
-
[[annotations]]
path = "apps/settings/tests/UserMigration/assets/account-complex.jpg"
precedence = "aggregate"
diff --git a/build/files-checker.php b/build/files-checker.php
index ecc33549a52b5..f2381e2e22280 100644
--- a/build/files-checker.php
+++ b/build/files-checker.php
@@ -60,8 +60,6 @@
'core',
'cron.php',
'custom.d.ts',
- 'cypress.config.ts',
- 'cypress',
'dist',
'eslint.config.js',
'flake.lock',
diff --git a/build/frontend-legacy/eslint.config.mjs b/build/frontend-legacy/eslint.config.mjs
index 18a95d62380b6..6a2025cd0db01 100644
--- a/build/frontend-legacy/eslint.config.mjs
+++ b/build/frontend-legacy/eslint.config.mjs
@@ -4,7 +4,6 @@
*/
import { recommendedVue2 } from '@nextcloud/eslint-config'
-import CypressEslint from 'eslint-plugin-cypress'
import { defineConfig } from 'eslint/config'
import * as globals from 'globals'
@@ -48,18 +47,6 @@ export default defineConfig([
'jsdoc/require-jsdoc': 'off',
},
},
- // Cypress setup
- CypressEslint.configs.recommended,
- {
- name: 'server/cypress',
- files: ['cypress/**', '**/*.cy.*'],
- rules: {
- 'no-console': 'off',
- 'jsdoc/require-jsdoc': 'off',
- '@typescript-eslint/no-explicit-any': 'off',
- '@typescript-eslint/no-unused-expressions': 'off',
- },
- },
// customer server ignore files
{
name: 'server/ignored-files',
diff --git a/build/image-optimization.sh b/build/image-optimization.sh
index ddb213ea5bb4b..2b2037b17b8a0 100755
--- a/build/image-optimization.sh
+++ b/build/image-optimization.sh
@@ -45,9 +45,6 @@ function recursive_optimize_images() {
elif [[ "$DIR_NAME" == "build" ]]; then
echo -e "\033[0;36mIgnoring build for image optimization\033[0m"
return
- elif [[ "$DIR_NAME" == "cypress" ]]; then
- echo -e "\033[0;36mIgnoring cypress for image optimization\033[0m"
- return
elif [[ "$DIR_NAME" == "node_modules" ]]; then
echo -e "\033[0;36mIgnoring node_modules for image optimization\033[0m"
return
diff --git a/cypress.config.ts b/cypress.config.ts
deleted file mode 100644
index 29b83148220a3..0000000000000
--- a/cypress.config.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-/* eslint-disable no-console */
-/*!
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { configureNextcloud, getContainerName, runExec, runOcc, startNextcloud, stopNextcloud, waitOnNextcloud } from '@nextcloud/e2e-test-server'
-import { defineConfig } from 'cypress'
-import cypressSplit from 'cypress-split'
-import vitePreprocessor from 'cypress-vite'
-import { existsSync, rmSync } from 'node:fs'
-import { dirname, resolve } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { nodePolyfills } from 'vite-plugin-node-polyfills'
-
-if (!globalThis.__dirname) {
- // Cypress has their own weird parser
- globalThis.__dirname = dirname(fileURLToPath(new URL(import.meta.url)))
-}
-
-export default defineConfig({
- projectId: '37xpdh',
-
- // 16/9 screen ratio
- viewportWidth: 1280,
- viewportHeight: 720,
-
- // Tries again when in run mode (cypress run) e.g. on CI
- retries: {
- runMode: 5,
- // do not retry in `cypress open`
- openMode: 0,
- },
-
- // Needed to trigger `after:run` events with cypress open
- experimentalInteractiveRunEvents: true,
-
- // disabled if running in CI but enabled in debug mode
- video: !process.env.CI || !!process.env.RUNNER_DEBUG,
-
- // faster video processing
- videoCompression: false,
-
- // Prevent elements to be scrolled under a top bar during actions (click, clear, type, etc). Default is 'top'.
- // https://github.com/cypress-io/cypress/issues/871
- scrollBehavior: 'center',
-
- // Visual regression testing
- env: {
- failSilently: false,
- type: 'actual',
- },
-
- screenshotsFolder: 'cypress/snapshots/actual',
- trashAssetsBeforeRuns: true,
-
- e2e: {
- // Disable session isolation
- testIsolation: false,
-
- requestTimeout: 30000,
-
- // We've imported your old cypress plugins here.
- // You may want to clean this up later by importing these.
- async setupNodeEvents(on, config) {
- on('file:preprocessor', vitePreprocessor({
- plugins: [nodePolyfills()],
- }))
-
- // This allows to store global data (e.g. the name of a snapshot)
- // because Cypress.env() and other options are local to the current spec file.
- const data: Record = {}
- on('task', {
- setVariable({ key, value }) {
- data[key] = value
- return null
- },
- getVariable({ key }) {
- return data[key] ?? null
- },
- // allow to clear the downloads folder
- deleteFolder(path: string) {
- try {
- if (existsSync(path)) {
- rmSync(path, { maxRetries: 10, recursive: true })
- }
- return null
- } catch (error) {
- throw Error(`Error while deleting ${path}. Original error: ${error}`, { cause: error })
- }
- },
- })
-
- // Disable spell checking to prevent rendering differences
- on('before:browser:launch', (browser, launchOptions) => {
- if (browser.family === 'chromium' && browser.name !== 'electron') {
- launchOptions.preferences.default['browser.enable_spellchecking'] = false
- return launchOptions
- }
-
- if (browser.family === 'firefox') {
- launchOptions.preferences['layout.spellcheckDefault'] = 0
- return launchOptions
- }
-
- if (browser.name === 'electron') {
- launchOptions.preferences.spellcheck = false
- return launchOptions
- }
- })
-
- // Remove container after run
- on('after:run', () => {
- if (!process.env.CI) {
- stopNextcloud()
- }
- })
-
- cypressSplit(on, config)
-
- const mounts = {
- '3rdparty': resolve(__dirname, './3rdparty'),
- apps: resolve(__dirname, './apps'),
- core: resolve(__dirname, './core'),
- cypress: resolve(__dirname, './cypress'),
- dist: resolve(__dirname, './dist'),
- lib: resolve(__dirname, './lib'),
- ocs: resolve(__dirname, './ocs'),
- 'ocs-provider': resolve(__dirname, './ocs-provider'),
- resources: resolve(__dirname, './resources'),
- tests: resolve(__dirname, './tests'),
- 'console.php': resolve(__dirname, './console.php'),
- 'cron.php': resolve(__dirname, './cron.php'),
- 'index.php': resolve(__dirname, './index.php'),
- occ: resolve(__dirname, './occ'),
- 'public.php': resolve(__dirname, './public.php'),
- 'remote.php': resolve(__dirname, './remote.php'),
- 'status.php': resolve(__dirname, './status.php'),
- 'version.php': resolve(__dirname, './version.php'),
- } as Record
-
- for (const [key, path] of Object.entries(mounts)) {
- if (!existsSync(path)) {
- delete mounts[key]
- }
- }
-
- // Before the browser launches
- // starting Nextcloud testing container
- const port = 8042
- const ip = await startNextcloud(process.env.BRANCH, false, {
- mounts,
- exposePort: port,
- forceRecreate: true,
- })
- // Setting container's IP as base Url
- config.baseUrl = `http://localhost:${port}/index.php`
- // make sure not to write into apps but use a local apps folder
- runExec(['mkdir', 'apps-cypress'])
- runExec(['cp', 'cypress/fixtures/app.config.php', 'config'])
- // now wait until Nextcloud is ready and configure it
- await waitOnNextcloud(ip)
- await configureNextcloud()
- // additionally we do not want to DoS the app store
- runOcc(['config:system:set', 'appstoreenabled', '--value', 'false', '--type', 'boolean'])
- console.log('Running cron to finish setup and avoid first-run effects during tests π')
- runExec(['php', 'cron.php'])
-
- // for later use in tests save the container name
- // @ts-expect-error we are adding a custom property
- config.dockerContainerName = getContainerName()
-
- // IMPORTANT: return the config otherwise cypress-split will not work
- return config
- },
- },
-})
diff --git a/cypress/e2e/core-utils.ts b/cypress/e2e/core-utils.ts
deleted file mode 100644
index c01ee05a61eaf..0000000000000
--- a/cypress/e2e/core-utils.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-/**
- * Get the unified search modal (if open)
- */
-export function getUnifiedSearchModal() {
- return cy.get('#unified-search')
-}
-
-/**
- * Handle the confirm password dialog (if needed)
- *
- * @param adminPassword The admin password for the dialog
- */
-export function handlePasswordConfirmation(adminPassword = 'admin') {
- const handleModal = (context: Cypress.Chainable) => {
- return context.contains('.modal-container', 'Authentication required')
- .if()
- .within(() => {
- cy.get('input[type="password"]')
- .type(adminPassword)
- cy.findByRole('button', { name: 'Confirm' })
- .click()
- })
- }
-
- return cy.get('body')
- .if()
- .then(() => handleModal(cy.get('body')))
- .else()
- // Handle if inside a cy.within
- .root().closest('body')
- .then(($body) => handleModal(cy.wrap($body)))
-}
-
-/**
- * Open the unified search modal
- */
-export function openUnifiedSearch() {
- cy.get('button[aria-label="Unified search"]').click({ force: true })
- // wait for it to be open
- getUnifiedSearchModal().should('be.visible')
-}
-
-/**
- * Close the unified search modal
- */
-export function closeUnifiedSearch() {
- getUnifiedSearchModal().find('button[aria-label="Close"]').click({ force: true })
- getUnifiedSearchModal().should('not.be.visible')
-}
-
-/**
- * Get the input field of the unified search
- */
-export function getUnifiedSearchInput() {
- return getUnifiedSearchModal().find('[data-cy-unified-search-input]')
-}
-
-export enum UnifiedSearchFilter {
- FilterCurrentView = 'current-view',
- Places = 'places',
- People = 'people',
- Date = 'date',
-}
-
-/**
- * Get a filter action from the unified search
- *
- * @param filter The filter to get
- */
-export function getUnifiedSearchFilter(filter: UnifiedSearchFilter) {
- return getUnifiedSearchModal().find(`[data-cy-unified-search-filters] [data-cy-unified-search-filter="${CSS.escape(filter)}"]`)
-}
-
-/**
- * Assertion that an element is fully within the current viewport.
- *
- * @param $el The element
- * @param expected If the element is expected to be fully in viewport or not fully
- * @example
- * ```js
- * cy.get('#my-element')
- * .should(beFullyInViewport)
- * ```
- */
-export function beFullyInViewport($el: JQuery, expected = true) {
- const { top, left, bottom, right } = $el.get(0)!.getBoundingClientRect()
- const innerHeight = Cypress.$('body').innerHeight()!
- const innerWidth = Cypress.$('body').innerWidth()!
- const fullyVisible = top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth
-
- console.debug(`fullyVisible: ${fullyVisible}, top: ${top >= 0}, left: ${left >= 0}, bottom: ${bottom <= innerHeight}, right: ${right <= innerWidth}`)
-
- if (expected) {
- expect(fullyVisible, 'Fully within viewport').to.be.true
- } else {
- expect(fullyVisible, 'Not fully within viewport').to.be.false
- }
-}
-
-/**
- * Opposite of `beFullyInViewport` - resolves when element is not or only partially in viewport.
- *
- * @param $el The element
- * @example
- * ```js
- * cy.get('#my-element')
- * .should(notBeFullyInViewport)
- * ```
- */
-export function notBeFullyInViewport($el: JQuery) {
- return beFullyInViewport($el, false)
-}
diff --git a/cypress/e2e/dashboard/widget-performance.cy.ts b/cypress/e2e/dashboard/widget-performance.cy.ts
deleted file mode 100644
index 99e46d7b0ae38..0000000000000
--- a/cypress/e2e/dashboard/widget-performance.cy.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-/**
- * Regression test of https://github.com/nextcloud/server/issues/48403
- * Ensure that only visible widget data is loaded
- */
-describe('dashboard: performance', () => {
- before(() => {
- cy.createRandomUser().then((user) => {
- // Enable one widget
- cy.runOccCommand(`user:setting -- '${user.userId}' dashboard layout files-favorites`)
- cy.login(user)
- })
- })
-
- it('Only load needed widgets', () => {
- cy.intercept('**/dashboard/api/v2/widget-items?widgets*').as('loadedWidgets')
-
- const now = new Date(2025, 0, 14, 15)
- cy.clock(now)
-
- // The dashboard is loaded
- cy.visit('/apps/dashboard')
- cy.get('#app-dashboard')
- .should('be.visible')
- .contains('Good afternoon')
- .should('be.visible')
-
- // Wait that one data is loaded (ensure the API works), this should be the favorite files.
- cy.wait('@loadedWidgets')
- // Wait and check no requests are made (ensure that the user statuses data is NOT loaded)
- // eslint-disable-next-line cypress/no-unnecessary-waiting
- cy.wait(4000, { timeout: 8000 })
- cy.get('@loadedWidgets.all').then((interceptions) => {
- expect(interceptions).to.have.length(1)
- })
- })
-})
diff --git a/cypress/e2e/files/FilesUtils.ts b/cypress/e2e/files/FilesUtils.ts
deleted file mode 100644
index 2d7a28ac6d677..0000000000000
--- a/cypress/e2e/files/FilesUtils.ts
+++ /dev/null
@@ -1,453 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-const ACTION_COPY_MOVE = 'move-copy'
-
-export const getRowForFileId = (fileid: string | number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
-export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
-
-// Atomic query so the lookup is retried as a whole when rows re-render
-// (chained .find() can fail with "subject no longer attached" mid-render).
-export const getActionsForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"] [data-cy-files-list-row-actions]`)
-export const getActionsForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"] [data-cy-files-list-row-actions]`)
-
-export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
-export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
-
-/**
- *
- * @param fileid
- * @param actionId
- */
-export function getActionEntryForFileId(fileid: number, actionId: string) {
- return getActionButtonForFileId(fileid)
- .should('have.attr', 'aria-controls')
- .then((menuId) => cy.get(`#${menuId}`)
- .should('exist')
- .find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
-}
-
-/**
- *
- * @param file
- * @param actionId
- */
-export function getActionEntryForFile(file: string, actionId: string) {
- return getActionButtonForFile(file)
- .should('have.attr', 'aria-controls')
- .then((menuId) => cy.get(`#${menuId}`)
- .should('exist')
- .find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
-}
-
-/**
- *
- * @param fileid
- * @param actionId
- */
-export function getInlineActionEntryForFileId(fileid: number, actionId: string) {
- return cy.get(`[data-cy-files-list-row-fileid="${fileid}"] [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
-}
-
-/**
- *
- * @param file
- * @param actionId
- */
-export function getInlineActionEntryForFile(file: string, actionId: string) {
- return cy.get(`[data-cy-files-list-row-name="${CSS.escape(file)}"] [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
-}
-
-/**
- *
- * @param fileid
- * @param actionId
- */
-export function triggerActionForFileId(fileid: number, actionId: string) {
- getActionButtonForFileId(fileid)
- .scrollIntoView()
- getActionButtonForFileId(fileid)
- .click({ force: true }) // force to avoid issues with overlaying file list header
- getActionEntryForFileId(fileid, actionId)
- .find('button')
- .should('be.visible')
- .click()
-}
-
-/**
- *
- * @param filename
- * @param actionId
- */
-export function triggerActionForFile(filename: string, actionId: string) {
- getActionButtonForFile(filename)
- .scrollIntoView()
- getActionButtonForFile(filename)
- .click({ force: true }) // force to avoid issues with overlaying file list header
- getActionEntryForFile(filename, actionId)
- .find('button')
- .should('be.visible')
- .click()
-}
-
-/**
- *
- * @param fileid
- * @param actionId
- */
-export function triggerInlineActionForFileId(fileid: number, actionId: string) {
- getActionsForFileId(fileid)
- .find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
- .should('exist')
- .click()
-}
-/**
- *
- * @param filename
- * @param actionId
- */
-export function triggerInlineActionForFile(filename: string, actionId: string) {
- getActionsForFile(filename)
- .find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
- .should('exist')
- .click()
-}
-
-/**
- *
- */
-export function selectAllFiles() {
- cy.get('[data-cy-files-list-selection-checkbox]')
- .findByRole('checkbox', { checked: false })
- .click({ force: true })
-}
-/**
- *
- */
-export function deselectAllFiles() {
- cy.get('[data-cy-files-list-selection-checkbox]')
- .findByRole('checkbox', { checked: true })
- .click({ force: true })
-}
-
-/**
- *
- * @param filename
- * @param options
- */
-export function selectRowForFile(filename: string, options: Partial = {}) {
- getRowForFile(filename)
- .find('[data-cy-files-list-row-checkbox]')
- .findByRole('checkbox')
- // don't use click to avoid triggering side effects events
- .trigger('change', { ...options, force: true })
- .should('be.checked')
- cy.get('[data-cy-files-list-selection-checkbox]').findByRole('checkbox').should('satisfy', (elements) => {
- return elements.length === 1 && (elements[0].checked === true || elements[0].indeterminate === true)
- })
-}
-
-export const getSelectionActionButton = () => cy.get('[data-cy-files-list-selection-actions]').findByRole('button', { name: 'Actions' })
-export const getSelectionActionEntry = (actionId: string) => cy.get(`[data-cy-files-list-selection-action="${CSS.escape(actionId)}"]`)
-/**
- *
- * @param actionId
- */
-export function triggerSelectionAction(actionId: string) {
- // Even if it's inline, we open the action menu to get all actions visible
- getSelectionActionButton().click({ force: true })
- // the entry might already be a button or a button might its child
- getSelectionActionEntry(actionId)
- .then(($el) => $el.is('button') ? cy.wrap($el) : cy.wrap($el).findByRole('menuitem').last())
- .should('exist')
- .click()
-}
-
-/**
- *
- * @param fileName
- * @param dirPath
- */
-export function moveFile(fileName: string, dirPath: string) {
- getRowForFile(fileName).should('be.visible')
- triggerActionForFile(fileName, ACTION_COPY_MOVE)
-
- cy.get('.file-picker').within(() => {
- // intercept the copy so we can wait for it
- cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
-
- if (dirPath === '/') {
- // select home folder
- cy.get('.breadcrumb')
- .findByRole('button', { name: 'All files' })
- .should('be.visible')
- .click()
- // click move
- cy.contains('button', 'Move').should('be.visible').click()
- } else if (dirPath === '.') {
- // click move
- cy.contains('button', 'Copy').should('be.visible').click()
- } else {
- const directories = dirPath.split('/')
- directories.forEach((directory) => {
- // select the folder
- cy.get(`[data-filename="${directory}"]`).should('be.visible').click()
- })
-
- // click move
- cy.contains('button', `Move to ${directories.at(-1)}`).should('be.visible').click()
- }
-
- cy.wait('@moveFile')
- })
-}
-
-/**
- *
- * @param fileName
- * @param dirPath
- */
-export function copyFile(fileName: string, dirPath: string) {
- getRowForFile(fileName).should('be.visible')
- triggerActionForFile(fileName, ACTION_COPY_MOVE)
-
- cy.get('.file-picker').within(() => {
- // intercept the copy so we can wait for it
- cy.intercept('COPY', /\/(remote|public)\.php\/dav\/files\//).as('copyFile')
-
- if (dirPath === '/') {
- // select home folder
- cy.get('.breadcrumb')
- .findByRole('button', { name: 'All files' })
- .should('be.visible')
- .click()
- // click copy
- cy.contains('button', 'Copy').should('be.visible').click()
- } else if (dirPath === '.') {
- // click copy
- cy.contains('button', 'Copy').should('be.visible').click()
- } else {
- const directories = dirPath.split('/')
- directories.forEach((directory) => {
- // select the folder
- cy.get(`[data-filename="${CSS.escape(directory)}"]`).should('be.visible').click()
- })
-
- // click copy
- cy.contains('button', `Copy to ${directories.at(-1)}`).should('be.visible').click()
- }
-
- cy.wait('@copyFile')
- })
-}
-
-/**
- *
- * @param fileName
- * @param newFileName
- */
-export function renameFile(fileName: string, newFileName: string) {
- getRowForFile(fileName)
- .should('exist')
- .scrollIntoView()
-
- triggerActionForFile(fileName, 'rename')
-
- // intercept the move so we can wait for it
- cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
-
- getRowForFile(fileName)
- .find('[data-cy-files-list-row-name] input')
- .type(`{selectAll}${newFileName}{enter}`)
-
- cy.wait('@moveFile')
-}
-
-/**
- *
- * @param dirPath
- */
-export function navigateToFolder(dirPath: string) {
- const directories = dirPath.split('/')
- for (const directory of directories) {
- if (directory === '') {
- continue
- }
-
- getRowForFile(directory).should('be.visible').find('[data-cy-files-list-row-name-link]').click()
- }
-}
-
-/**
- * Close the sidebar
- */
-export function closeSidebar() {
- // {force: true} as it might be hidden behind toasts
- cy.get('[data-cy-sidebar] .app-sidebar__close')
- .click({ force: true })
- cy.get('[data-cy-sidebar]')
- .should('not.be.visible')
- // eslint-disable-next-line cypress/no-unnecessary-waiting -- wait for the animation to finish
- cy.wait(500)
- cy.url()
- .should('not.contain', 'opendetails')
- // close all toasts
- cy.get('.toast-success')
- .if()
- .findAllByRole('button')
- .click({ force: true, multiple: true })
-}
-
-/**
- *
- * @param label
- */
-export function clickOnBreadcrumbs(label: string) {
- cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
- cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click()
- cy.wait('@propfind')
-}
-
-/**
- *
- * @param folderName
- */
-export function createFolder(folderName: string) {
- cy.intercept('MKCOL', /\/remote.php\/dav\/files\//).as('createFolder')
-
- // TODO: replace by proper data-cy selectors
- cy.get('[data-cy-upload-picker] .action-item__menutoggle').first().click()
- cy.get('[data-cy-upload-picker-menu-entry="newFolder"] button').click()
- cy.get('[data-cy-files-new-node-dialog]').should('be.visible')
- cy.get('[data-cy-files-new-node-dialog-input]').type(`{selectall}${folderName}`)
- cy.get('[data-cy-files-new-node-dialog-submit]').click()
-
- cy.wait('@createFolder')
-
- getRowForFile(folderName).should('be.visible')
-}
-
-/**
- * Check validity of an input element
- *
- * @param validity The expected validity message (empty string means it is valid)
- * @example
- * ```js
- * cy.findByRole('textbox')
- * .should(haveValidity(/must not be empty/i))
- * ```
- */
-export function haveValidity(validity: string | RegExp) {
- if (typeof validity === 'string') {
- return (el: JQuery) => expect((el.get(0) as HTMLInputElement).validationMessage).to.equal(validity)
- }
- return (el: JQuery) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
-}
-
-/**
- *
- * @param user
- * @param path
- */
-export function deleteFileWithRequest(user: User, path: string) {
- // Ensure path starts with a slash and has no double slashes
- path = `/${path}`.replace(/\/+/g, '/')
-
- cy.request('/csrftoken').then(({ body }) => {
- const requestToken = body.token
- cy.request({
- method: 'DELETE',
- url: `${Cypress.env('baseUrl')}/remote.php/dav/files/${user.userId}${path}`,
- auth: {
- user: user.userId,
- password: user.password,
- },
- headers: {
- requestToken,
- },
- retryOnStatusCodeFailure: true,
- })
- })
-}
-
-/**
- *
- * @param actionId
- */
-export function triggerFileListAction(actionId: string) {
- cy.get(`button[data-cy-files-list-action="${CSS.escape(actionId)}"]`).last()
- .should('exist').click({ force: true })
-}
-
-/**
- * Reloads the current folder
- *
- * @param intercept if true this will wait for the PROPFIND to complete before it resolves
- */
-export function reloadCurrentFolder(intercept = true) {
- cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
- cy.findByRole('navigation', { name: 'Current directory path' })
- .findAllByRole('button')
- .filter('[aria-haspopup="menu"]')
- .click()
- cy.findByRole('menu')
- .should('be.visible')
- .findByRole('menuitem', { name: 'Reload content' })
- .click()
-
- if (intercept) {
- cy.wait('@propfind')
- }
-}
-
-/**
- * Enable the grid mode for the files list.
- * Will fail if already enabled!
- */
-export function enableGridMode() {
- cy.intercept('**/apps/files/api/v1/config/grid_view').as('setGridMode')
- cy.findByRole('button', { name: 'Switch to grid view' })
- .should('be.visible')
- .click()
- cy.wait('@setGridMode')
-}
-
-/**
- * Calculate the needed viewport height to limit the visible rows of the file list.
- * Requires a logged in user.
- *
- * @param rows The number of rows that should be displayed at the same time
- */
-export function calculateViewportHeight(rows: number): Cypress.Chainable {
- cy.visit('/apps/files')
-
- cy.get('[data-cy-files-list]')
- .should('be.visible')
-
- cy.get('[data-cy-files-list-tbody] tr', { timeout: 5000 })
- .and('be.visible')
-
- return cy.get('[data-cy-files-list]')
- .should('be.visible')
- .then((filesList) => {
- const windowHeight = Cypress.$('body').outerHeight()!
- // Size of other page elements
- const outerHeight = Math.ceil(windowHeight - filesList.outerHeight()!)
- // Size of before and filters
- const beforeHeight = Math.ceil(Cypress.$('.files-list__before').outerHeight()!)
- const filterHeight = Math.ceil(Cypress.$('.files-list__filters').outerHeight()!)
- // Size of the table header
- const tableHeaderHeight = Math.ceil(Cypress.$('[data-cy-files-list-thead]').outerHeight()!)
- // table row height
- const rowHeight = Math.ceil(Cypress.$('[data-cy-files-list-tbody] tr').outerHeight()!)
-
- // sum it up
- const viewportHeight = outerHeight + beforeHeight + filterHeight + tableHeaderHeight + rows * rowHeight
- cy.log(`Calculated viewport height: ${viewportHeight} (${outerHeight} + ${beforeHeight} + ${filterHeight} + ${tableHeaderHeight} + ${rows} * ${rowHeight})`)
- return cy.wrap(viewportHeight)
- })
-}
diff --git a/cypress/e2e/files_sharing/FilesSharingUtils.ts b/cypress/e2e/files_sharing/FilesSharingUtils.ts
deleted file mode 100644
index 432b8b5ddbfab..0000000000000
--- a/cypress/e2e/files_sharing/FilesSharingUtils.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { closeSidebar, triggerActionForFile } from '../files/FilesUtils.ts'
-
-export interface ShareSetting {
- read: boolean
- update: boolean
- delete: boolean
- create: boolean
- share: boolean
- download: boolean
- note: string
- expiryDate: Date
-}
-
-export function createShare(fileName: string, username: string, shareSettings: Partial = {}) {
- openSharingPanel(fileName)
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
-
- cy.get('#app-sidebar-vue').within(() => {
- cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch')
- cy.findByRole('combobox', { name: /Search for internal recipients/i })
- .type(`{selectAll}${username}`)
- cy.wait('@userSearch')
- })
-
- cy.get(`[user="${username}"]`).click()
-
- // HACK: Save the share and then update it, as permissions changes are currently not saved for new share.
- cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
- cy.wait('@createShare')
- closeSidebar()
-
- updateShare(fileName, 0, shareSettings)
-}
-
-export function openSharingDetails(index: number) {
- cy.get('#app-sidebar-vue').within(() => {
- cy.findAllByRole('button', { name: /open sharing details/i })
- .should('have.length.at.least', index + 1)
- .eq(index)
- .click({ force: true })
- cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]')
- .click()
- })
-}
-
-export function updateShare(fileName: string, index: number, shareSettings: Partial = {}) {
- openSharingPanel(fileName)
- openSharingDetails(index)
-
- cy.intercept({ times: 1, method: 'PUT', url: '**/apps/files_sharing/api/v1/shares/*' }).as('updateShare')
-
- cy.get('#app-sidebar-vue').within(() => {
- if (shareSettings.download !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox')
- if (shareSettings.download) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@downloadCheckbox')
- .check({ force: true, scrollBehavior: 'nearest' })
- cy.get('@downloadCheckbox')
- .should('be.checked')
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@downloadCheckbox')
- .uncheck({ force: true, scrollBehavior: 'nearest' })
- cy.get('@downloadCheckbox')
- .should('not.be.checked')
- }
- }
-
- if (shareSettings.read !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="read"]').find('input').as('readCheckbox')
- if (shareSettings.read) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@readCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.update !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="update"]').find('input').as('updateCheckbox')
- if (shareSettings.update) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@updateCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.create !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="create"]').find('input').as('createCheckbox')
- if (shareSettings.create) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@createCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@createCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.delete !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="delete"]').find('input').as('deleteCheckbox')
- if (shareSettings.delete) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@deleteCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.note !== undefined) {
- cy.findByRole('checkbox', { name: /note to recipient/i }).check({ force: true, scrollBehavior: 'nearest' })
- cy.findByRole('textbox', { name: /note to recipient/i }).type(shareSettings.note)
- }
-
- if (shareSettings.expiryDate !== undefined) {
- cy.findByRole('checkbox', { name: /expiration date/i })
- .check({ force: true, scrollBehavior: 'nearest' })
- cy.get('#share-date-picker')
- .type(`${shareSettings.expiryDate.getFullYear()}-${String(shareSettings.expiryDate.getMonth() + 1).padStart(2, '0')}-${String(shareSettings.expiryDate.getDate()).padStart(2, '0')}`)
- }
-
- cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
-
- cy.wait('@updateShare')
- })
- closeSidebar()
-}
-
-export function openSharingPanel(fileName: string) {
- triggerActionForFile(fileName, 'details')
-
- cy.get('[data-cy-sidebar]')
- .as('sidebar')
- .should('be.visible')
- cy.get('@sidebar')
- .find('[aria-controls="tab-sharing"]')
- .click()
-}
-
-type FileRequestOptions = {
- label?: string
- note?: string
- password?: string
- /* YYYY-MM-DD format */
- expiration?: string
-}
-
-/**
- * Create a file request for a folder
- *
- * @param path The path of the folder, leading slash is required
- * @param options The options for the file request
- */
-export function createFileRequest(path: string, options: FileRequestOptions = {}) {
- if (!path.startsWith('/')) {
- throw new Error('Path must start with a slash')
- }
-
- // Navigate to the folder
- cy.visit('/apps/files/files?dir=' + path)
-
- // Open the file request dialog
- cy.get('[data-cy-upload-picker] .action-item__menutoggle').first().click()
- cy.contains('.upload-picker__menu-entry button', 'Create file request').click()
- cy.get('[data-cy-file-request-dialog]').should('be.visible')
-
- // Check and fill the first page options
- cy.get('[data-cy-file-request-dialog-fieldset="label"]').should('be.visible')
- cy.get('[data-cy-file-request-dialog-fieldset="destination"]').should('be.visible')
- cy.get('[data-cy-file-request-dialog-fieldset="note"]').should('be.visible')
-
- cy.get('[data-cy-file-request-dialog-fieldset="destination"] input').should('contain.value', path)
- if (options.label) {
- cy.get('[data-cy-file-request-dialog-fieldset="label"] input').type(`{selectall}${options.label}`)
- }
- if (options.note) {
- cy.get('[data-cy-file-request-dialog-fieldset="note"] textarea').type(`{selectall}${options.note}`)
- }
-
- // Go to the next page
- cy.get('[data-cy-file-request-dialog-controls="next"]').click()
- cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="checkbox"]').should('exist')
- cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="date"]').should('not.exist')
- cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="checkbox"]').should('exist')
- cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="password"]').should('not.exist')
- if (options.expiration) {
- cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="checkbox"]').check({ force: true })
- cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="date"]').type(`{selectall}${options.expiration}`)
- }
- if (options.password) {
- cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="checkbox"]').check({ force: true })
- cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="password"]').type(`{selectall}${options.password}`)
- }
-
- // Create the file request
- cy.get('[data-cy-file-request-dialog-controls="next"]').click()
-
- // Get the file request URL
- cy.get('[data-cy-file-request-dialog-fieldset="link"]').then(($link) => {
- const url = $link.val()
- cy.log(`File request URL: ${url}`)
- cy.wrap(url).as('fileRequestUrl')
- })
-
- // Close
- cy.get('[data-cy-file-request-dialog-controls="finish"]').click()
-}
diff --git a/cypress/e2e/files_sharing/ShareOptionsType.ts b/cypress/e2e/files_sharing/ShareOptionsType.ts
deleted file mode 100644
index 6771590f24429..0000000000000
--- a/cypress/e2e/files_sharing/ShareOptionsType.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-export type ShareOptions = {
- enforcePassword?: boolean
- enforceExpirationDate?: boolean
- alwaysAskForPassword?: boolean
- defaultExpirationDateSet?: boolean
-}
-
-export const defaultShareOptions: ShareOptions = {
- enforcePassword: false,
- enforceExpirationDate: false,
- alwaysAskForPassword: false,
- defaultExpirationDateSet: false,
-}
diff --git a/cypress/e2e/files_sharing/expiry-date.cy.ts b/cypress/e2e/files_sharing/expiry-date.cy.ts
deleted file mode 100644
index 0055b499d38d8..0000000000000
--- a/cypress/e2e/files_sharing/expiry-date.cy.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { closeSidebar } from '../files/FilesUtils.ts'
-import { createShare, openSharingDetails, openSharingPanel, updateShare } from './FilesSharingUtils.ts'
-
-describe('files_sharing: Expiry date', () => {
- const expectedDefaultDate = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000)
- const expectedDefaultDateString = `${expectedDefaultDate.getFullYear()}-${String(expectedDefaultDate.getMonth() + 1).padStart(2, '0')}-${String(expectedDefaultDate.getDate()).padStart(2, '0')}`
- const fortnight = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000)
- const fortnightString = `${fortnight.getFullYear()}-${String(fortnight.getMonth() + 1).padStart(2, '0')}-${String(fortnight.getDate()).padStart(2, '0')}`
-
- let alice: User
- let bob: User
-
- before(() => {
- // Ensure we have the admin setting setup for default dates with 2 days in the future
- cy.runOccCommand('config:app:set --value yes core shareapi_default_internal_expire_date')
- cy.runOccCommand('config:app:set --value 2 core shareapi_internal_expire_after_n_days')
-
- cy.createRandomUser().then((user) => {
- alice = user
- cy.login(alice)
- })
- cy.createRandomUser().then((user) => {
- bob = user
- })
- })
-
- after(() => {
- cy.runOccCommand('config:app:delete core shareapi_default_internal_expire_date')
- cy.runOccCommand('config:app:delete core shareapi_enforce_internal_expire_date')
- cy.runOccCommand('config:app:delete core shareapi_internal_expire_after_n_days')
- })
-
- beforeEach(() => {
- cy.runOccCommand('config:app:delete core shareapi_enforce_internal_expire_date')
- })
-
- it('See default expiry date is set and enforced', () => {
- // Enforce the date
- cy.runOccCommand('config:app:set --value yes core shareapi_enforce_internal_expire_date')
- const dir = 'defaultExpiryDateEnforced'
- prepareDirectory(dir)
-
- validateExpiryDate(dir, expectedDefaultDateString)
- cy.findByRole('checkbox', { name: /expiration date/i })
- .should('be.checked')
- .and('be.disabled')
- })
-
- it('See default expiry date is set also if not enforced', () => {
- const dir = 'defaultExpiryDate'
- prepareDirectory(dir)
-
- validateExpiryDate(dir, expectedDefaultDateString)
- cy.findByRole('checkbox', { name: /expiration date/i })
- .should('be.checked')
- .and('not.be.disabled')
- .check({ force: true, scrollBehavior: 'nearest' })
- })
-
- it('Can set custom expiry date', () => {
- const dir = 'customExpiryDate'
- prepareDirectory(dir)
- updateShare(dir, 0, { expiryDate: fortnight })
- validateExpiryDate(dir, fortnightString)
- })
-
- it('Custom expiry date survives reload', () => {
- const dir = 'customExpiryDateReload'
- prepareDirectory(dir)
- updateShare(dir, 0, { expiryDate: fortnight })
- validateExpiryDate(dir, fortnightString)
-
- cy.visit('/apps/files')
- validateExpiryDate(dir, fortnightString)
- })
-
- /**
- * Regression test for https://github.com/nextcloud/server/pull/50192
- * Ensure that admin default settings do not always override the user set value.
- */
- it('Custom expiry date survives unrelated update', () => {
- const dir = 'customExpiryUnrelatedChanges'
- prepareDirectory(dir)
- updateShare(dir, 0, { expiryDate: fortnight })
- validateExpiryDate(dir, fortnightString)
- closeSidebar()
-
- cy.log('Upadate share and validate expiry date is kept')
- updateShare(dir, 0, { note: 'Only note changed' })
- validateExpiryDate(dir, fortnightString)
-
- cy.log('Reload page and validate expiry date is kept')
- cy.visit('/apps/files')
- validateExpiryDate(dir, fortnightString)
- })
-
- /**
- * Prepare directory, login and share to bob
- *
- * @param name The directory name
- */
- function prepareDirectory(name: string) {
- cy.mkdir(alice, `/${name}`)
- cy.login(alice)
- cy.visit('/apps/files')
- createShare(name, bob.userId)
- }
-
- /**
- * Validate expiry date on a share
- *
- * @param filename The filename to validate
- * @param expectedDate The expected date in YYYY-MM-dd
- */
- function validateExpiryDate(filename: string, expectedDate: string) {
- openSharingPanel(filename)
- openSharingDetails(0)
-
- cy.get('#share-date-picker')
- .should('exist')
- .and('have.value', expectedDate)
- }
-})
diff --git a/cypress/e2e/files_sharing/file-request.cy.ts b/cypress/e2e/files_sharing/file-request.cy.ts
deleted file mode 100644
index 76965f320bb70..0000000000000
--- a/cypress/e2e/files_sharing/file-request.cy.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { createFolder, getRowForFile, navigateToFolder } from '../files/FilesUtils.ts'
-import { createFileRequest } from './FilesSharingUtils.ts'
-
-function enterGuestName(name: string) {
- cy.findByRole('dialog', { name: /Upload files to/ })
- .should('be.visible')
- .within(() => {
- cy.findByRole('textbox', { name: 'Name' })
- .should('be.visible')
-
- cy.findByRole('textbox', { name: 'Name' })
- .type(`{selectall}${name}`)
-
- cy.findByRole('button', { name: 'Submit name' })
- .should('be.visible')
- .click()
- })
-
- cy.findByRole('dialog', { name: /Upload files to/ })
- .should('not.exist')
-}
-
-describe('Files', { testIsolation: true }, () => {
- const folderName = 'test-folder'
- let user: User
- let url = ''
-
- it('Login with a user and create a file request', () => {
- cy.createRandomUser().then((_user) => {
- user = _user
- cy.login(user)
- })
-
- cy.visit('/apps/files')
- createFolder(folderName)
-
- createFileRequest(`/${folderName}`)
- cy.get('@fileRequestUrl').should('contain', '/s/').then((_url: string) => {
- cy.logout()
- url = _url
- })
- })
-
- it('Open the file request as a guest', () => {
- cy.visit(url)
- enterGuestName('Guest')
-
- // Check various elements on the page
- cy.contains(`Upload files to ${folderName}`)
- .should('be.visible')
- cy.findByRole('button', { name: 'Upload' })
- .should('be.visible')
-
- cy.intercept('PUT', '/public.php/dav/files/*/*').as('uploadFile')
-
- // Upload a file
- cy.get('[data-cy-files-sharing-file-drop] input[type="file"]')
- .should('exist')
- .selectFile({
- contents: Cypress.Buffer.from('abcdef'),
- fileName: 'file.txt',
- mimeType: 'text/plain',
- lastModified: Date.now(),
- }, { force: true })
-
- cy.wait('@uploadFile').its('response.statusCode').should('eq', 201)
- })
-
- it('Check the uploaded file', () => {
- cy.login(user)
- cy.visit(`/apps/files/files?dir=/${folderName}`)
- getRowForFile('Guest')
- .should('be.visible')
- navigateToFolder('Guest')
- getRowForFile('file.txt').should('be.visible')
- })
-})
diff --git a/cypress/e2e/files_sharing/files-download.cy.ts b/cypress/e2e/files_sharing/files-download.cy.ts
deleted file mode 100644
index 9ee3bda069996..0000000000000
--- a/cypress/e2e/files_sharing/files-download.cy.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import {
- getActionButtonForFile,
- getActionEntryForFile,
- getRowForFile,
-} from '../files/FilesUtils.ts'
-import { createShare } from './FilesSharingUtils.ts'
-
-describe('files_sharing: Download forbidden', { testIsolation: true }, () => {
- let user: User
- let sharee: User
-
- beforeEach(() => {
- cy.runOccCommand('config:app:set --value yes core shareapi_allow_view_without_download')
- cy.createRandomUser().then(($user) => {
- user = $user
- })
- cy.createRandomUser().then(($user) => {
- sharee = $user
- })
- })
-
- after(() => {
- cy.runOccCommand('config:app:delete core shareapi_allow_view_without_download')
- })
-
- it('cannot download a folder if disabled', () => {
- // share the folder
- cy.mkdir(user, '/folder')
- cy.login(user)
- cy.visit('/apps/files')
- createShare('folder', sharee.userId, { read: true, download: false })
- cy.logout()
-
- // Now for the sharee
- cy.login(sharee)
-
- // visit shared files view
- cy.visit('/apps/files')
- // see the shared folder
- getActionButtonForFile('folder')
- .should('be.visible')
- // open the action menu
- .click({ force: true })
- // see no download action
- getActionEntryForFile('folder', 'download')
- .should('not.exist')
-
- // Disable view without download option
- cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
-
- // visit shared files view
- cy.visit('/apps/files')
- // see the shared folder
- getRowForFile('folder').should('be.visible')
- getActionButtonForFile('folder')
- .should('be.visible')
- // open the action menu
- .click({ force: true })
- getActionEntryForFile('folder', 'download').should('not.exist')
- })
-
- it('cannot download a file if disabled', () => {
- // share the folder
- cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
- cy.login(user)
- cy.visit('/apps/files')
- createShare('file.txt', sharee.userId, { read: true, download: false })
- cy.logout()
-
- // Now for the sharee
- cy.login(sharee)
-
- // visit shared files view
- cy.visit('/apps/files')
- // see the shared folder
- getActionButtonForFile('file.txt')
- .should('be.visible')
- // open the action menu
- .click({ force: true })
- // see no download action
- getActionEntryForFile('file.txt', 'download')
- .should('not.exist')
-
- // Disable view without download option
- cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
-
- // visit shared files view
- cy.visit('/apps/files')
- // see the shared folder
- getRowForFile('file.txt').should('be.visible')
- getActionButtonForFile('file.txt')
- .should('be.visible')
- // open the action menu
- .click({ force: true })
- getActionEntryForFile('file.txt', 'download').should('not.exist')
- })
-})
diff --git a/cypress/e2e/files_sharing/files-shares-view.cy.ts b/cypress/e2e/files_sharing/files-shares-view.cy.ts
deleted file mode 100644
index c20fad87a67ba..0000000000000
--- a/cypress/e2e/files_sharing/files-shares-view.cy.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { getRowForFile } from '../files/FilesUtils.ts'
-import { createShare } from './FilesSharingUtils.ts'
-
-describe('files_sharing: Files view', { testIsolation: true }, () => {
- let user: User
- let sharee: User
-
- beforeEach(() => {
- cy.createRandomUser().then(($user) => {
- user = $user
- })
- cy.createRandomUser().then(($user) => {
- sharee = $user
- })
- })
-
- /**
- * Regression test of https://github.com/nextcloud/server/issues/46108
- */
- it('opens a shared folder when clicking on it', () => {
- cy.mkdir(user, '/folder')
- cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/file')
- cy.login(user)
- cy.visit('/apps/files')
-
- // share the folder
- createShare('folder', sharee.userId, { read: true, download: true })
- // visit the own shares
- cy.visit('/apps/files/sharingout')
- // see the shared folder
- getRowForFile('folder').should('be.visible')
- // click on the folder should open it in files
- getRowForFile('folder').findByRole('button', { name: /open in files/i }).click()
- // See the URL has changed
- cy.url().should('match', /apps\/files\/files\/.+dir=\/folder/)
- // Content of the shared folder
- getRowForFile('file').should('be.visible')
-
- cy.logout()
- // Now for the sharee
- cy.login(sharee)
-
- // visit shared files view
- cy.visit('/apps/files/sharingin')
- // see the shared folder
- getRowForFile('folder').should('be.visible')
- // click on the folder should open it in files
- getRowForFile('folder').findByRole('button', { name: /open in files/i }).click()
- // See the URL has changed
- cy.url().should('match', /apps\/files\/files\/.+dir=\/folder/)
- // Content of the shared folder
- getRowForFile('file').should('be.visible')
- })
-})
diff --git a/cypress/e2e/files_sharing/limit_to_same_group.cy.ts b/cypress/e2e/files_sharing/limit_to_same_group.cy.ts
deleted file mode 100644
index 21e37ae745ef0..0000000000000
--- a/cypress/e2e/files_sharing/limit_to_same_group.cy.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { randomString } from '../../support/utils/randomString.ts'
-import { createShare } from './FilesSharingUtils.ts'
-
-describe('Limit to sharing to people in the same group', () => {
- let alice: User
- let bob: User
- let randomFileName1 = ''
- let randomFileName2 = ''
- let randomGroupName = ''
- let randomGroupName2 = ''
- let randomGroupName3 = ''
-
- before(() => {
- randomFileName1 = randomString(10) + '.txt'
- randomFileName2 = randomString(10) + '.txt'
- randomGroupName = randomString(10)
- randomGroupName2 = randomString(10)
- randomGroupName3 = randomString(10)
-
- cy.runOccCommand('config:app:set core shareapi_only_share_with_group_members --value yes')
-
- cy.createRandomUser()
- .then((user) => {
- alice = user
- })
- cy.createRandomUser()
- .then((user) => {
- bob = user
-
- cy.runOccCommand(`group:add ${randomGroupName}`)
- cy.runOccCommand(`group:add ${randomGroupName2}`)
- cy.runOccCommand(`group:add ${randomGroupName3}`)
- cy.runOccCommand(`group:adduser ${randomGroupName} ${alice.userId}`)
- cy.runOccCommand(`group:adduser ${randomGroupName} ${bob.userId}`)
- cy.runOccCommand(`group:adduser ${randomGroupName2} ${alice.userId}`)
- cy.runOccCommand(`group:adduser ${randomGroupName2} ${bob.userId}`)
- cy.runOccCommand(`group:adduser ${randomGroupName3} ${bob.userId}`)
-
- cy.uploadContent(alice, new Blob(['share to bob'], { type: 'text/plain' }), 'text/plain', `/${randomFileName1}`)
- cy.uploadContent(bob, new Blob(['share by bob'], { type: 'text/plain' }), 'text/plain', `/${randomFileName2}`)
-
- cy.login(alice)
- cy.visit('/apps/files')
- createShare(randomFileName1, bob.userId)
- cy.logout()
-
- cy.login(bob)
- cy.visit('/apps/files')
- createShare(randomFileName2, alice.userId)
- cy.logout()
- })
- })
-
- after(() => {
- cy.runOccCommand('config:app:set core shareapi_only_share_with_group_members --value no')
- })
-
- it('Alice can see the shared file', () => {
- cy.login(alice)
- cy.visit('/apps/files')
- cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName2}"]`).should('exist')
- })
-
- it('Bob can see the shared file', () => {
- cy.login(alice)
- cy.visit('/apps/files')
- cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName1}"]`).should('exist')
- })
-
- context('Bob is removed from the first group', () => {
- before(() => {
- cy.runOccCommand(`group:removeuser ${randomGroupName} ${bob.userId}`)
- })
-
- it('Alice can see the shared file', () => {
- cy.login(alice)
- cy.visit('/apps/files')
- cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName2}"]`).should('exist')
- })
-
- it('Bob can see the shared file', () => {
- cy.login(alice)
- cy.visit('/apps/files')
- cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName1}"]`).should('exist')
- })
- })
-
- context('Bob is removed from the second group', () => {
- before(() => {
- cy.runOccCommand(`group:removeuser ${randomGroupName2} ${bob.userId}`)
- })
-
- it('Alice cannot see the shared file', () => {
- cy.login(alice)
- cy.visit('/apps/files')
- cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName2}"]`).should('not.exist')
- })
-
- it('Bob cannot see the shared file', () => {
- cy.login(alice)
- cy.visit('/apps/files')
- cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName1}"]`).should('not.exist')
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/note-to-recipient.cy.ts b/cypress/e2e/files_sharing/note-to-recipient.cy.ts
deleted file mode 100644
index 989155a6608f9..0000000000000
--- a/cypress/e2e/files_sharing/note-to-recipient.cy.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { navigateToFolder } from '../files/FilesUtils.ts'
-import { createShare, openSharingPanel } from './FilesSharingUtils.ts'
-
-describe('files_sharing: Note to recipient', { testIsolation: true }, () => {
- let user: User
- let sharee: User
-
- beforeEach(() => {
- cy.createRandomUser().then(($user) => {
- user = $user
- })
- cy.createRandomUser().then(($user) => {
- sharee = $user
- })
- })
-
- it('displays the note to the sharee', () => {
- cy.mkdir(user, '/folder')
- cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/file')
- cy.login(user)
- cy.visit('/apps/files')
-
- // share the folder
- createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
-
- cy.logout()
- // Now for the sharee
- cy.login(sharee)
-
- // visit shared files view
- cy.visit('/apps/files')
- navigateToFolder('folder')
- cy.get('.note-to-recipient')
- .should('be.visible')
- .and('contain.text', 'Hello, this is the note.')
- })
-
- it('displays the note to the sharee even if the file list is empty', () => {
- cy.mkdir(user, '/folder')
- cy.login(user)
- cy.visit('/apps/files')
-
- // share the folder
- createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
-
- cy.logout()
- // Now for the sharee
- cy.login(sharee)
-
- // visit shared files view
- cy.visit('/apps/files')
- navigateToFolder('folder')
- cy.get('.note-to-recipient')
- .should('be.visible')
- .and('contain.text', 'Hello, this is the note.')
- })
-
- /**
- * Regression test for https://github.com/nextcloud/server/issues/46188
- */
- it('shows an existing note when editing a share', () => {
- cy.mkdir(user, '/folder')
- cy.login(user)
- cy.visit('/apps/files')
-
- // share the folder
- createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
-
- // reload just to be sure
- cy.visit('/apps/files')
-
- // open the sharing tab
- openSharingPanel('folder')
-
- cy.get('[data-cy-sidebar]').within(() => {
- // Open the share
- cy.get('[data-cy-files-sharing-share-actions]').first().click({ force: true })
-
- cy.findByRole('checkbox', { name: /note to recipient/i })
- .and('be.checked')
- cy.findByRole('textbox', { name: /note to recipient/i })
- .should('be.visible')
- .and('have.value', 'Hello, this is the note.')
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/PublicShareUtils.ts b/cypress/e2e/files_sharing/public-share/PublicShareUtils.ts
deleted file mode 100644
index 0577c0120fede..0000000000000
--- a/cypress/e2e/files_sharing/public-share/PublicShareUtils.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-import type { ShareOptions } from '../ShareOptionsType.ts'
-
-import { openSharingPanel } from '../FilesSharingUtils.ts'
-
-export interface ShareContext {
- user: User
- url?: string
-}
-
-const defaultShareContext: ShareContext = {
- user: {} as User,
- url: undefined,
-}
-
-/**
- * Retrieves the URL of the share.
- * Throws an error if the share context is not initialized properly.
- *
- * @param context The current share context (defaults to `defaultShareContext` if not provided).
- * @return The share URL.
- * @throws {Error} if the share context has no URL.
- */
-export function getShareUrl(context: ShareContext = defaultShareContext): string {
- if (!context.url) {
- throw new Error('You need to setup the share first!')
- }
- return context.url
-}
-
-/**
- * Setup the available data
- *
- * @param user The current share context
- * @param shareName The name of the shared folder
- */
-export function setupData(user: User, shareName: string): void {
- cy.mkdir(user, `/${shareName}`)
- cy.mkdir(user, `/${shareName}/subfolder`)
- cy.uploadContent(user, new Blob(['foo']), 'text/plain', `/${shareName}/foo.txt`)
- cy.uploadContent(user, new Blob(['bar']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
-}
-
-/**
- * Check the password state based on enforcement and default presence.
- *
- * @param enforced Whether the password is enforced.
- * @param alwaysAskForPassword Wether the password should always be asked for.
- */
-function checkPasswordState(enforced: boolean, alwaysAskForPassword: boolean) {
- if (enforced) {
- cy.contains('Password protection (enforced)').should('exist')
- } else if (alwaysAskForPassword) {
- cy.contains('Password protection').should('exist')
- }
- cy.contains('Enter a password')
- .should('exist')
- .and('not.be.disabled')
-}
-
-/**
- * Check the expiration date state based on enforcement and default presence.
- *
- * @param enforced Whether the expiration date is enforced.
- * @param hasDefault Whether a default expiration date is set.
- */
-function checkExpirationDateState(enforced: boolean, hasDefault: boolean) {
- if (enforced) {
- cy.contains('Enable link expiration (enforced)').should('exist')
- } else if (hasDefault) {
- cy.contains('Enable link expiration').should('exist')
- }
- cy.contains('Enter expiration date')
- .should('exist')
- .and('not.be.disabled')
- cy.get('input[data-cy-files-sharing-expiration-date-input]').should('exist')
- cy.get('input[data-cy-files-sharing-expiration-date-input]')
- .invoke('val')
- .then((val) => {
- expect(val).to.not.be.undefined
-
- const inputDate = new Date(typeof val === 'number' ? val : String(val))
- const expectedDate = new Date()
- expectedDate.setDate(expectedDate.getDate() + 2)
- expect(inputDate.toDateString()).to.eq(expectedDate.toDateString())
- })
-}
-
-/**
- * Create a public link share
- *
- * @param context The current share context
- * @param shareName The name of the shared folder
- * @param options The share options
- */
-export function createLinkShare(context: ShareContext, shareName: string, options: ShareOptions | null = null): Cypress.Chainable {
- cy.login(context.user)
- cy.visit('/apps/files')
- openSharingPanel(shareName)
-
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createLinkShare')
- cy.findByRole('button', { name: 'Create a new share link' }).click()
- // Conduct optional checks based on the provided options
- if (options) {
- cy.get('.sharing-entry__actions').should('be.visible') // Wait for the dialog to open
- checkPasswordState(options.enforcePassword ?? false, options.alwaysAskForPassword ?? false)
- checkExpirationDateState(options.enforceExpirationDate ?? false, options.defaultExpirationDateSet ?? false)
- cy.findByRole('button', { name: 'Create share' }).click()
- }
-
- return cy.wait('@createLinkShare')
- .should(({ response }) => {
- expect(response?.statusCode).to.eq(200)
- const url = response?.body?.ocs?.data?.url
- expect(url).to.match(/^https?:\/\//)
- context.url = url
- })
- .then(() => cy.wrap(context.url as string))
-}
-
-/**
- * open link share details for specific index
- *
- * @param index
- */
-export function openLinkShareDetails(index: number) {
- cy.findByRole('list', { name: 'Link shares' })
- .findAllByRole('listitem')
- .eq(index)
- .findByRole('button', { name: /Actions/i })
- .click()
- cy.findByRole('menuitem', { name: /Customize link/i }).click()
-}
-
-/**
- * Adjust share permissions to be editable
- */
-function adjustSharePermission(): void {
- openLinkShareDetails(0)
-
- cy.get('[data-cy-files-sharing-share-permissions-bundle]').should('be.visible')
- cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]').click()
-
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
- cy.findByRole('button', { name: 'Update share' }).click()
- cy.wait('@updateShare').its('response.statusCode').should('eq', 200)
-}
-
-/**
- * Setup a public share and backup the state.
- * If the setup was already done in another run, the state will be restored.
- *
- * @param shareName The name of the shared folder
- * @return The URL of the share
- */
-export function setupPublicShare(shareName = 'shared'): Cypress.Chainable {
- return cy.task('getVariable', { key: `public-share-data--${shareName}` })
- .then((data) => {
- const { dataSnapshot, shareUrl } = data as any || {}
- if (dataSnapshot) {
- cy.restoreState(dataSnapshot)
- defaultShareContext.url = shareUrl
- return cy.wrap(shareUrl as string)
- } else {
- const shareData: Record = {}
- return cy.createRandomUser()
- .then((user) => {
- defaultShareContext.user = user
- })
- .then(() => setupData(defaultShareContext.user, shareName))
- .then(() => createLinkShare(defaultShareContext, shareName))
- .then((url) => {
- shareData.shareUrl = url
- })
- .then(() => adjustSharePermission())
- .then(() => cy.saveState().then((snapshot) => {
- shareData.dataSnapshot = snapshot
- }))
- .then(() => cy.task('setVariable', { key: `public-share-data--${shareName}`, value: shareData }))
- .then(() => cy.log(`Public share setup, URL: ${shareData.shareUrl}`))
- .then(() => cy.wrap(defaultShareContext.url))
- }
- })
-}
diff --git a/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts b/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts
deleted file mode 100644
index 524cf3d3f8acd..0000000000000
--- a/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { copyFile, getRowForFile, moveFile, navigateToFolder } from '../../files/FilesUtils.ts'
-import { getShareUrl, setupPublicShare } from './PublicShareUtils.ts'
-
-describe('files_sharing: Public share - copy and move files', { testIsolation: true }, () => {
- beforeEach(() => {
- setupPublicShare()
- .then(() => cy.logout())
- .then(() => cy.visit(getShareUrl()))
- })
-
- it('Can copy a file to new folder', () => {
- getRowForFile('foo.txt').should('be.visible')
- getRowForFile('subfolder').should('be.visible')
-
- copyFile('foo.txt', 'subfolder')
-
- // still visible
- getRowForFile('foo.txt').should('be.visible')
- navigateToFolder('subfolder')
-
- cy.url().should('contain', 'dir=/subfolder')
- getRowForFile('foo.txt').should('be.visible')
- getRowForFile('bar.txt').should('be.visible')
- getRowForFile('subfolder').should('not.exist')
- })
-
- it('Can move a file to new folder', () => {
- getRowForFile('foo.txt').should('be.visible')
- getRowForFile('subfolder').should('be.visible')
-
- moveFile('foo.txt', 'subfolder')
-
- // wait until visible again
- getRowForFile('subfolder').should('be.visible')
-
- // file should be moved -> not exist anymore
- getRowForFile('foo.txt').should('not.exist')
- navigateToFolder('subfolder')
-
- cy.url().should('contain', 'dir=/subfolder')
- getRowForFile('foo.txt').should('be.visible')
- getRowForFile('subfolder').should('not.exist')
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/default-view.cy.ts b/cypress/e2e/files_sharing/public-share/default-view.cy.ts
deleted file mode 100644
index c09c6de5085c0..0000000000000
--- a/cypress/e2e/files_sharing/public-share/default-view.cy.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { getRowForFile } from '../../files/FilesUtils.ts'
-import { createLinkShare, setupData } from './PublicShareUtils.ts'
-
-describe('files_sharing: Public share - setting the default view mode', () => {
- let user: User
-
- beforeEach(() => {
- cy.createRandomUser()
- .then(($user) => (user = $user))
- .then(() => setupData(user, 'shared'))
- })
-
- it('is by default in list view', () => {
- const context = { user }
- createLinkShare(context, 'shared')
- .then((url) => {
- cy.logout()
- cy.visit(url!)
-
- // See file is visible
- getRowForFile('foo.txt').should('be.visible')
- // See we are in list view
- cy.findByRole('button', { name: 'Switch to grid view' })
- .should('be.visible')
- .and('not.be.disabled')
- })
- })
-
- it('can be toggled by user', () => {
- const context = { user }
- createLinkShare(context, 'shared')
- .then((url) => {
- cy.logout()
- cy.visit(url!)
-
- // See file is visible
- getRowForFile('foo.txt')
- .should('be.visible')
- // See we are in list view
- .find('.files-list__row-icon')
- .should(($el) => expect($el.outerWidth()).to.be.lessThan(99))
-
- // See the grid view toggle
- cy.findByRole('button', { name: 'Switch to grid view' })
- .should('be.visible')
- .and('not.be.disabled')
- // And can change to grid view
- .click()
-
- // See we are in grid view
- getRowForFile('foo.txt')
- .find('.files-list__row-icon')
- .should(($el) => expect($el.outerWidth()).to.be.greaterThan(99))
-
- // See the grid view toggle is now the list view toggle
- cy.findByRole('button', { name: 'Switch to list view' })
- .should('be.visible')
- .and('not.be.disabled')
- })
- })
-
- it('can be changed to default grid view', () => {
- const context = { user }
- createLinkShare(context, 'shared')
- .then((url) => {
- // Can set the "grid" view checkbox
- cy.findByRole('list', { name: 'Link shares' })
- .findAllByRole('listitem')
- .first()
- .findByRole('button', { name: /Actions/i })
- .click()
- cy.findByRole('menuitem', { name: /Customize link/i }).click()
- cy.findByRole('button', { name: /Advanced settings/i }).click()
- cy.findByRole('checkbox', { name: /Show files in grid view/i })
- .scrollIntoView()
- cy.findByRole('checkbox', { name: /Show files in grid view/i })
- .should('not.be.checked')
- .check({ force: true })
-
- // Wait for the share update
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
- cy.findByRole('button', { name: 'Update share' }).click()
- cy.wait('@updateShare').its('response.statusCode').should('eq', 200)
-
- // Logout and visit the share
- cy.logout()
- cy.visit(url!)
-
- // See file is visible
- getRowForFile('foo.txt').should('be.visible')
- // See we are in list view
- cy.findByRole('button', { name: 'Switch to list view' })
- .should('be.visible')
- .and('not.be.disabled')
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/download.cy.ts b/cypress/e2e/files_sharing/public-share/download.cy.ts
deleted file mode 100644
index 5c9c0ed7302bf..0000000000000
--- a/cypress/e2e/files_sharing/public-share/download.cy.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-import type { ShareContext } from './PublicShareUtils.ts'
-
-import { zipFileContains } from '../../../support/utils/assertions.ts'
-import { deleteDownloadsFolderBeforeEach } from '../../../support/utils/deleteDownloadsFolder.ts'
-import { getRowForFile, getRowForFileId, triggerActionForFile, triggerActionForFileId } from '../../files/FilesUtils.ts'
-import { createLinkShare, getShareUrl, openLinkShareDetails, setupPublicShare } from './PublicShareUtils.ts'
-
-describe('files_sharing: Public share - downloading files', { testIsolation: true }, () => {
- // in general there is no difference except downloading
- // as file shares have the source of the share token but a different displayname
- describe('file share', () => {
- let fileId: number
-
- before(() => {
- cy.createRandomUser().then((user) => {
- const context: ShareContext = { user }
- cy.uploadContent(user, new Blob(['foo']), 'text/plain', '/file.txt')
- .then(({ headers }) => { fileId = Number.parseInt(headers['oc-fileid']) })
- cy.login(user)
- createLinkShare(context, 'file.txt')
- .then(() => cy.logout())
- .then(() => cy.visit(context.url!))
- })
- })
-
- it('can download the file', () => {
- getRowForFileId(fileId)
- .should('be.visible')
- getRowForFileId(fileId)
- .find('[data-cy-files-list-row-name]')
- .should((el) => expect(el.text()).to.match(/file\s*\.txt/)) // extension is sparated so there might be a space between
- triggerActionForFileId(fileId, 'download')
- // check a file is downloaded with the correct name
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/file.txt`, 'utf-8', { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 5)
- .and('contain', 'foo')
- })
- })
-
- describe('folder share', () => {
- const shareName = 'a-folder-share'
-
- before(() => setupPublicShare(shareName))
-
- deleteDownloadsFolderBeforeEach()
-
- beforeEach(() => {
- cy.logout()
- cy.visit(getShareUrl())
- })
-
- it('Can download all files', () => {
- getRowForFile('foo.txt').should('be.visible')
-
- cy.get('[data-cy-files-list]').within(() => {
- cy.findByRole('checkbox', { name: /Toggle selection for all files/i })
- .should('exist')
- .check({ force: true })
-
- // see that two files are selected
- cy.contains('2 selected').should('be.visible')
-
- // click download
- cy.findByRole('button', { name: 'Download (selected)' })
- .should('be.visible')
- .click()
-
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 30)
- // Check all files are included
- .and(zipFileContains([
- 'foo.txt',
- 'subfolder/',
- 'subfolder/bar.txt',
- ]))
- })
- })
-
- it('Can download selected files', () => {
- getRowForFile('subfolder')
- .should('be.visible')
-
- cy.get('[data-cy-files-list]').within(() => {
- getRowForFile('subfolder')
- .findByRole('checkbox')
- .check({ force: true })
-
- // see that two files are selected
- cy.contains('1 selected').should('be.visible')
-
- // click download
- cy.findByRole('button', { name: 'Download (selected)' })
- .should('be.visible')
- .click()
-
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/subfolder.zip`, null, { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 30)
- // Check all files are included
- .and(zipFileContains([
- 'subfolder/',
- 'subfolder/bar.txt',
- ]))
- })
- })
-
- it('Can download folder by action', () => {
- getRowForFile('subfolder')
- .should('be.visible')
-
- cy.get('[data-cy-files-list]').within(() => {
- triggerActionForFile('subfolder', 'download')
-
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/subfolder.zip`, null, { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 30)
- // Check all files are included
- .and(zipFileContains([
- 'subfolder/',
- 'subfolder/bar.txt',
- ]))
- })
- })
-
- it('Can download file by action', () => {
- getRowForFile('foo.txt')
- .should('be.visible')
-
- cy.get('[data-cy-files-list]').within(() => {
- triggerActionForFile('foo.txt', 'download')
-
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 5)
- .and('contain', 'foo')
- })
- })
-
- it('Can download file by selection', () => {
- getRowForFile('foo.txt')
- .should('be.visible')
-
- cy.get('[data-cy-files-list]').within(() => {
- getRowForFile('foo.txt')
- .findByRole('checkbox')
- .check({ force: true })
-
- cy.findByRole('button', { name: 'Download (selected)' })
- .click()
-
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 5)
- .and('contain', 'foo')
- })
- })
- })
-
- describe('download permission - link share', () => {
- let context: ShareContext
- beforeEach(() => {
- cy.createRandomUser().then((user) => {
- cy.mkdir(user, '/test')
-
- context = { user }
- createLinkShare(context, 'test')
- cy.login(context.user)
- cy.visit('/apps/files')
- })
- })
-
- deleteDownloadsFolderBeforeEach()
-
- it('download permission is retained', () => {
- getRowForFile('test').should('be.visible')
- triggerActionForFile('test', 'details')
-
- openLinkShareDetails(0)
- cy.findByRole('button', { name: /advanced settings/i }).click()
-
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('update')
-
- cy.findByRole('checkbox', { name: /hide download/i })
- .should('exist')
- .and('not.be.checked')
- .check({ force: true })
- cy.findByRole('checkbox', { name: /hide download/i })
- .should('be.checked')
- cy.findByRole('button', { name: /update share/i })
- .click()
-
- cy.wait('@update')
-
- openLinkShareDetails(0)
- cy.findByRole('button', { name: /advanced settings/i }).click()
- cy.findByRole('checkbox', { name: /hide download/i })
- .should('be.checked')
-
- cy.reload()
-
- openLinkShareDetails(0)
- cy.findByRole('button', { name: /advanced settings/i }).click()
- cy.findByRole('checkbox', { name: /hide download/i })
- .should('be.checked')
- })
- })
-
- describe('download permission - mail share', () => {
- let user: User
-
- beforeEach(() => {
- cy.createRandomUser().then(($user) => {
- user = $user
- cy.mkdir(user, '/test')
- cy.login(user)
- cy.visit('/apps/files')
- })
- })
-
- it('download permission is retained', () => {
- getRowForFile('test').should('be.visible')
- triggerActionForFile('test', 'details')
-
- cy.findByRole('combobox', { name: /Enter external recipients/i })
- .type('test@example.com')
-
- cy.get('.option[sharetype="4"][user="test@example.com"]')
- .parent('li')
- .click()
- cy.findByRole('button', { name: /advanced settings/i })
- .should('be.visible')
- .click()
-
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('update')
-
- cy.findByRole('checkbox', { name: /hide download/i })
- .should('exist')
- .and('not.be.checked')
- .check({ force: true })
- cy.findByRole('button', { name: /save share/i })
- .click()
-
- cy.wait('@update')
-
- openLinkShareDetails(0)
- cy.findByRole('button', { name: /advanced settings/i })
- .click()
- cy.findByRole('checkbox', { name: /hide download/i })
- .should('exist')
- .and('be.checked')
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/header-avatar.cy.ts b/cypress/e2e/files_sharing/public-share/header-avatar.cy.ts
deleted file mode 100644
index c986ce634ac56..0000000000000
--- a/cypress/e2e/files_sharing/public-share/header-avatar.cy.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { ShareContext } from './PublicShareUtils.ts'
-
-import { createLinkShare, setupData } from './PublicShareUtils.ts'
-
-/**
- * This tests ensures that on public shares the header avatar menu correctly works
- */
-describe('files_sharing: Public share - header avatar menu', { testIsolation: true }, () => {
- let context: ShareContext
- let firstPublicShareUrl = ''
- let secondPublicShareUrl = ''
-
- before(() => {
- cy.createRandomUser()
- .then((user) => {
- context = {
- user,
- url: undefined,
- }
- setupData(context.user, 'public1')
- setupData(context.user, 'public2')
- createLinkShare(context, 'public1').then((shareUrl) => {
- firstPublicShareUrl = shareUrl
- cy.log(`Created first share with URL: ${shareUrl}`)
- })
- createLinkShare(context, 'public2').then((shareUrl) => {
- secondPublicShareUrl = shareUrl
- cy.log(`Created second share with URL: ${shareUrl}`)
- })
- })
- })
-
- beforeEach(() => {
- cy.logout()
- cy.visit(firstPublicShareUrl)
- })
-
- it('See the undefined avatar menu', () => {
- cy.get('header')
- .findByRole('navigation', { name: /User menu/i })
- .should('be.visible')
- .findByRole('button', { name: /User menu/i })
- .should('be.visible')
- .click()
- cy.get('#header-menu-public-page-user-menu')
- .as('headerMenu')
-
- // Note that current guest user is not identified
- cy.get('@headerMenu')
- .should('be.visible')
- .findByRole('note')
- .should('be.visible')
- .should('contain', 'not identified')
-
- // Button to set guest name
- cy.get('@headerMenu')
- .findByRole('link', { name: /Set public name/i })
- .should('be.visible')
- })
-
- it('Can set public name', () => {
- cy.get('header')
- .findByRole('navigation', { name: /User menu/i })
- .should('be.visible')
- .findByRole('button', { name: /User menu/i })
- .should('be.visible')
- .as('userMenuButton')
-
- // Open the user menu
- cy.get('@userMenuButton').click()
- cy.get('#header-menu-public-page-user-menu')
- .as('headerMenu')
-
- cy.get('@headerMenu')
- .findByRole('link', { name: /Set public name/i })
- .should('be.visible')
- .click()
-
- // Check the dialog is visible
- cy.findByRole('dialog', { name: /Guest identification/i })
- .should('be.visible')
- .as('guestIdentificationDialog')
-
- // Check the note is visible
- cy.get('@guestIdentificationDialog')
- .findByRole('note')
- .should('contain', 'not identified')
-
- // Check the input is visible
- cy.get('@guestIdentificationDialog')
- .findByRole('textbox', { name: /Name/i })
- .should('be.visible')
- .type('{selectAll}John Doe{enter}')
-
- // Check that the dialog is closed
- cy.get('@guestIdentificationDialog')
- .should('not.exist')
-
- // Check that the avatar changed
- cy.get('@userMenuButton')
- .find('img')
- .invoke('attr', 'src')
- .should('include', 'avatar/guest/John%20Doe')
- })
-
- it('Guest name us persistent and can be changed', () => {
- cy.get('header')
- .findByRole('navigation', { name: /User menu/i })
- .should('be.visible')
- .findByRole('button', { name: /User menu/i })
- .should('be.visible')
- .as('userMenuButton')
-
- // Open the user menu
- cy.get('@userMenuButton').click()
- cy.get('#header-menu-public-page-user-menu')
- .as('headerMenu')
-
- cy.get('@headerMenu')
- .findByRole('link', { name: /Set public name/i })
- .should('be.visible')
- .click()
-
- // Check the dialog is visible
- cy.findByRole('dialog', { name: /Guest identification/i })
- .should('be.visible')
- .as('guestIdentificationDialog')
-
- // Set the name
- cy.get('@guestIdentificationDialog')
- .findByRole('textbox', { name: /Name/i })
- .should('be.visible')
- .type('{selectAll}Jane Doe{enter}')
-
- // Check that the dialog is closed
- cy.get('@guestIdentificationDialog')
- .should('not.exist')
-
- // Create another share
- cy.visit(secondPublicShareUrl)
-
- cy.get('header')
- .findByRole('navigation', { name: /User menu/i })
- .should('be.visible')
- .findByRole('button', { name: /User menu/i })
- .should('be.visible')
- .as('userMenuButton')
-
- // Open the user menu
- cy.get('@userMenuButton').click()
- cy.get('#header-menu-public-page-user-menu')
- .as('headerMenu')
-
- // See the note with the current name
- cy.get('@headerMenu')
- .findByRole('note')
- .should('contain', 'Your guest name: Jane Doe')
-
- cy.get('@headerMenu')
- .findByRole('link', { name: /Change public name/i })
- .should('be.visible')
- .click()
-
- // Check the dialog is visible
- cy.findByRole('dialog', { name: /Guest identification/i })
- .should('be.visible')
- .as('guestIdentificationDialog')
-
- // Check that the note states the current name
- // cy.get('@guestIdentificationDialog')
- // .findByRole('note')
- // .should('contain', 'are currently identified as Jane Doe')
-
- // Change the name
- cy.get('@guestIdentificationDialog')
- .findByRole('textbox', { name: /Name/i })
- .should('be.visible')
- .type('{selectAll}Foo Bar{enter}')
-
- // Check that the dialog is closed
- cy.get('@guestIdentificationDialog')
- .should('not.exist')
-
- // Check that the avatar changed with the second name
- cy.get('@userMenuButton')
- .find('img')
- .invoke('attr', 'src')
- .should('include', 'avatar/guest/Foo%20Bar')
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/header-menu.cy.ts b/cypress/e2e/files_sharing/public-share/header-menu.cy.ts
deleted file mode 100644
index 56da2d2e2e000..0000000000000
--- a/cypress/e2e/files_sharing/public-share/header-menu.cy.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { haveValidity, zipFileContains } from '../../../support/utils/assertions.ts'
-import { getShareUrl, setupPublicShare } from './PublicShareUtils.ts'
-
-/**
- * This tests ensures that on public shares the header actions menu correctly works
- */
-describe('files_sharing: Public share - header actions menu', { testIsolation: true }, () => {
- before(() => setupPublicShare())
- beforeEach(() => {
- cy.logout()
- cy.visit(getShareUrl())
- })
-
- it('Can download all files', () => {
- cy.get('header')
- .findByRole('button', { name: 'Download' })
- .should('be.visible')
- cy.get('header')
- .findByRole('button', { name: 'Download' })
- .click()
-
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/shared.zip`, null, { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 30)
- // Check all files are included
- .and(zipFileContains([
- 'shared/',
- 'shared/foo.txt',
- 'shared/subfolder/',
- 'shared/subfolder/bar.txt',
- ]))
- })
-
- it('Can copy direct link', () => {
- // Check the button
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .should('be.visible')
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .click()
- // See the menu
- cy.findByRole('menu', { name: /More action/i })
- .should('be.visible')
- // see correct link in item
- cy.findByRole('menuitem', { name: 'Direct link' })
- .should('be.visible')
- .and('have.attr', 'href')
- .then((attribute) => expect(attribute).to.match(new RegExp(`^${Cypress.env('baseUrl')}/public.php/dav/files/.+/?accept=zip$`)))
- // see menu closes on click
- cy.findByRole('menuitem', { name: 'Direct link' })
- .click()
- cy.findByRole('menu', { name: /More actions/i })
- .should('not.exist')
- })
-
- it('Can create federated share', () => {
- // Check the button
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .should('be.visible')
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .click()
- // See the menu
- cy.findByRole('menu', { name: /More action/i })
- .should('be.visible')
- // see correct button
- cy.findByRole('menuitem', { name: /Add to your/i })
- .should('be.visible')
- .click()
- // see the dialog
- cy.findByRole('dialog', { name: /Add to your Nextcloud/i })
- .should('be.visible')
- cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).within(() => {
- cy.findByRole('textbox')
- .type('user@nextcloud.local')
- // create share
- cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare')
- .as('createFederatedShare')
- cy.findByRole('button', { name: 'Create share' })
- .click()
- cy.wait('@createFederatedShare')
- })
- })
-
- it('Has user feedback while creating federated share', () => {
- // Check the button
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .should('be.visible')
- .click()
- // see correct button
- cy.findByRole('menuitem', { name: /Add to your/i })
- .should('be.visible')
- .click()
- // see the dialog
- cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible').within(() => {
- cy.findByRole('textbox')
- .type('user@nextcloud.local')
- // intercept request, the request is continued when the promise is resolved
- const { promise, resolve } = Promise.withResolvers()
- cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare', (request) => {
- // we need to wait in the onResponse handler as the intercept handler times out otherwise
- request.on('response', async (response) => {
- await promise
- response.statusCode = 503
- })
- }).as('createFederatedShare')
-
- // create the share
- cy.findByRole('button', { name: 'Create share' })
- .click()
- // see that while the share is created the button is disabled
- cy.findByRole('button', { name: 'Create share' })
- .should('be.disabled')
- .then(() => {
- // continue the request
- resolve(null)
- })
- cy.wait('@createFederatedShare')
- // see that the button is no longer disabled
- cy.findByRole('button', { name: 'Create share' })
- .should('not.be.disabled')
- })
- })
-
- it('Has input validation for federated share', () => {
- // Check the button
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .should('be.visible')
- .click()
- // see correct button
- cy.findByRole('menuitem', { name: /Add to your/i })
- .should('be.visible')
- .click()
- // see the dialog
- cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible').within(() => {
- // Check domain only
- cy.findByRole('textbox')
- .type('nextcloud.local')
- cy.findByRole('textbox')
- .should(haveValidity(/user/i))
- // Check no valid domain
- cy.findByRole('textbox')
- .type('{selectAll}user@invalid')
- cy.findByRole('textbox')
- .should(haveValidity(/invalid.+url/i))
- })
- })
-
- it('See primary action is moved to menu on small screens', () => {
- cy.viewport(490, 490)
- // Check the button does not exist
- cy.get('header').within(() => {
- cy.findByRole('button', { name: 'Direct link' })
- .should('not.exist')
- cy.findByRole('button', { name: 'Download' })
- .should('not.exist')
- cy.findByRole('button', { name: /Add to your/i })
- .should('not.exist')
- // Open the menu
- cy.findByRole('button', { name: /More actions/i })
- .should('be.visible')
- .click()
- })
-
- // See correct number of menu item
- cy.findByRole('menu', { name: 'More actions' })
- .findAllByRole('menuitem')
- .should('have.length', 3)
- cy.findByRole('menu', { name: 'More actions' })
- .within(() => {
- // See that download, federated share and direct link are moved to the menu
- cy.findByRole('menuitem', { name: /^Download/ })
- .should('be.visible')
- cy.findByRole('menuitem', { name: /Add to your/i })
- .should('be.visible')
- cy.findByRole('menuitem', { name: 'Direct link' })
- .should('be.visible')
-
- // See that direct link works
- cy.findByRole('menuitem', { name: 'Direct link' })
- .should('be.visible')
- .and('have.attr', 'href')
- .then((attribute) => expect(attribute).to.match(new RegExp(`^${Cypress.env('baseUrl')}/public.php/dav/files/.+/?accept=zip$`)))
- // See remote share works
- cy.findByRole('menuitem', { name: /Add to your/i })
- .should('be.visible')
- .click()
- })
- cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible')
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/rename-files.cy.ts b/cypress/e2e/files_sharing/public-share/rename-files.cy.ts
deleted file mode 100644
index a0fa5492be571..0000000000000
--- a/cypress/e2e/files_sharing/public-share/rename-files.cy.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { getRowForFile, haveValidity, triggerActionForFile } from '../../files/FilesUtils.ts'
-import { getShareUrl, setupPublicShare } from './PublicShareUtils.ts'
-
-describe('files_sharing: Public share - renaming files', { testIsolation: true }, () => {
- beforeEach(() => {
- setupPublicShare()
- .then(() => cy.logout())
- .then(() => cy.visit(getShareUrl()))
- })
-
- it('can rename a file', () => {
- // All are visible by default
- getRowForFile('foo.txt').should('be.visible')
-
- triggerActionForFile('foo.txt', 'rename')
-
- getRowForFile('foo.txt')
- .findByRole('textbox', { name: 'Filename' })
- .should('be.visible')
- .type('{selectAll}other.txt')
- .should(haveValidity(''))
- .type('{enter}')
-
- // See it is renamed
- getRowForFile('other.txt').should('be.visible')
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts b/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts
deleted file mode 100644
index dd11bd0b2cfac..0000000000000
--- a/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { ShareOptions } from '../ShareOptionsType.ts'
-import type { ShareContext } from './PublicShareUtils.ts'
-
-import { defaultShareOptions } from '../ShareOptionsType.ts'
-import { createLinkShare, setupData } from './PublicShareUtils.ts'
-
-describe('files_sharing: Before create checks', () => {
- let shareContext: ShareContext
-
- before(() => {
- // Setup data for the shared folder once before all tests
- cy.createRandomUser().then((randomUser) => {
- shareContext = {
- user: randomUser,
- }
- })
- })
-
- afterEach(() => {
- cy.runOccCommand('config:app:delete core shareapi_enable_link_password_by_default')
- cy.runOccCommand('config:app:delete core shareapi_enforce_links_password')
- cy.runOccCommand('config:app:delete core shareapi_default_expire_date')
- cy.runOccCommand('config:app:delete core shareapi_enforce_expire_date')
- cy.runOccCommand('config:app:delete core shareapi_expire_after_n_days')
- })
-
- const applyShareOptions = (options: ShareOptions = defaultShareOptions): void => {
- cy.runOccCommand(`config:app:set --value ${options.alwaysAskForPassword ? 'yes' : 'no'} core shareapi_enable_link_password_by_default`)
- cy.runOccCommand(`config:app:set --value ${options.enforcePassword ? 'yes' : 'no'} core shareapi_enforce_links_password`)
- cy.runOccCommand(`config:app:set --value ${options.enforceExpirationDate ? 'yes' : 'no'} core shareapi_enforce_expire_date`)
- cy.runOccCommand(`config:app:set --value ${options.defaultExpirationDateSet ? 'yes' : 'no'} core shareapi_default_expire_date`)
- if (options.defaultExpirationDateSet) {
- cy.runOccCommand('config:app:set --value 2 core shareapi_expire_after_n_days')
- }
- }
-
- it('Checks if user can create share when both password and expiration date are enforced', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: true,
- enforceExpirationDate: true,
- defaultExpirationDateSet: true,
- }
- applyShareOptions(shareOptions)
- const shareName = 'passwordAndExpireEnforced'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share when password is enforced and expiration date has a default set', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: true,
- defaultExpirationDateSet: true,
- }
- applyShareOptions(shareOptions)
- const shareName = 'passwordEnforcedDefaultExpire'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share when password is optionally requested and expiration date is enforced', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- defaultExpirationDateSet: true,
- enforceExpirationDate: true,
- }
- applyShareOptions(shareOptions)
- const shareName = 'defaultPasswordExpireEnforced'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share when password is optionally requested and expiration date have defaults set', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- defaultExpirationDateSet: true,
- }
- applyShareOptions(shareOptions)
- const shareName = 'defaultPasswordAndExpire'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share with password enforced and expiration date set but not enforced', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: true,
- defaultExpirationDateSet: true,
- enforceExpirationDate: false,
- }
- applyShareOptions(shareOptions)
- const shareName = 'passwordEnforcedExpireSetNotEnforced'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create a share when both password and expiration date have default values but are both not enforced', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: false,
- defaultExpirationDateSet: true,
- enforceExpirationDate: false,
- }
- applyShareOptions(shareOptions)
- const shareName = 'defaultPasswordAndExpirationNotEnforced'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share with password not enforced but expiration date enforced', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: false,
- defaultExpirationDateSet: true,
- enforceExpirationDate: true,
- }
- applyShareOptions(shareOptions)
- const shareName = 'noPasswordExpireEnforced'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share with password not enforced and expiration date has a default set', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: false,
- defaultExpirationDateSet: true,
- enforceExpirationDate: false,
- }
- applyShareOptions(shareOptions)
- const shareName = 'defaultExpireNoPasswordEnforced'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share with expiration date set and password not enforced', () => {
- const shareOptions: ShareOptions = {
- alwaysAskForPassword: true,
- enforcePassword: false,
- defaultExpirationDateSet: true,
- }
- applyShareOptions(shareOptions)
-
- const shareName = 'noPasswordExpireDefault'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-
- it('Checks if user can create share with password not enforced, expiration date not enforced, and no defaults set', () => {
- applyShareOptions()
- const shareName = 'noPasswordNoExpireNoDefaults'
- setupData(shareContext.user, shareName)
- createLinkShare(shareContext, shareName, null).then((shareUrl) => {
- shareContext.url = shareUrl
- cy.log(`Created share with URL: ${shareUrl}`)
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts b/cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts
deleted file mode 100644
index 0430ca9b2d186..0000000000000
--- a/cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { createLinkShare, openLinkShareDetails } from './PublicShareUtils.ts'
-
-describe('files_sharing: sidebar tab', () => {
- let alice: User
-
- beforeEach(() => {
- cy.createRandomUser()
- .then((user) => {
- alice = user
- cy.mkdir(user, '/test')
- cy.login(user)
- cy.visit('/apps/files')
- })
- })
-
- /**
- * Regression tests of https://github.com/nextcloud/server/issues/53566
- * Where the ' char was shown as '
- */
- it('correctly lists shares by label with special characters', () => {
- createLinkShare({ user: alice }, 'test')
- openLinkShareDetails(0)
- cy.findByRole('button', { name: /advanced settings/i }).click()
- cy.findByRole('textbox', { name: /share label/i })
- .should('be.visible')
- .type('Alice\' share')
-
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('PUT')
- cy.findByRole('button', { name: /update share/i }).click()
- cy.wait('@PUT')
-
- // see the label is shown correctly
- cy.findByRole('list', { name: /link shares/i })
- .findAllByRole('listitem')
- .should('have.length', 1)
- .first()
- .should('contain.text', 'Share link (Alice\' share)')
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts b/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts
deleted file mode 100644
index c3c289774ccb2..0000000000000
--- a/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { getRowForFile } from '../../files/FilesUtils.ts'
-import { openSharingPanel } from '../FilesSharingUtils.ts'
-
-describe('files_sharing: Public share - File drop', { testIsolation: true }, () => {
- let shareUrl: string
- let user: string
- const shareName = 'shared'
-
- before(() => {
- cy.createRandomUser().then(($user) => {
- user = $user.userId
- cy.mkdir($user, `/${shareName}`)
- cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/foo.txt`)
- cy.login($user)
- // open the files app
- cy.visit('/apps/files')
- // open the sidebar
- openSharingPanel(shareName)
- // create the share
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
- cy.findByRole('button', { name: 'Create a new share link' })
- .click()
- // extract the link
- cy.wait('@createShare').should(({ response }) => {
- const { ocs } = response?.body ?? {}
- shareUrl = ocs?.data.url
- expect(shareUrl).to.match(/^http:\/\//)
- })
-
- // Update the share to be a file drop
- cy.findByRole('list', { name: 'Link shares' })
- .findAllByRole('listitem')
- .first()
- .findByRole('button', { name: /Actions/i })
- .click()
- cy.findByRole('menuitem', { name: /Customize link/i })
- .should('be.visible')
- .click()
- cy.get('[data-cy-files-sharing-share-permissions-bundle]')
- .should('be.visible')
- cy.get('[data-cy-files-sharing-share-permissions-bundle="file-drop"]')
- .click()
-
- // save the update
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
- cy.findByRole('button', { name: 'Update share' })
- .click()
- cy.wait('@updateShare')
- })
- })
-
- beforeEach(() => {
- cy.logout()
- cy.visit(shareUrl)
- })
-
- it('Cannot see share content', () => {
- cy.contains(`Upload files to ${shareName}`)
- .should('be.visible')
-
- // foo exists
- cy.userFileExists(user, `${shareName}/foo.txt`).should('be.gt', 0)
- // but is not visible
- getRowForFile('foo.txt')
- .should('not.exist')
- })
-
- it('Can only see upload files and upload folders menu entries', () => {
- cy.contains(`Upload files to ${shareName}`)
- .should('be.visible')
-
- cy.findByRole('button', { name: 'New' })
- .should('be.visible')
- .click()
- // See upload actions
- cy.findByRole('menuitem', { name: 'Upload files' })
- .should('be.visible')
- cy.findByRole('menuitem', { name: 'Upload folders' })
- .should('be.visible')
- // But no other
- cy.findByRole('menu')
- .findAllByRole('menuitem')
- .should('have.length', 2)
- })
-
- it('Can only see dedicated upload button', () => {
- cy.contains(`Upload files to ${shareName}`)
- .should('be.visible')
-
- cy.findByRole('button', { name: 'Upload' })
- .should('be.visible')
- .click()
- // See upload actions
- cy.findByRole('menuitem', { name: 'Upload files' })
- .should('be.visible')
- cy.findByRole('menuitem', { name: 'Upload folders' })
- .should('be.visible')
- // But no other
- cy.findByRole('menu')
- .findAllByRole('menuitem')
- .should('have.length', 2)
- })
-
- it('Can upload files', () => {
- cy.contains(`Upload files to ${shareName}`)
- .should('be.visible')
-
- const { promise, resolve } = Promise.withResolvers()
- cy.intercept('PUT', '**/public.php/dav/files/**', (request) => {
- if (request.url.includes('first.txt')) {
- // just continue the first one
- request.continue()
- } else {
- // We delay the second one until we checked that the progress bar is visible
- request.on('response', async () => {
- await promise
- })
- }
- }).as('uploadFile')
-
- cy.get('[data-cy-files-sharing-file-drop] input[type="file"]')
- .should('exist')
- .selectFile([
- { fileName: 'first.txt', contents: Buffer.from('8 bytes!') },
- { fileName: 'second.md', contents: Buffer.from('x'.repeat(128)) },
- ], { force: true })
-
- cy.wait('@uploadFile')
-
- cy.findByRole('progressbar')
- .should('be.visible')
- .and((el) => { expect(Number.parseInt(el.attr('value') ?? '0')).be.gte(50) })
- // continue second request
- .then(() => resolve(null))
-
- cy.wait('@uploadFile')
-
- // Check files uploaded
- cy.userFileExists(user, `${shareName}/first.txt`).should('eql', 8)
- cy.userFileExists(user, `${shareName}/second.md`).should('eql', 128)
- })
-
- describe('Terms of service', { testIsolation: true }, () => {
- before(() => cy.runOccCommand('config:app:set --value \'TEST: Some disclaimer text\' --type string core shareapi_public_link_disclaimertext'))
- beforeEach(() => cy.visit(shareUrl))
- after(() => cy.runOccCommand('config:app:delete core shareapi_public_link_disclaimertext'))
-
- it('shows ToS on file-drop view', () => {
- cy.get('[data-cy-files-sharing-file-drop]')
- .contains(`Upload files to ${shareName}`)
- .should('be.visible')
- cy.get('[data-cy-files-sharing-file-drop]')
- .contains('agree to the terms of service')
- .should('be.visible')
- cy.findByRole('button', { name: /Terms of service/i })
- .should('be.visible')
- .click()
-
- cy.findByRole('dialog', { name: 'Terms of service' })
- .should('contain.text', 'TEST: Some disclaimer text')
- // close
- .findByRole('button', { name: 'Close' })
- .click()
-
- cy.findByRole('dialog', { name: 'Terms of service' })
- .should('not.exist')
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts b/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts
deleted file mode 100644
index 0545e7d88db6d..0000000000000
--- a/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { getActionButtonForFile, getRowForFile, navigateToFolder } from '../../files/FilesUtils.ts'
-import { openSharingPanel } from '../FilesSharingUtils.ts'
-
-describe('files_sharing: Public share - View only', { testIsolation: true }, () => {
- let shareUrl: string
- const shareName = 'shared'
-
- before(() => {
- cy.createRandomUser().then(($user) => {
- cy.mkdir($user, `/${shareName}`)
- cy.mkdir($user, `/${shareName}/subfolder`)
- cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/foo.txt`)
- cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/subfolder/bar.txt`)
- cy.login($user)
- // open the files app
- cy.visit('/apps/files')
- // open the sidebar
- openSharingPanel(shareName)
- // create the share
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
- cy.findByRole('button', { name: 'Create a new share link' })
- .click()
- // extract the link
- cy.wait('@createShare').should(({ response }) => {
- const { ocs } = response?.body ?? {}
- shareUrl = ocs?.data.url
- expect(shareUrl).to.match(/^http:\/\//)
- })
-
- // Update the share to be a view-only-no-download share
- cy.findByRole('list', { name: 'Link shares' })
- .findAllByRole('listitem')
- .first()
- .findByRole('button', { name: /Actions/i })
- .click()
- cy.findByRole('menuitem', { name: /Customize link/i })
- .should('be.visible')
- .click()
- cy.get('[data-cy-files-sharing-share-permissions-bundle]')
- .should('be.visible')
- cy.get('[data-cy-files-sharing-share-permissions-bundle="read-only"]')
- .click()
- cy.findByRole('button', { name: /advanced settings/i }).click()
- cy.findByRole('checkbox', { name: 'Hide download' })
- .check({ force: true })
- // save the update
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
- cy.findByRole('button', { name: 'Update share' })
- .click()
- cy.wait('@updateShare')
- })
- })
-
- beforeEach(() => {
- cy.logout()
- cy.visit(shareUrl)
- })
-
- it('Can see the files list', () => {
- // foo exists
- getRowForFile('foo.txt')
- .should('be.visible')
- })
-
- it('But no actions available', () => {
- // foo exists
- getRowForFile('foo.txt')
- .should('be.visible')
- // but no actions
- getActionButtonForFile('foo.txt')
- .should('not.exist')
-
- // TODO: We really need Viewer in the server repo.
- // So we could at least test viewing images
- })
-
- it('Can navigate to subfolder', () => {
- getRowForFile('subfolder')
- .should('be.visible')
-
- navigateToFolder('subfolder')
-
- getRowForFile('bar.txt')
- .should('be.visible')
-
- // but also no actions
- getActionButtonForFile('bar.txt')
- .should('not.exist')
- })
-
- it('Cannot upload files', () => {
- // wait for file list to be ready
- getRowForFile('foo.txt')
- .should('be.visible')
- })
-})
diff --git a/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts b/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts
deleted file mode 100644
index f2363defcee82..0000000000000
--- a/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { getActionButtonForFile, getRowForFile, navigateToFolder } from '../../files/FilesUtils.ts'
-import { openSharingPanel } from '../FilesSharingUtils.ts'
-
-describe('files_sharing: Public share - View only', { testIsolation: true }, () => {
- let shareUrl: string
- const shareName = 'shared'
-
- before(() => {
- cy.createRandomUser().then(($user) => {
- cy.mkdir($user, `/${shareName}`)
- cy.mkdir($user, `/${shareName}/subfolder`)
- cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/foo.txt`)
- cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
- cy.login($user)
- // open the files app
- cy.visit('/apps/files')
- // open the sidebar
- openSharingPanel(shareName)
- // create the share
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
- cy.findByRole('button', { name: 'Create a new share link' })
- .click()
- // extract the link
- cy.wait('@createShare').should(({ response }) => {
- const { ocs } = response?.body ?? {}
- shareUrl = ocs?.data.url
- expect(shareUrl).to.match(/^http:\/\//)
- })
-
- // Update the share to be a view-only-no-download share
- cy.findByRole('list', { name: 'Link shares' })
- .findAllByRole('listitem')
- .first()
- .findByRole('button', { name: /Actions/i })
- .click()
- cy.findByRole('menuitem', { name: /Customize link/i })
- .should('be.visible')
- .click()
- cy.get('[data-cy-files-sharing-share-permissions-bundle]')
- .should('be.visible')
- cy.get('[data-cy-files-sharing-share-permissions-bundle="read-only"]')
- .click()
- // save the update
- cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
- cy.findByRole('button', { name: 'Update share' })
- .click()
- cy.wait('@updateShare')
- })
- })
-
- beforeEach(() => {
- cy.logout()
- cy.visit(shareUrl)
- })
-
- it('Can see the files list', () => {
- // foo exists
- getRowForFile('foo.txt')
- .should('be.visible')
- })
-
- it('Can navigate to subfolder', () => {
- getRowForFile('subfolder')
- .should('be.visible')
-
- navigateToFolder('subfolder')
-
- getRowForFile('bar.txt')
- .should('be.visible')
- })
-
- it('Cannot upload files', () => {
- // wait for file list to be ready
- getRowForFile('foo.txt')
- .should('be.visible')
- })
-
- it('Only download action is actions available', () => {
- getActionButtonForFile('foo.txt')
- .should('be.visible')
- .click()
-
- // Only the download action
- cy.findByRole('menuitem', { name: 'Download' })
- .should('be.visible')
- cy.findAllByRole('menuitem')
- .should('have.length', 1)
-
- // Can download
- cy.findByRole('menuitem', { name: 'Download' }).click()
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 5)
- .and('contain', 'content')
- })
-})
diff --git a/cypress/e2e/files_sharing/share-permissions-bundle.cy.ts b/cypress/e2e/files_sharing/share-permissions-bundle.cy.ts
deleted file mode 100644
index dec8173c4a9c1..0000000000000
--- a/cypress/e2e/files_sharing/share-permissions-bundle.cy.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { openSharingPanel } from './FilesSharingUtils.ts'
-
-describe('files_sharing: Share permissions bundle configuration', () => {
- let alice: User
- let bob: User
-
- before(() => {
- cy.createRandomUser().then(($user) => {
- alice = $user
- })
- cy.createRandomUser().then(($user) => {
- bob = $user
- })
- })
-
- beforeEach(() => {
- cy.runOccCommand('config:app:delete files_sharing shareapi_exclude_reshare_from_edit')
- })
-
- after(() => {
- cy.runOccCommand('config:app:delete files_sharing shareapi_exclude_reshare_from_edit')
- })
-
- /**
- * Helper to create a user share and select "Allow editing"
- */
- function createUserShareWithEdit(itemName: string) {
- openSharingPanel(itemName)
-
- cy.get('#app-sidebar-vue').within(() => {
- cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('shareeSearch')
- cy.findByRole('combobox', { name: /Search for internal recipients/i })
- .type(`{selectAll}${bob.userId}`)
- cy.wait('@shareeSearch')
- })
-
- cy.get(`[user="${bob.userId}"]`).click()
-
- // Select "Allow editing" permission bundle
- cy.get('[data-cy-files-sharing-share-permissions-bundle]').should('be.visible')
- cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]').click()
-
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
- cy.findByRole('button', { name: 'Save share' }).click()
-
- return cy.wait('@createShare')
- }
-
- describe('Default behavior (SHARE included in edit)', () => {
- it('Creates user share with "Allow editing" with SHARE permission for folders', () => {
- const folderName = 'test-folder-with-share'
- cy.mkdir(alice, `/${folderName}`)
- cy.login(alice)
- cy.visit('/apps/files')
-
- createUserShareWithEdit(folderName).should(({ response }) => {
- // Verify permission value is 31 (ALL with SHARE: READ=1 + UPDATE=2 + CREATE=4 + DELETE=8 + SHARE=16)
- expect(response?.body?.ocs?.data?.permissions).to.equal(31)
- })
- })
-
- it('Creates user share with "Allow editing" with SHARE permission for files', () => {
- const fileName = 'test-file-with-share.txt'
- cy.uploadContent(alice, new Blob(['content']), 'text/plain', `/${fileName}`)
- cy.login(alice)
- cy.visit('/apps/files')
-
- createUserShareWithEdit(fileName).should(({ response }) => {
- // Verify permission value is 19 (ALL_FILE with SHARE: READ=1 + UPDATE=2 + SHARE=16)
- expect(response?.body?.ocs?.data?.permissions).to.equal(19)
- })
- })
- })
-
- describe('With SHARE excluded from edit (config enabled)', () => {
- beforeEach(() => {
- cy.runOccCommand('config:app:set --value yes files_sharing shareapi_exclude_reshare_from_edit')
- })
-
- it('Creates user share with "Allow editing" without SHARE permission for folders', () => {
- const folderName = 'test-folder-no-share'
- cy.mkdir(alice, `/${folderName}`)
- cy.login(alice)
- cy.visit('/apps/files')
-
- createUserShareWithEdit(folderName).should(({ response }) => {
- // Verify permission value is 15 (ALL without SHARE: READ=1 + UPDATE=2 + CREATE=4 + DELETE=8)
- expect(response?.body?.ocs?.data?.permissions).to.equal(15)
- })
- })
-
- it('Creates user share with "Allow editing" without SHARE permission for files', () => {
- const fileName = 'test-file-no-share.txt'
- cy.uploadContent(alice, new Blob(['content']), 'text/plain', `/${fileName}`)
- cy.login(alice)
- cy.visit('/apps/files')
-
- createUserShareWithEdit(fileName).should(({ response }) => {
- // Verify permission value is 3 (ALL_FILE without SHARE: READ=1 + UPDATE=2)
- expect(response?.body?.ocs?.data?.permissions).to.equal(3)
- })
- })
- })
-})
diff --git a/cypress/e2e/files_sharing/share-status-action.cy.ts b/cypress/e2e/files_sharing/share-status-action.cy.ts
deleted file mode 100644
index 30a76334b091c..0000000000000
--- a/cypress/e2e/files_sharing/share-status-action.cy.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-import { closeSidebar, enableGridMode, getActionButtonForFile, getActionsForFile, getInlineActionEntryForFile, getRowForFile } from '../files/FilesUtils.ts'
-import { createShare } from './FilesSharingUtils.ts'
-
-describe('files_sharing: Sharing status action', { testIsolation: true }, () => {
- /**
- * Regression test of https://github.com/nextcloud/server/issues/45723
- */
- it('No "shared" tag when user ID is purely numerical but there are no shares', () => {
- const user = {
- language: 'en',
- password: 'test1234',
- userId: String(Math.floor(Math.random() * 1000)),
- } as User
- cy.createUser(user)
- cy.mkdir(user, '/folder')
- cy.login(user)
-
- cy.visit('/apps/files')
-
- getRowForFile('folder').should('be.visible')
- getActionsForFile('folder')
- .findByRole('button', { name: 'Shared' })
- .should('not.exist')
- })
-
- it('Render quick option for sharing', () => {
- cy.createRandomUser().then((user) => {
- cy.mkdir(user, '/folder')
- cy.login(user)
-
- cy.visit('/apps/files')
- })
-
- getRowForFile('folder').should('be.visible')
- getActionsForFile('folder')
- .findByRole('button', { name: /Sharing options/ })
- .should('be.visible')
- .click({ force: true })
-
- // check the click opened the sidebar
- cy.get('[data-cy-sidebar]')
- .should('be.visible')
- // and ensure the sharing tab is selected
- .findByRole('tab', { name: 'Sharing', selected: true })
- .should('exist')
- })
-
- describe('Sharing inline status action handling', () => {
- let user: User
- let sharee: User
-
- before(() => {
- cy.createRandomUser().then(($user) => {
- sharee = $user
- })
- cy.createRandomUser().then(($user) => {
- user = $user
- cy.mkdir(user, '/folder')
- cy.login(user)
-
- cy.visit('/apps/files')
- getRowForFile('folder').should('be.visible')
-
- createShare('folder', sharee.userId)
- closeSidebar()
- })
- cy.logout()
- })
-
- it('Render inline status action for sharer', () => {
- cy.login(user)
- cy.visit('/apps/files')
-
- getInlineActionEntryForFile('folder', 'sharing-status')
- .should('have.attr', 'aria-label', `Shared with ${sharee.userId}`)
- .should('have.attr', 'title', `Shared with ${sharee.userId}`)
- .should('be.visible')
- })
-
- it('Render status action in gridview for sharer', () => {
- cy.login(user)
- cy.visit('/apps/files')
- enableGridMode()
-
- getRowForFile('folder')
- .should('be.visible')
- getActionButtonForFile('folder')
- .click()
- cy.findByRole('menu')
- .findByRole('menuitem', { name: /shared with/i })
- .should('be.visible')
- })
-
- it('Render inline status action for sharee', () => {
- cy.login(sharee)
- cy.visit('/apps/files')
-
- getInlineActionEntryForFile('folder', 'sharing-status')
- .should('have.attr', 'aria-label', `Shared by ${user.userId}`)
- .should('be.visible')
- })
-
- it('Render status action in grid view for sharee', () => {
- cy.login(sharee)
- cy.visit('/apps/files')
-
- enableGridMode()
-
- getRowForFile('folder')
- .should('be.visible')
- getActionButtonForFile('folder')
- .click()
- cy.findByRole('menu')
- .findByRole('menuitem', { name: `Shared by ${user.userId}` })
- .should('be.visible')
- })
- })
-})
diff --git a/cypress/e2e/settings/usersUtils.ts b/cypress/e2e/settings/usersUtils.ts
deleted file mode 100644
index c9bc7800fd1ae..0000000000000
--- a/cypress/e2e/settings/usersUtils.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import type { User } from '@nextcloud/e2e-test-server/cypress'
-
-/**
- * Assert that `element` does not exist or is not visible
- * Useful in cases such as when NcModal is opened/closed rapidly
- *
- * @param element Element that is inspected
- */
-export function assertNotExistOrNotVisible(element: JQuery) {
- const doesNotExist = element.length === 0
- const isNotVisible = !element.is(':visible')
-
- expect(doesNotExist || isNotVisible, 'does not exist or is not visible').to.be.true
-}
-
-/**
- * Get the settings users list
- *
- * @return Cypress chainable object
- */
-export function getUserList() {
- return cy.get('[data-cy-user-list]')
-}
-
-/**
- * Get the row entry for given userId within the settings users list
- *
- * @param userId the user to query
- * @return Cypress chainable object
- */
-export function getUserListRow(userId: string) {
- return getUserList().find(`[data-cy-user-row="${userId}"]`)
-}
-
-/**
- *
- * @param selector
- */
-export function waitLoading(selector: string) {
- // We need to make sure the element is loading, otherwise the "done loading" will succeed even if we did not start loading.
- // But Cypress might also be simply too slow to catch the loading phase. Thats why we need to wait in this case.
- // eslint-disable-next-line cypress/no-unnecessary-waiting
- cy.get(`${selector}[data-loading]`).if().should('exist').else().wait(1000)
- // https://github.com/NoriSte/cypress-wait-until/issues/75#issuecomment-572685623
- cy.waitUntil(() => Cypress.$(selector).length > 0 && !Cypress.$(selector).attr('data-loading')?.length, { timeout: 10000 })
-}
-
-/**
- * Open the edit dialog for a user by clicking the Edit action on their row
- *
- * @param user The user whose edit dialog to open
- */
-export function openEditDialog(user: User) {
- getUserListRow(user.userId).should('exist')
- .find('[data-cy-user-list-action-edit]')
- .click({ force: true })
- // Wait for the dialog to appear
- cy.get('.edit-dialog [data-test="form"]').should('be.visible')
-}
-
-/**
- * Save the currently open edit dialog by clicking the Save button
- * and wait for the dialog to close
- */
-export function saveEditDialog() {
- cy.get('[data-test="submit"]').click()
- // Wait for dialog to close
- cy.get('.edit-dialog').should('not.exist')
-}
diff --git a/cypress/e2e/systemtags/utils.ts b/cypress/e2e/systemtags/utils.ts
deleted file mode 100644
index 295a5307c6398..0000000000000
--- a/cypress/e2e/systemtags/utils.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
-
-export function addTagToFile(fileName: string, newTag: string): void {
- getRowForFile(fileName).should('be.visible')
- triggerActionForFile(fileName, 'systemtags:bulk')
-
- createNewTagInDialog(newTag)
-}
-
-export function createNewTagInDialog(newTag: string): void {
- cy.intercept('POST', '/remote.php/dav/systemtags').as('createTag')
- cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
- cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
-
- cy.get('[data-cy-systemtags-picker-input]').type(newTag)
-
- cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 0)
- cy.get('[data-cy-systemtags-picker-button-create]').should('be.visible')
- cy.get('[data-cy-systemtags-picker-button-create]').click()
-
- cy.wait('@createTag')
- // Verify the new tag is selected by default
- cy.get('[data-cy-systemtags-picker-tag]').contains(newTag)
- .parents('[data-cy-systemtags-picker-tag]')
- .findByRole('checkbox', { hidden: true }).should('be.checked')
-
- // Apply changes
- cy.get('[data-cy-systemtags-picker-button-submit]').click()
-
- cy.wait('@assignTagData')
- cy.get('[data-cy-systemtags-picker]').should('not.exist')
-}
diff --git a/cypress/e2e/theming/themingUtils.ts b/cypress/e2e/theming/themingUtils.ts
deleted file mode 100644
index 59487920e586a..0000000000000
--- a/cypress/e2e/theming/themingUtils.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { colord } from 'colord'
-
-export const defaultPrimary = '#00679e'
-export const defaultBackground = 'jo-myoung-hee-fluid.webp'
-
-/**
- * Check if a CSS variable is set to a specific color
- *
- * @param variable Variable to check
- * @param expectedColor Color that is expected
- */
-export function validateCSSVariable(variable: string, expectedColor: string) {
- const value = window.getComputedStyle(Cypress.$('body').get(0)).getPropertyValue(variable)
- console.debug(`${variable}, is: ${colord(value).toHex()} expected: ${expectedColor}`)
- return colord(value).isEqual(expectedColor)
-}
-
-/**
- * Validate the current page body css variables
- *
- * @param expectedColor the expected primary color
- * @param expectedBackground the expected background
- * @param expectedBackgroundColor the expected background color (null to ignore)
- */
-export function validateBodyThemingCss(expectedColor = defaultPrimary, expectedBackground: string | null = defaultBackground, expectedBackgroundColor: string | null = defaultPrimary) {
- // We must use `Cypress.$` here as any assertions (get is an assertion) is not allowed in wait-until's check function, see documentation
- const guestBackgroundColor = Cypress.$('body').css('background-color')
- const guestBackgroundImage = Cypress.$('body').css('background-image')
-
- const isValidBackgroundColor = expectedBackgroundColor === null || colord(guestBackgroundColor).isEqual(expectedBackgroundColor)
- const isValidBackgroundImage = !expectedBackground
- ? guestBackgroundImage === 'none'
- : guestBackgroundImage.includes(expectedBackground)
-
- console.debug({
- isValidBackgroundColor,
- isValidBackgroundImage,
- guestBackgroundColor: colord(guestBackgroundColor).toHex(),
- guestBackgroundImage,
- })
-
- return isValidBackgroundColor && isValidBackgroundImage && validateCSSVariable('--color-primary', expectedColor)
-}
-
-/**
- * Check background color of element
- *
- * @param element JQuery element to check
- * @param color expected color
- */
-export function expectBackgroundColor(element: JQuery, color: string) {
- expect(colord(element.css('background-color')).toHex()).equal(colord(color).toHex())
-}
-
-/**
- * Validate the user theming default select option css
- *
- * @param expectedColor the expected color
- * @param expectedBackground the expected background
- */
-export function validateUserThemingDefaultCss(expectedColor = defaultPrimary, expectedBackground: string | null = defaultBackground) {
- const backgroundImage = Cypress.$('body').css('background-image')
- const backgroundColor = Cypress.$('body').css('background-color')
-
- const isValidBackgroundImage = !expectedBackground
- ? (backgroundImage === 'none' || Cypress.$('body').css('background-image') === 'none')
- : backgroundImage.includes(expectedBackground)
-
- console.debug({
- colorPickerOptionColor: colord(backgroundColor).toHex(),
- expectedColor,
- isValidBackgroundImage,
- backgroundImage,
- })
-
- return isValidBackgroundImage && colord(backgroundColor).isEqual(expectedColor)
-}
-
-/**
- * @param trigger - The color picker trigger
- * @param index - The color index to pick, if not provided a random one will be picked
- */
-export function pickColor(trigger: Cypress.Chainable, index?: number): Cypress.Chainable {
- // Pick one of the first 8 options
- const randColour = index ?? Math.floor(Math.random() * 8)
-
- let oldColor = ''
- trigger.as('trigger').then(($el) => {
- oldColor = $el.css('background-color')
- })
-
- cy.get('@trigger').scrollIntoView()
- cy.get('@trigger').click({ force: true })
-
- // Click on random color
- cy.get('.color-picker__simple-color-circle').eq(randColour).click()
-
- // Wait for color change
- cy.get('@trigger')
- .should(($el) => $el.css('background-color') !== oldColor)
-
- cy.findByRole('button', { name: /Choose/i }).click()
-
- // Get the selected color from the color preview block
- return cy.get('@trigger').then(($el) => $el.css('background-color'))
-}
diff --git a/cypress/fixtures/app.config.php b/cypress/fixtures/app.config.php
deleted file mode 100644
index 162b8616f2d1c..0000000000000
--- a/cypress/fixtures/app.config.php
+++ /dev/null
@@ -1,20 +0,0 @@
- [
- [
- 'path' => '/var/www/html/apps',
- 'url' => '/apps',
- 'writable' => false,
- ],
- [
- 'path' => '/var/www/html/apps-cypress',
- 'url' => '/apps-cypress',
- 'writable' => true,
- ],
- ],
-];
diff --git a/cypress/fixtures/appstore/apps.json b/cypress/fixtures/appstore/apps.json
deleted file mode 100644
index e0b0a5ee5d1c9..0000000000000
--- a/cypress/fixtures/appstore/apps.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "ocs": {
- "meta": {
- "status": "ok",
- "statuscode": 200,
- "message": "OK"
- },
- "data": [
- {
- "id": "calendar",
- "name": "Calendar",
- "isCompatible": true,
- "active": false,
- "installed": false,
- "internal": false
- },
- {
- "id": "contacts",
- "name": "Contacts",
- "isCompatible": true,
- "active": false,
- "installed": false,
- "internal": false
- }
- ]
- }
-}
diff --git a/cypress/fixtures/testapp/appinfo/info.xml b/cypress/fixtures/testapp/appinfo/info.xml
deleted file mode 100644
index a0deada5329c2..0000000000000
--- a/cypress/fixtures/testapp/appinfo/info.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- testapp
- Test App
- Test App
-
- 0.0.1
- agpl
- Ferdinand Thiessen
- TestApp
- games
- https://github.com/nextcloud/server/issues
-
-
-
-
-
- Test App
- testapp.page.index
-
-
- Test App 2
- testapp.page.index
-
-
-
diff --git a/cypress/fixtures/testapp/appinfo/routes.php b/cypress/fixtures/testapp/appinfo/routes.php
deleted file mode 100644
index b5471c5a0b260..0000000000000
--- a/cypress/fixtures/testapp/appinfo/routes.php
+++ /dev/null
@@ -1,12 +0,0 @@
- [
- ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
- ]
-];
diff --git a/cypress/fixtures/testapp/img/app.svg b/cypress/fixtures/testapp/img/app.svg
deleted file mode 100644
index 42b64b58d325a..0000000000000
--- a/cypress/fixtures/testapp/img/app.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
diff --git a/cypress/fixtures/testapp/lib/AppInfo/Application.php b/cypress/fixtures/testapp/lib/AppInfo/Application.php
deleted file mode 100644
index 94f6e936a8790..0000000000000
--- a/cypress/fixtures/testapp/lib/AppInfo/Application.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
diff --git a/cypress/pages/FilesFilters.ts b/cypress/pages/FilesFilters.ts
deleted file mode 100644
index 9a10f39a5dc1e..0000000000000
--- a/cypress/pages/FilesFilters.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-/**
- * Page object model for the files filters
- */
-export class FilesFilterPage {
- /**
- * Get the filters menu button (only on narrow and medium widths)
- */
- getFiltersMenuToggle() {
- return cy.get('[data-test-id="files-list-filters"]')
- .findByRole('button', { name: 'Filters' })
- }
-
- /**
- * Get and trigger the filter within the menu (only on narrow and medium widths)
- *
- * @param name - The name of the filter button
- */
- triggerFilterMenu(name: string | RegExp) {
- cy.get('[data-test-id="files-list-filters"]')
- .findByRole('button', { name: 'Filters' })
- .should('be.visible')
- .as('filtersMenuToggle')
- .click()
-
- cy.get('@filtersMenuToggle')
- .should('have.attr', 'aria-expanded', 'true')
-
- cy.findByRole('menu')
- .should('be.visible')
- .findByRole('menuitem', { name })
- .should('be.visible')
- .click()
- }
-
- /**
- * Get and trigger the filter button if the files list is wide enough to show all filters
- *
- * @param name - The name of the filter button
- */
- triggerFilterButton(name: string | RegExp) {
- cy.get('[data-test-id="files-list-filters"]')
- .findByRole('button', { name })
- .should('be.visible')
- .click()
- }
-
- triggerFilter(name: string | RegExp) {
- cy.get('[data-cy-files-list]')
- .should('be.visible')
- .if(($el) => expect($el.get(0).clientWidth).to.be.gte(1024))
- .then(() => this.triggerFilterButton(name))
- .else()
- .then(() => this.triggerFilterMenu(name))
- }
-
- closeFilterMenu() {
- cy.get('[data-test-id="files-list-filters"]')
- .findAllByRole('button')
- .filter('[aria-expanded="true"]')
- .click({ multiple: true })
- }
-
- activeFiltersList() {
- return cy.findByRole('list', { name: 'Active filters' })
- }
-
- activeFilters() {
- return this.activeFiltersList().findAllByRole('listitem')
- }
-
- removeFilter(name: string | RegExp) {
- const el = typeof name === 'string'
- ? this.activeFilters().should('contain.text', name)
- : this.activeFilters().should('match', name)
- el.should('exist')
- // click the button
- el.findByRole('button', { name: 'Remove filter' })
- .should('exist')
- .click({ force: true })
- }
-}
diff --git a/cypress/pages/FilesNavigation.ts b/cypress/pages/FilesNavigation.ts
deleted file mode 100644
index b9a48d9f1333b..0000000000000
--- a/cypress/pages/FilesNavigation.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-/**
- * Page object model for the files app navigation
- */
-export class FilesNavigationPage {
- navigation() {
- return cy.findByRole('navigation', { name: 'Files' })
- }
-
- searchInput() {
- return this.navigation().findByRole('searchbox')
- }
-
- searchScopeTrigger() {
- return this.navigation().findByRole('button', { name: /search scope options/i })
- }
-
- /**
- * Only available after clicking on the search scope trigger
- */
- searchScopeMenu() {
- return cy.findByRole('menu', { name: /search scope options/i })
- }
-
- searchClearButton() {
- return this.navigation().findByRole('button', { name: /clear search/i })
- }
-
- settingsToggle() {
- return this.navigation().findByRole('link', { name: 'Files settings' })
- }
-
- views() {
- return this.navigation().findByRole('list', { name: 'Views' })
- }
-
- quota() {
- return this.navigation().find('[data-cy-files-navigation-settings-quota]')
- }
-}
diff --git a/cypress/pages/NavigationHeader.ts b/cypress/pages/NavigationHeader.ts
deleted file mode 100644
index e911a597ce4c5..0000000000000
--- a/cypress/pages/NavigationHeader.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*!
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-/**
- * Page object model for the Nextcloud navigation header.
- *
- * The app launcher (waffle menu) is an NcPopover whose content is teleported
- * to , so the menu items do not live inside the