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 = {} - 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 @@ - - - - - - - image/svg+xml - - - - - - - - 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