From a020d581470c2d68fe7efcff3b78efdafd13c6bd Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 3 Aug 2026 22:06:03 +0200 Subject: [PATCH 1/2] test: migrate to Playwright Backport of all changes from `master` to migrate from Cypress to Playwright. Signed-off-by: Ferdinand Thiessen --- .github/workflows/cypress.yml | 280 -- .github/workflows/playwright.yml | 325 ++ .nextcloudignore | 3 +- REUSE.toml | 118 +- apps/files/src/actions/moveOrCopyAction.ts | 2 + build/files-checker.php | 3 +- build/frontend-legacy/eslint.config.mjs | 14 +- cypress.config.ts | 211 -- cypress/e2e/appstore/apps.cy.ts | 279 -- cypress/e2e/core-utils.ts | 117 - cypress/e2e/core/404-error.cy.ts | 19 - cypress/e2e/core/header_access-levels.cy.ts | 101 - cypress/e2e/core/header_app-menu.cy.ts | 58 - cypress/e2e/core/header_contacts-menu.cy.ts | 135 - cypress/e2e/core/setup.ts | 213 -- .../e2e/dashboard/widget-performance.cy.ts | 41 - cypress/e2e/dav/availability.cy.ts | 129 - cypress/e2e/files/FilesUtils.ts | 453 --- cypress/e2e/files/LivePhotosUtils.ts | 116 - cypress/e2e/files/drag-n-drop.cy.ts | 246 -- .../files/duplicated-node-regression.cy.ts | 33 - cypress/e2e/files/favorites.cy.ts | 163 - cypress/e2e/files/files-actions.cy.ts | 70 - cypress/e2e/files/files-copy-move.cy.ts | 176 - cypress/e2e/files/files-delete.cy.ts | 70 - cypress/e2e/files/files-download.cy.ts | 333 -- cypress/e2e/files/files-filtering.cy.ts | 280 -- cypress/e2e/files/files-navigation.cy.ts | 56 - cypress/e2e/files/files-renaming.cy.ts | 288 -- cypress/e2e/files/files-selection.cy.ts | 77 - cypress/e2e/files/files-settings.cy.ts | 159 - cypress/e2e/files/files-sidebar.cy.ts | 137 - cypress/e2e/files/files-sorting.cy.ts | 330 -- cypress/e2e/files/files-xml-regression.cy.ts | 51 - cypress/e2e/files/files.cy.ts | 58 - cypress/e2e/files/hotkeys.cy.ts | 113 - cypress/e2e/files/live_photos.cy.ts | 175 - cypress/e2e/files/new-menu.cy.ts | 122 - cypress/e2e/files/recent-view.cy.ts | 45 - cypress/e2e/files/router-query.cy.ts | 181 - cypress/e2e/files/scrolling.cy.ts | 215 -- cypress/e2e/files/search.cy.ts | 217 -- cypress/e2e/files_external/StorageUtils.ts | 74 - .../files-external-failed.cy.ts | 85 - .../files-user-credentials.cy.ts | 148 - .../home-folder-root-mount-permissions.cy.ts | 49 - cypress/e2e/files_external/settings.cy.ts | 157 - .../e2e/files_sharing/FilesSharingUtils.ts | 216 -- cypress/e2e/files_sharing/ShareOptionsType.ts | 18 - cypress/e2e/files_sharing/expiry-date.cy.ts | 130 - cypress/e2e/files_sharing/file-request.cy.ts | 84 - .../e2e/files_sharing/files-copy-move.cy.ts | 156 - .../e2e/files_sharing/files-download.cy.ts | 103 - .../e2e/files_sharing/files-shares-view.cy.ts | 60 - .../files_sharing/limit_to_same_group.cy.ts | 112 - .../e2e/files_sharing/note-to-recipient.cy.ts | 92 - .../public-share/PublicShareUtils.ts | 188 - .../public-share/copy-move-files.cy.ts | 48 - .../public-share/default-view.cy.ts | 103 - .../files_sharing/public-share/download.cy.ts | 272 -- .../public-share/header-avatar.cy.ts | 194 - .../public-share/header-menu.cy.ts | 201 -- .../public-share/rename-files.cy.ts | 31 - .../public-share/required-before-create.cy.ts | 191 - .../public-share/sidebar-tab.cy.ts | 46 - .../public-share/view_file-drop.cy.ts | 173 - .../view_view-only-no-download.cy.ts | 100 - .../public-share/view_view-only.cy.ts | 102 - .../share-permissions-bundle.cy.ts | 111 - .../files_sharing/share-status-action.cy.ts | 124 - .../files_trashbin/files-trash-action.cy.ts | 69 - cypress/e2e/files_trashbin/files.cy.ts | 137 - .../e2e/files_versions/filesVersionsUtils.ts | 97 - .../e2e/files_versions/version_creation.cy.ts | 48 - .../version_cross_share_move_and_copy.cy.ts | 103 - .../e2e/files_versions/version_deletion.cy.ts | 61 - .../e2e/files_versions/version_download.cy.ts | 59 - .../files_versions/version_expiration.cy.ts | 57 - .../e2e/files_versions/version_naming.cy.ts | 91 - .../files_versions/version_restoration.cy.ts | 75 - .../e2e/files_versions/version_sharing.cy.ts | 48 - cypress/e2e/login/login-redirect.cy.ts | 60 - cypress/e2e/login/login.cy.ts | 153 - cypress/e2e/login/webauth.cy.ts | 152 - cypress/e2e/settings/access-levels.cy.ts | 73 - cypress/e2e/settings/personal-info.cy.ts | 449 --- cypress/e2e/settings/users-group-admin.cy.ts | 186 - cypress/e2e/settings/users.cy.ts | 131 - cypress/e2e/settings/usersUtils.ts | 74 - cypress/e2e/settings/users_columns.cy.ts | 99 - cypress/e2e/settings/users_disable.cy.ts | 79 - cypress/e2e/settings/users_groups.cy.ts | 305 -- cypress/e2e/settings/users_manager.cy.ts | 82 - cypress/e2e/settings/users_modify.cy.ts | 176 - cypress/e2e/settings/users_search.cy.ts | 117 - cypress/e2e/systemtags/admin-settings.cy.ts | 116 - .../e2e/systemtags/files-bulk-action.cy.ts | 468 --- .../e2e/systemtags/files-inline-action.cy.ts | 78 - cypress/e2e/systemtags/files-sidebar.cy.ts | 41 - cypress/e2e/systemtags/files-view.cy.ts | 42 - cypress/e2e/systemtags/utils.ts | 37 - cypress/e2e/theming/a11y-color-contrast.cy.ts | 166 - .../theming/admin-settings_background.cy.ts | 385 -- .../e2e/theming/admin-settings_branding.cy.ts | 219 -- .../e2e/theming/admin-settings_colors.cy.ts | 67 - .../theming/admin-settings_default-app.cy.ts | 130 - cypress/e2e/theming/themingUtils.ts | 110 - .../e2e/theming/user-settings_app-order.cy.ts | 302 -- .../theming/user-settings_background.cy.ts | 270 -- cypress/fixtures/app.config.php | 20 - cypress/fixtures/appstore/apps.json | 27 - cypress/fixtures/testapp/appinfo/info.xml | 31 - cypress/fixtures/testapp/appinfo/routes.php | 12 - cypress/fixtures/testapp/img/app.svg | 60 - .../testapp/lib/AppInfo/Application.php | 18 - .../testapp/lib/Controller/PageController.php | 27 - cypress/fixtures/testapp/templates/main.php | 8 - cypress/pages/FilesFilters.ts | 86 - cypress/pages/FilesNavigation.ts | 44 - cypress/pages/NavigationHeader.ts | 117 - cypress/pages/SettingsAppOrderList.ts | 43 - cypress/pages/UnifiedSearch.ts | 73 - cypress/support/commands.ts | 285 -- cypress/support/commonUtils.ts | 68 - cypress/support/cypress-e2e.d.ts | 62 - cypress/support/e2e.ts | 28 - cypress/support/utils/assertions.ts | 44 - .../support/utils/deleteDownloadsFolder.ts | 11 - cypress/support/utils/randomString.ts | 19 - cypress/tsconfig.json | 14 - eslint.config.js | 21 +- package-lock.json | 3142 +---------------- package.json | 18 +- playwright.config.ts | 69 + tests/data/REUSE.toml | 124 + .../fixtures => tests/data/images}/image.jpg | Bin tests/playwright/README.md | 165 + .../e2e/appstore/admin-settings-apps.spec.ts | 210 ++ tests/playwright/e2e/core/404-error.spec.ts | 21 + .../e2e/core/header-access-levels.spec.ts | 60 + .../e2e/core/header-app-menu.spec.ts | 82 + .../e2e/core/header-contacts-menu.spec.ts | 120 + tests/playwright/e2e/core/setup.spec.ts | 180 + .../e2e/dashboard/widget-performance.spec.ts | 37 + tests/playwright/e2e/dav/availability.spec.ts | 96 + .../playwright/e2e/files/drag-n-drop.spec.ts | 125 + .../files/duplicated-node-regression.spec.ts | 31 + .../e2e/files/files-actions.spec.ts | 41 + .../e2e/files/files-copy-move.spec.ts | 156 + .../playwright/e2e/files/files-delete.spec.ts | 54 + .../e2e/files/files-download.spec.ts | 251 ++ .../e2e/files/files-favorites.spec.ts | 110 + .../e2e/files/files-filtering.spec.ts | 155 + .../e2e/files/files-navigation.spec.ts | 51 + .../e2e/files/files-renaming.spec.ts | 238 ++ .../e2e/files/files-selection.spec.ts | 63 + .../e2e/files/files-settings.spec.ts | 98 + .../e2e/files/files-sidebar.spec.ts | 110 + .../e2e/files/files-sorting.spec.ts | 178 + .../e2e/files/files-xml-regression.spec.ts | 53 + tests/playwright/e2e/files/files.spec.ts | 43 + tests/playwright/e2e/files/hotkeys.spec.ts | 74 + .../playwright/e2e/files/live-photos.spec.ts | 185 + tests/playwright/e2e/files/new-menu.spec.ts | 63 + .../playwright/e2e/files/recent-view.spec.ts | 38 + .../playwright/e2e/files/router-query.spec.ts | 108 + tests/playwright/e2e/files/scrolling.spec.ts | 93 + tests/playwright/e2e/files/search.spec.ts | 144 + .../admin-settings-external-storage.spec.ts | 82 + ...in-settings-home-folder-root-mount.spec.ts | 49 + .../admin-settings-user-credentials.spec.ts | 161 + .../files-external-failed.spec.ts | 76 + .../admin-settings-download-forbidden.spec.ts | 67 + .../admin-settings-expiry-date.spec.ts | 135 + ...admin-settings-limit-to-same-group.spec.ts | 99 + .../admin-settings-permissions-bundle.spec.ts | 90 + .../e2e/files_sharing/file-request.spec.ts | 79 + .../e2e/files_sharing/files-copy-move.spec.ts | 83 + .../files_sharing/files-shares-view.spec.ts | 40 + .../files_sharing/note-to-recipient.spec.ts | 55 + .../admin-settings-file-drop-terms.spec.ts | 44 + ...in-settings-required-before-create.spec.ts | 176 + .../copy-move-rename-files.spec.ts | 67 + .../public-share/default-view.spec.ts | 50 + .../public-share/download.spec.ts | 123 + .../public-share/file-drop.spec.ts | 98 + .../public-share/header-avatar.spec.ts | 56 + .../public-share/header-menu.spec.ts | 117 + .../public-share/share-editor.spec.ts | 82 + .../public-share/view-only.spec.ts | 90 + .../files_sharing/share-status-action.spec.ts | 91 + .../files_trashbin/files-trash-action.spec.ts | 54 + .../e2e/files_trashbin/files.spec.ts | 102 + .../files_versions/version-creation.spec.ts | 28 + .../version-cross-share-move-and-copy.spec.ts | 176 + .../files_versions/version-deletion.spec.ts | 66 + .../files_versions/version-download.spec.ts | 58 + .../files_versions/version-expiration.spec.ts | 62 + .../e2e/files_versions/version-naming.spec.ts | 79 + .../version-restoration.spec.ts | 88 + .../files_versions/version-sharing.spec.ts | 33 + .../e2e/login/login-redirect.spec.ts | 53 + tests/playwright/e2e/login/login.spec.ts | 88 + tests/playwright/e2e/login/webauth.spec.ts | 116 + .../e2e/settings/access-levels.spec.ts | 42 + .../e2e/settings/personal-info.spec.ts | 446 +++ .../admin-settings-restrictions.spec.ts | 48 + .../e2e/systemtags/admin-settings.spec.ts | 95 + .../e2e/systemtags/files-bulk-action.spec.ts | 145 + .../systemtags/files-inline-action.spec.ts | 74 + .../e2e/systemtags/files-sidebar.spec.ts | 35 + .../e2e/systemtags/files-view.spec.ts | 43 + .../e2e/theming/a11y-color-contrast.spec.ts | 117 + .../theming/admin-settings-background.spec.ts | 123 + .../theming/admin-settings-branding.spec.ts | 101 + .../e2e/theming/admin-settings-colors.spec.ts | 30 + .../admin-settings-default-app.spec.ts | 63 + .../theming/user-settings-app-order.spec.ts | 46 + .../theming/user-settings-background.spec.ts | 61 + .../e2e/users/users-columns.spec.ts | 77 + .../e2e/users/users-disable.spec.ts | 66 + .../e2e/users/users-group-admin.spec.ts | 59 + .../playwright/e2e/users/users-groups.spec.ts | 224 ++ .../e2e/users/users-manager.spec.ts | 80 + .../playwright/e2e/users/users-modify.spec.ts | 160 + .../playwright/e2e/users/users-search.spec.ts | 108 + tests/playwright/e2e/users/users.spec.ts | 80 + tests/playwright/merge.config.ts | 11 + tests/playwright/start-nextcloud-server.js | 118 + .../support/fixtures/admin-appstore-page.ts | 14 + .../support/fixtures/admin-session.ts | 23 + .../support/fixtures/admin-theming-page.ts | 14 + .../support/fixtures/admin-with-user.ts | 14 + .../support/fixtures/external-storage-page.ts | 23 + .../playwright/support/fixtures/files-page.ts | 43 + .../support/fixtures/files-sharing-page.ts | 47 + .../support/fixtures/files-trashbin-page.ts | 90 + .../fixtures/files-versions-tab-page.ts | 20 + .../support/fixtures/public-share-page.ts | 59 + .../support/fixtures/random-user-session.ts | 25 + .../support/fixtures/random-user.ts | 29 + .../support/fixtures/sharing-page.ts | 62 + .../support/fixtures/systemtags-files-page.ts | 24 + tests/playwright/support/matchers.ts | 67 + .../support/sections/AccountMenuPage.ts | 45 + .../support/sections/AdminThemingPage.ts | 109 + .../support/sections/AppstorePage.ts | 198 ++ .../BackgroundFilePickerDialogPage.ts | 41 + .../support/sections/ContactsMenuPage.ts | 70 + .../support/sections/CopyMoveDialogPage.ts | 169 + .../sections/ExternalStorageSettingsPage.ts | 79 + .../support/sections/FilesFilterPage.ts | 49 + .../support/sections/FilesListPage.ts | 490 +++ .../support/sections/FilesNavigationPage.ts | 89 + .../support/sections/FilesSidebarPage.ts | 33 + .../playwright/support/sections/LoginPage.ts | 33 + .../support/sections/NavigationHeaderPage.ts | 50 + .../support/sections/PublicSharePage.ts | 156 + .../support/sections/SettingsUsersPage.ts | 135 + .../playwright/support/sections/SetupPage.ts | 142 + .../playwright/support/sections/SharingTab.ts | 295 ++ .../sections/SystemTagsFilesListPage.ts | 131 + .../support/sections/TrashbinListPage.ts | 37 + .../support/sections/UserThemingPage.ts | 31 + .../support/sections/VersionsTab.ts | 205 ++ tests/playwright/support/utils/css.ts | 13 + tests/playwright/support/utils/dav.ts | 218 ++ tests/playwright/support/utils/drag-drop.ts | 45 + .../support/utils/files_external.ts | 102 + tests/playwright/support/utils/live-photos.ts | 38 + .../support/utils/password-confirmation.ts | 33 + tests/playwright/support/utils/sharing.ts | 298 ++ tests/playwright/support/utils/systemtags.ts | 62 + tests/playwright/support/utils/theming.ts | 90 + tests/playwright/support/utils/users.ts | 61 + tests/playwright/support/utils/versions.ts | 75 + tests/playwright/support/utils/viewport.ts | 76 + tests/playwright/support/utils/zip.ts | 30 + tests/playwright/tsconfig.json | 8 + 279 files changed, 13862 insertions(+), 18789 deletions(-) delete mode 100644 .github/workflows/cypress.yml create mode 100644 .github/workflows/playwright.yml delete mode 100644 cypress.config.ts delete mode 100644 cypress/e2e/appstore/apps.cy.ts delete mode 100644 cypress/e2e/core-utils.ts delete mode 100644 cypress/e2e/core/404-error.cy.ts delete mode 100644 cypress/e2e/core/header_access-levels.cy.ts delete mode 100644 cypress/e2e/core/header_app-menu.cy.ts delete mode 100644 cypress/e2e/core/header_contacts-menu.cy.ts delete mode 100644 cypress/e2e/core/setup.ts delete mode 100644 cypress/e2e/dashboard/widget-performance.cy.ts delete mode 100644 cypress/e2e/dav/availability.cy.ts delete mode 100644 cypress/e2e/files/FilesUtils.ts delete mode 100644 cypress/e2e/files/LivePhotosUtils.ts delete mode 100644 cypress/e2e/files/drag-n-drop.cy.ts delete mode 100644 cypress/e2e/files/duplicated-node-regression.cy.ts delete mode 100644 cypress/e2e/files/favorites.cy.ts delete mode 100644 cypress/e2e/files/files-actions.cy.ts delete mode 100644 cypress/e2e/files/files-copy-move.cy.ts delete mode 100644 cypress/e2e/files/files-delete.cy.ts delete mode 100644 cypress/e2e/files/files-download.cy.ts delete mode 100644 cypress/e2e/files/files-filtering.cy.ts delete mode 100644 cypress/e2e/files/files-navigation.cy.ts delete mode 100644 cypress/e2e/files/files-renaming.cy.ts delete mode 100644 cypress/e2e/files/files-selection.cy.ts delete mode 100644 cypress/e2e/files/files-settings.cy.ts delete mode 100644 cypress/e2e/files/files-sidebar.cy.ts delete mode 100644 cypress/e2e/files/files-sorting.cy.ts delete mode 100644 cypress/e2e/files/files-xml-regression.cy.ts delete mode 100644 cypress/e2e/files/files.cy.ts delete mode 100644 cypress/e2e/files/hotkeys.cy.ts delete mode 100644 cypress/e2e/files/live_photos.cy.ts delete mode 100644 cypress/e2e/files/new-menu.cy.ts delete mode 100644 cypress/e2e/files/recent-view.cy.ts delete mode 100644 cypress/e2e/files/router-query.cy.ts delete mode 100644 cypress/e2e/files/scrolling.cy.ts delete mode 100644 cypress/e2e/files/search.cy.ts delete mode 100644 cypress/e2e/files_external/StorageUtils.ts delete mode 100644 cypress/e2e/files_external/files-external-failed.cy.ts delete mode 100644 cypress/e2e/files_external/files-user-credentials.cy.ts delete mode 100644 cypress/e2e/files_external/home-folder-root-mount-permissions.cy.ts delete mode 100644 cypress/e2e/files_external/settings.cy.ts delete mode 100644 cypress/e2e/files_sharing/FilesSharingUtils.ts delete mode 100644 cypress/e2e/files_sharing/ShareOptionsType.ts delete mode 100644 cypress/e2e/files_sharing/expiry-date.cy.ts delete mode 100644 cypress/e2e/files_sharing/file-request.cy.ts delete mode 100644 cypress/e2e/files_sharing/files-copy-move.cy.ts delete mode 100644 cypress/e2e/files_sharing/files-download.cy.ts delete mode 100644 cypress/e2e/files_sharing/files-shares-view.cy.ts delete mode 100644 cypress/e2e/files_sharing/limit_to_same_group.cy.ts delete mode 100644 cypress/e2e/files_sharing/note-to-recipient.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/PublicShareUtils.ts delete mode 100644 cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/default-view.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/download.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/header-avatar.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/header-menu.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/rename-files.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/required-before-create.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts delete mode 100644 cypress/e2e/files_sharing/public-share/view_view-only.cy.ts delete mode 100644 cypress/e2e/files_sharing/share-permissions-bundle.cy.ts delete mode 100644 cypress/e2e/files_sharing/share-status-action.cy.ts delete mode 100644 cypress/e2e/files_trashbin/files-trash-action.cy.ts delete mode 100644 cypress/e2e/files_trashbin/files.cy.ts delete mode 100644 cypress/e2e/files_versions/filesVersionsUtils.ts delete mode 100644 cypress/e2e/files_versions/version_creation.cy.ts delete mode 100644 cypress/e2e/files_versions/version_cross_share_move_and_copy.cy.ts delete mode 100644 cypress/e2e/files_versions/version_deletion.cy.ts delete mode 100644 cypress/e2e/files_versions/version_download.cy.ts delete mode 100644 cypress/e2e/files_versions/version_expiration.cy.ts delete mode 100644 cypress/e2e/files_versions/version_naming.cy.ts delete mode 100644 cypress/e2e/files_versions/version_restoration.cy.ts delete mode 100644 cypress/e2e/files_versions/version_sharing.cy.ts delete mode 100644 cypress/e2e/login/login-redirect.cy.ts delete mode 100644 cypress/e2e/login/login.cy.ts delete mode 100644 cypress/e2e/login/webauth.cy.ts delete mode 100644 cypress/e2e/settings/access-levels.cy.ts delete mode 100644 cypress/e2e/settings/personal-info.cy.ts delete mode 100644 cypress/e2e/settings/users-group-admin.cy.ts delete mode 100644 cypress/e2e/settings/users.cy.ts delete mode 100644 cypress/e2e/settings/usersUtils.ts delete mode 100644 cypress/e2e/settings/users_columns.cy.ts delete mode 100644 cypress/e2e/settings/users_disable.cy.ts delete mode 100644 cypress/e2e/settings/users_groups.cy.ts delete mode 100644 cypress/e2e/settings/users_manager.cy.ts delete mode 100644 cypress/e2e/settings/users_modify.cy.ts delete mode 100644 cypress/e2e/settings/users_search.cy.ts delete mode 100644 cypress/e2e/systemtags/admin-settings.cy.ts delete mode 100644 cypress/e2e/systemtags/files-bulk-action.cy.ts delete mode 100644 cypress/e2e/systemtags/files-inline-action.cy.ts delete mode 100644 cypress/e2e/systemtags/files-sidebar.cy.ts delete mode 100644 cypress/e2e/systemtags/files-view.cy.ts delete mode 100644 cypress/e2e/systemtags/utils.ts delete mode 100644 cypress/e2e/theming/a11y-color-contrast.cy.ts delete mode 100644 cypress/e2e/theming/admin-settings_background.cy.ts delete mode 100644 cypress/e2e/theming/admin-settings_branding.cy.ts delete mode 100644 cypress/e2e/theming/admin-settings_colors.cy.ts delete mode 100644 cypress/e2e/theming/admin-settings_default-app.cy.ts delete mode 100644 cypress/e2e/theming/themingUtils.ts delete mode 100644 cypress/e2e/theming/user-settings_app-order.cy.ts delete mode 100644 cypress/e2e/theming/user-settings_background.cy.ts delete mode 100644 cypress/fixtures/app.config.php delete mode 100644 cypress/fixtures/appstore/apps.json delete mode 100644 cypress/fixtures/testapp/appinfo/info.xml delete mode 100644 cypress/fixtures/testapp/appinfo/routes.php delete mode 100644 cypress/fixtures/testapp/img/app.svg delete mode 100644 cypress/fixtures/testapp/lib/AppInfo/Application.php delete mode 100644 cypress/fixtures/testapp/lib/Controller/PageController.php delete mode 100644 cypress/fixtures/testapp/templates/main.php delete mode 100644 cypress/pages/FilesFilters.ts delete mode 100644 cypress/pages/FilesNavigation.ts delete mode 100644 cypress/pages/NavigationHeader.ts delete mode 100644 cypress/pages/SettingsAppOrderList.ts delete mode 100644 cypress/pages/UnifiedSearch.ts delete mode 100644 cypress/support/commands.ts delete mode 100644 cypress/support/commonUtils.ts delete mode 100644 cypress/support/cypress-e2e.d.ts delete mode 100644 cypress/support/e2e.ts delete mode 100644 cypress/support/utils/assertions.ts delete mode 100644 cypress/support/utils/deleteDownloadsFolder.ts delete mode 100644 cypress/support/utils/randomString.ts delete mode 100644 cypress/tsconfig.json create mode 100644 playwright.config.ts create mode 100644 tests/data/REUSE.toml rename {cypress/fixtures => tests/data/images}/image.jpg (100%) create mode 100644 tests/playwright/README.md create mode 100644 tests/playwright/e2e/appstore/admin-settings-apps.spec.ts create mode 100644 tests/playwright/e2e/core/404-error.spec.ts create mode 100644 tests/playwright/e2e/core/header-access-levels.spec.ts create mode 100644 tests/playwright/e2e/core/header-app-menu.spec.ts create mode 100644 tests/playwright/e2e/core/header-contacts-menu.spec.ts create mode 100644 tests/playwright/e2e/core/setup.spec.ts create mode 100644 tests/playwright/e2e/dashboard/widget-performance.spec.ts create mode 100644 tests/playwright/e2e/dav/availability.spec.ts create mode 100644 tests/playwright/e2e/files/drag-n-drop.spec.ts create mode 100644 tests/playwright/e2e/files/duplicated-node-regression.spec.ts create mode 100644 tests/playwright/e2e/files/files-actions.spec.ts create mode 100644 tests/playwright/e2e/files/files-copy-move.spec.ts create mode 100644 tests/playwright/e2e/files/files-delete.spec.ts create mode 100644 tests/playwright/e2e/files/files-download.spec.ts create mode 100644 tests/playwright/e2e/files/files-favorites.spec.ts create mode 100644 tests/playwright/e2e/files/files-filtering.spec.ts create mode 100644 tests/playwright/e2e/files/files-navigation.spec.ts create mode 100644 tests/playwright/e2e/files/files-renaming.spec.ts create mode 100644 tests/playwright/e2e/files/files-selection.spec.ts create mode 100644 tests/playwright/e2e/files/files-settings.spec.ts create mode 100644 tests/playwright/e2e/files/files-sidebar.spec.ts create mode 100644 tests/playwright/e2e/files/files-sorting.spec.ts create mode 100644 tests/playwright/e2e/files/files-xml-regression.spec.ts create mode 100644 tests/playwright/e2e/files/files.spec.ts create mode 100644 tests/playwright/e2e/files/hotkeys.spec.ts create mode 100644 tests/playwright/e2e/files/live-photos.spec.ts create mode 100644 tests/playwright/e2e/files/new-menu.spec.ts create mode 100644 tests/playwright/e2e/files/recent-view.spec.ts create mode 100644 tests/playwright/e2e/files/router-query.spec.ts create mode 100644 tests/playwright/e2e/files/scrolling.spec.ts create mode 100644 tests/playwright/e2e/files/search.spec.ts create mode 100644 tests/playwright/e2e/files_external/admin-settings-external-storage.spec.ts create mode 100644 tests/playwright/e2e/files_external/admin-settings-home-folder-root-mount.spec.ts create mode 100644 tests/playwright/e2e/files_external/admin-settings-user-credentials.spec.ts create mode 100644 tests/playwright/e2e/files_external/files-external-failed.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/admin-settings-download-forbidden.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/admin-settings-expiry-date.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/admin-settings-limit-to-same-group.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/admin-settings-permissions-bundle.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/file-request.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/files-copy-move.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/files-shares-view.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/note-to-recipient.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/admin-settings-file-drop-terms.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/admin-settings-required-before-create.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/copy-move-rename-files.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/default-view.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/download.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/file-drop.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/header-avatar.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/header-menu.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/share-editor.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/public-share/view-only.spec.ts create mode 100644 tests/playwright/e2e/files_sharing/share-status-action.spec.ts create mode 100644 tests/playwright/e2e/files_trashbin/files-trash-action.spec.ts create mode 100644 tests/playwright/e2e/files_trashbin/files.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-creation.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-cross-share-move-and-copy.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-deletion.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-download.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-expiration.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-naming.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-restoration.spec.ts create mode 100644 tests/playwright/e2e/files_versions/version-sharing.spec.ts create mode 100644 tests/playwright/e2e/login/login-redirect.spec.ts create mode 100644 tests/playwright/e2e/login/login.spec.ts create mode 100644 tests/playwright/e2e/login/webauth.spec.ts create mode 100644 tests/playwright/e2e/settings/access-levels.spec.ts create mode 100644 tests/playwright/e2e/settings/personal-info.spec.ts create mode 100644 tests/playwright/e2e/systemtags/admin-settings-restrictions.spec.ts create mode 100644 tests/playwright/e2e/systemtags/admin-settings.spec.ts create mode 100644 tests/playwright/e2e/systemtags/files-bulk-action.spec.ts create mode 100644 tests/playwright/e2e/systemtags/files-inline-action.spec.ts create mode 100644 tests/playwright/e2e/systemtags/files-sidebar.spec.ts create mode 100644 tests/playwright/e2e/systemtags/files-view.spec.ts create mode 100644 tests/playwright/e2e/theming/a11y-color-contrast.spec.ts create mode 100644 tests/playwright/e2e/theming/admin-settings-background.spec.ts create mode 100644 tests/playwright/e2e/theming/admin-settings-branding.spec.ts create mode 100644 tests/playwright/e2e/theming/admin-settings-colors.spec.ts create mode 100644 tests/playwright/e2e/theming/admin-settings-default-app.spec.ts create mode 100644 tests/playwright/e2e/theming/user-settings-app-order.spec.ts create mode 100644 tests/playwright/e2e/theming/user-settings-background.spec.ts create mode 100644 tests/playwright/e2e/users/users-columns.spec.ts create mode 100644 tests/playwright/e2e/users/users-disable.spec.ts create mode 100644 tests/playwright/e2e/users/users-group-admin.spec.ts create mode 100644 tests/playwright/e2e/users/users-groups.spec.ts create mode 100644 tests/playwright/e2e/users/users-manager.spec.ts create mode 100644 tests/playwright/e2e/users/users-modify.spec.ts create mode 100644 tests/playwright/e2e/users/users-search.spec.ts create mode 100644 tests/playwright/e2e/users/users.spec.ts create mode 100644 tests/playwright/merge.config.ts create mode 100644 tests/playwright/start-nextcloud-server.js create mode 100644 tests/playwright/support/fixtures/admin-appstore-page.ts create mode 100644 tests/playwright/support/fixtures/admin-session.ts create mode 100644 tests/playwright/support/fixtures/admin-theming-page.ts create mode 100644 tests/playwright/support/fixtures/admin-with-user.ts create mode 100644 tests/playwright/support/fixtures/external-storage-page.ts create mode 100644 tests/playwright/support/fixtures/files-page.ts create mode 100644 tests/playwright/support/fixtures/files-sharing-page.ts create mode 100644 tests/playwright/support/fixtures/files-trashbin-page.ts create mode 100644 tests/playwright/support/fixtures/files-versions-tab-page.ts create mode 100644 tests/playwright/support/fixtures/public-share-page.ts create mode 100644 tests/playwright/support/fixtures/random-user-session.ts create mode 100644 tests/playwright/support/fixtures/random-user.ts create mode 100644 tests/playwright/support/fixtures/sharing-page.ts create mode 100644 tests/playwright/support/fixtures/systemtags-files-page.ts create mode 100644 tests/playwright/support/matchers.ts create mode 100644 tests/playwright/support/sections/AccountMenuPage.ts create mode 100644 tests/playwright/support/sections/AdminThemingPage.ts create mode 100644 tests/playwright/support/sections/AppstorePage.ts create mode 100644 tests/playwright/support/sections/BackgroundFilePickerDialogPage.ts create mode 100644 tests/playwright/support/sections/ContactsMenuPage.ts create mode 100644 tests/playwright/support/sections/CopyMoveDialogPage.ts create mode 100644 tests/playwright/support/sections/ExternalStorageSettingsPage.ts create mode 100644 tests/playwright/support/sections/FilesFilterPage.ts create mode 100644 tests/playwright/support/sections/FilesListPage.ts create mode 100644 tests/playwright/support/sections/FilesNavigationPage.ts create mode 100644 tests/playwright/support/sections/FilesSidebarPage.ts create mode 100644 tests/playwright/support/sections/LoginPage.ts create mode 100644 tests/playwright/support/sections/NavigationHeaderPage.ts create mode 100644 tests/playwright/support/sections/PublicSharePage.ts create mode 100644 tests/playwright/support/sections/SettingsUsersPage.ts create mode 100644 tests/playwright/support/sections/SetupPage.ts create mode 100644 tests/playwright/support/sections/SharingTab.ts create mode 100644 tests/playwright/support/sections/SystemTagsFilesListPage.ts create mode 100644 tests/playwright/support/sections/TrashbinListPage.ts create mode 100644 tests/playwright/support/sections/UserThemingPage.ts create mode 100644 tests/playwright/support/sections/VersionsTab.ts create mode 100644 tests/playwright/support/utils/css.ts create mode 100644 tests/playwright/support/utils/dav.ts create mode 100644 tests/playwright/support/utils/drag-drop.ts create mode 100644 tests/playwright/support/utils/files_external.ts create mode 100644 tests/playwright/support/utils/live-photos.ts create mode 100644 tests/playwright/support/utils/password-confirmation.ts create mode 100644 tests/playwright/support/utils/sharing.ts create mode 100644 tests/playwright/support/utils/systemtags.ts create mode 100644 tests/playwright/support/utils/theming.ts create mode 100644 tests/playwright/support/utils/users.ts create mode 100644 tests/playwright/support/utils/versions.ts create mode 100644 tests/playwright/support/utils/viewport.ts create mode 100644 tests/playwright/support/utils/zip.ts create mode 100644 tests/playwright/tsconfig.json diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml deleted file mode 100644 index 1f7382eb50ed3..0000000000000 --- a/.github/workflows/cypress.yml +++ /dev/null @@ -1,280 +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 - -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 - -jobs: - init: - runs-on: ubuntu-latest - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: 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: ['setup', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] - # Hack as strategy.job-total includes the "setup" and GitHub does not allow math expressions - # Always align this number with the total of e2e runners (max. index + 1) - total-containers: [10] - - services: - mysql: - # Only start mysql if we are running the setup tests - image: ${{matrix.containers == 'setup' && 'ghcr.io/nextcloud/continuous-integration-mysql-8.4:latest' || ''}} # zizmor: ignore[unpinned-images] - ports: - - '3306/tcp' - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: oc_autotest - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - - mariadb: - # Only start mariadb if we are running the setup tests - image: ${{matrix.containers == 'setup' && 'mariadb:11.4' || ''}} # zizmor: ignore[unpinned-images] - ports: - - '3306/tcp' - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: oc_autotest - options: --health-cmd="mariadb-admin ping" --health-interval 5s --health-timeout 2s --health-retries 5 - - postgres: - # Only start postgres if we are running the setup tests - image: ${{matrix.containers == 'setup' && 'ghcr.io/nextcloud/continuous-integration-postgres-17:latest' || ''}} # zizmor: ignore[unpinned-images] - ports: - - '5432/tcp' - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: rootpassword - POSTGRES_DB: nextcloud - options: --mount type=tmpfs,destination=/var/lib/postgresql/data --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 - - oracle: - # Only start oracle if we are running the setup tests - image: ${{matrix.containers == 'setup' && 'ghcr.io/gvenzl/oracle-free:23' || ''}} # zizmor: ignore[unpinned-images] - ports: - - '1521' - env: - ORACLE_PASSWORD: oracle - options: --health-cmd healthcheck.sh --health-interval 20s --health-timeout 10s --health-retries 10 - - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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@c495c3ddffba403ba11be95fffb67e25203b3799 # v7.1.10 - 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 }} - SETUP_TESTING: ${{ matrix.containers == 'setup' && 'true' || '' }} - - - 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 new file mode 100644 index 0000000000000..9fb0b9bdf3b97 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,325 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Playwright Tests + +on: + pull_request: + branches: + - master + - stable34 + types: + - opened + - synchronize + - reopened + - ready_for_review + - labeled + +permissions: + contents: 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 playwrightTouched = files.some((file) => file.filename.startsWith('tests/playwright')) + + if (hasForceLabel || hasToReviewLabel || hasToReleaseLabel || playwrightTouched) { + return + } else { + core.setFailed('Skipping Playwright: draft state, missing labels or no playwright path changes.') + } + + playwright-setup: + timeout-minutes: 15 + name: Playwright setup + runs-on: ubuntu-latest + needs: gate + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: true # for 3rdparty + - name: Read package.json + uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 + id: versions + - name: Set up node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.node-version }} + - name: Set up npm + run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' + - name: Install dependencies and build + run: | + npm ci + npm run build --if-present + - name: Save context + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: playwright-context-${{ github.run_id }} + path: ./ + + playwright-tests: + needs: [gate, playwright-setup] + timeout-minutes: 60 + name: Playwright tests ${{ matrix.shardIndex }} / ${{ matrix.shardTotal }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + 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 }} + + steps: + - name: Restore context + id: cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: playwright-context-${{ github.run_id }} + path: ./ + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: steps.cache.outputs.cache-hit != 'true' + with: + persist-credentials: false + submodules: true # for 3rdparty + + - name: Read package.json + uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 + id: versions + + - name: Set up node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.node-version }} + + - name: Set up npm + run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' + + - name: Install dependencies and build + if: steps.cache.outputs.cache-hit != 'true' + run: | + npm ci + npm run build --if-present + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npm run playwright -- --shard='${{ matrix.shardIndex }}/${{ matrix.shardTotal }}' + + - name: Show logs + if: failure() + 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_server cat data/nextcloud.log + + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blob-report-${{ matrix.shardIndex }} + path: blob-report + retention-days: 1 + + playwright-installer-tests: + needs: [gate, playwright-setup] + timeout-minutes: 30 + name: Playwright tests for installer + runs-on: ubuntu-latest + + # The installation-wizard tests exercise every supported database backend, so + # they need reachable database service containers. The Nextcloud container is + # joined to the GitHub Actions network at startup so it can resolve these by + # hostname (see tests/playwright/start-nextcloud-server.js). + services: + mysql: + image: mysql:9.7 # zizmor: ignore[unpinned-images] + ports: + - '3306/tcp' + env: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_USER: nextcloud + MYSQL_PASSWORD: nextcloud + MYSQL_DATABASE: nextcloud + options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 + + mariadb: + image: mariadb:12.3 # zizmor: ignore[unpinned-images] + ports: + - '3306/tcp' + env: + MARIADB_ROOT_PASSWORD: rootpassword + MARIADB_USER: nextcloud + MARIADB_PASSWORD: nextcloud + MARIADB_DATABASE: nextcloud + options: --health-cmd="mariadb-admin ping" --health-interval 5s --health-timeout 2s --health-retries 5 + + postgres: + image: postgres:18 # zizmor: ignore[unpinned-images] + ports: + - '5432/tcp' + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: rootpassword + POSTGRES_DB: nextcloud + options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 + + oracle: + image: ghcr.io/gvenzl/oracle-free:23 # zizmor: ignore[unpinned-images] + ports: + - '1521' + env: + ORACLE_PASSWORD: oracle + options: --health-cmd healthcheck.sh --health-interval 20s --health-timeout 10s --health-retries 10 + + steps: + - name: Restore context + id: cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: playwright-context-${{ github.run_id }} + path: ./ + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: steps.cache.outputs.cache-hit != 'true' + with: + persist-credentials: false + submodules: true # for 3rdparty + + - name: Read package.json + uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 + id: versions + + - name: Set up node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.node-version }} + + - name: Set up npm + run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' + + - name: Install dependencies and build + if: steps.cache.outputs.cache-hit != 'true' + run: | + npm ci + npm run build --if-present + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run Playwright setup tests + run: npm run playwright:setup + env: + PLAYWRIGHT_SETUP: 'true' + + - name: Show logs + if: failure() + 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_server cat data/nextcloud.log || true + + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blob-report-setup + path: blob-report + retention-days: 1 + + merge-reports: + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [gate, playwright-tests, playwright-installer-tests] + + runs-on: ubuntu-latest-low + steps: + - name: Restore context + id: cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + key: playwright-context-${{ github.run_id }} + path: ./ + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: steps.cache.outputs.cache-hit != 'true' + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + if: steps.cache.outputs.cache-hit != 'true' + with: + node-version: ${{ needs.playwright-tests.outputs.node-version }} + + - name: Set up npm + if: steps.cache.outputs.cache-hit != 'true' + run: npm i -g 'npm@${{ needs.playwright-tests.outputs.package-manager-version }}' + + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: npm ci + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports --config tests/playwright/merge.config.ts --reporter html,github ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 7 + + - name: Show the logs + run: | + echo 'To view the report:' + echo ' 1. Extract the folder from the zip file' + echo ' 2. run "npx playwright show-report name-of-my-extracted-playwright-report"' + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [gate, playwright-tests, playwright-installer-tests] + + if: always() + + name: playwright-test-summary + + steps: + - name: Summary status + run: if ${{ needs.playwright-tests.result != 'success' || needs.playwright-installer-tests.result != 'success' }}; then exit 1; fi diff --git a/.nextcloudignore b/.nextcloudignore index b49dc8e0bd1f8..4641d10afdf34 100644 --- a/.nextcloudignore +++ b/.nextcloudignore @@ -34,13 +34,12 @@ codecov.yml cs-fixer csfixer custom.d.ts -cypress -cypress.config.ts eslint.config.js flake.lock flake.nix openapi-extractor phpunit +playwright.config.ts psalm psalm*.xml rector diff --git a/REUSE.toml b/REUSE.toml index d9cfce23cb756..e56a5b2367a25 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -10,25 +10,25 @@ SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2016-2024 Nextcloud translators" SPDX-License-Identifier = "AGPL-3.0-only OR AGPL-3.0-or-later" [[annotations]] -path = ["apps/appstore/l10n/**.js", "apps/appstore/l10n/**.json", "apps/cloud_federation_api/l10n/**.js", "apps/cloud_federation_api/l10n/**.json", "apps/contactsinteraction/l10n/**.js", "apps/contactsinteraction/l10n/**.json", "apps/dashboard/l10n/**.js", "apps/dashboard/l10n/**.json", "apps/files_reminders/l10n/**.js", "apps/files_reminders/l10n/**.json", "apps/lookup_server_connector/l10n/**.js", "apps/lookup_server_connector/l10n/**.json", "apps/profile/l10n/**.js", "apps/profile/l10n/**.json", "apps/sharebymail/l10n/**.js", "apps/oauth2/l10n/**.js", "apps/oauth2/l10n/**.json", "apps/sharebymail/l10n/**.json", "apps/theming/l10n/**.js", "apps/theming/l10n/**.json", "apps/twofactor_backupcodes/l10n/**.js", "apps/twofactor_backupcodes/l10n/**.json", "apps/user_status/l10n/**.js", "apps/user_status/l10n/**.json", "apps/weather_status/l10n/**.js", "apps/weather_status/l10n/**.json", "apps/webhook_listeners/l10n/**.js", "apps/webhook_listeners/l10n/**.json", "apps/workflowengine/l10n/**.js", "apps/workflowengine/l10n/**.json"] +path = ["apps/cloud_federation_api/l10n/**.js", "apps/cloud_federation_api/l10n/**.json", "apps/contactsinteraction/l10n/**.js", "apps/contactsinteraction/l10n/**.json", "apps/dashboard/l10n/**.js", "apps/dashboard/l10n/**.json", "apps/files_reminders/l10n/**.js", "apps/files_reminders/l10n/**.json", "apps/lookup_server_connector/l10n/**.js", "apps/lookup_server_connector/l10n/**.json", "apps/sharebymail/l10n/**.js", "apps/oauth2/l10n/**.js", "apps/oauth2/l10n/**.json", "apps/sharebymail/l10n/**.json", "apps/theming/l10n/**.js", "apps/theming/l10n/**.json", "apps/twofactor_backupcodes/l10n/**.js", "apps/twofactor_backupcodes/l10n/**.json", "apps/user_status/l10n/**.js", "apps/user_status/l10n/**.json", "apps/weather_status/l10n/**.js", "apps/weather_status/l10n/**.json", "apps/webhook_listeners/l10n/**.js", "apps/webhook_listeners/l10n/**.json", "apps/workflowengine/l10n/**.js", "apps/workflowengine/l10n/**.json"] precedence = "aggregate" 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" [[annotations]] -path = ["apps/cloud_federation_api/l10n/zh_TW.js", "apps/cloud_federation_api/l10n/zh_TW.json", "apps/contactsinteraction/l10n/zh_TW.js", "apps/contactsinteraction/l10n/zh_TW.json", "apps/dashboard/l10n/zh_TW.js", "apps/dashboard/l10n/zh_TW.json", "apps/files_reminders/l10n/zh_TW.js", "apps/files_reminders/l10n/zh_TW.json", "apps/lookup_server_connector/l10n/zh_TW.js", "apps/lookup_server_connector/l10n/zh_TW.json", "apps/profile/l10n/zh_TW.js", "apps/profile/l10n/zh_TW.json", "apps/sharebymail/l10n/zh_TW.js", "apps/oauth2/l10n/zh_TW.js", "apps/oauth2/l10n/zh_TW.json", "apps/sharebymail/l10n/zh_TW.json", "apps/theming/l10n/zh_TW.js", "apps/theming/l10n/zh_TW.json", "apps/twofactor_backupcodes/l10n/zh_TW.js", "apps/twofactor_backupcodes/l10n/zh_TW.json", "apps/user_status/l10n/zh_TW.js", "apps/user_status/l10n/zh_TW.json", "apps/weather_status/l10n/zh_TW.js", "apps/weather_status/l10n/zh_TW.json", "apps/webhook_listeners/l10n/zh_TW.js", "apps/webhook_listeners/l10n/zh_TW.json", "apps/workflowengine/l10n/zh_TW.js", "apps/workflowengine/l10n/zh_TW.json"] +path = ["apps/cloud_federation_api/l10n/zh_TW.js", "apps/cloud_federation_api/l10n/zh_TW.json", "apps/contactsinteraction/l10n/zh_TW.js", "apps/contactsinteraction/l10n/zh_TW.json", "apps/dashboard/l10n/zh_TW.js", "apps/dashboard/l10n/zh_TW.json", "apps/files_reminders/l10n/zh_TW.js", "apps/files_reminders/l10n/zh_TW.json", "apps/lookup_server_connector/l10n/zh_TW.js", "apps/lookup_server_connector/l10n/zh_TW.json", "apps/sharebymail/l10n/zh_TW.js", "apps/oauth2/l10n/zh_TW.js", "apps/oauth2/l10n/zh_TW.json", "apps/sharebymail/l10n/zh_TW.json", "apps/theming/l10n/zh_TW.js", "apps/theming/l10n/zh_TW.json", "apps/twofactor_backupcodes/l10n/zh_TW.js", "apps/twofactor_backupcodes/l10n/zh_TW.json", "apps/user_status/l10n/zh_TW.js", "apps/user_status/l10n/zh_TW.json", "apps/weather_status/l10n/zh_TW.js", "apps/weather_status/l10n/zh_TW.json", "apps/webhook_listeners/l10n/zh_TW.js", "apps/webhook_listeners/l10n/zh_TW.json", "apps/workflowengine/l10n/zh_TW.js", "apps/workflowengine/l10n/zh_TW.json"] precedence = "aggregate" SPDX-FileCopyrightText = "2016-2024 Nextcloud translators, 2024 moda-l10n " SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["tests/data/block-aligned-plus-one.txt", "tests/data/block-aligned.txt", "tests/data/data.tar.gz", "tests/data/data.zip", "tests/data/desktopapp.png", "tests/data/desktopapp.svg", "tests/data/certificates/badCertificate.crt", "tests/data/certificates/expiredCertificate.crt", "tests/data/certificates/goodCertificate.crt", "tests/data/certificates/openSslTrustedCertificate.crt", "tests/data/integritycheck/app/AnotherFile.txt", "tests/data/integritycheck/app/subfolder/file.txt", "tests/data/integritycheck/appWithInvalidData/AnotherFile.txt", "tests/data/integritycheck/appWithInvalidData/UnecessaryFile", "apps/user_ldap/tests/Integration/data/avatar-invalid.gif", "apps/user_ldap/tests/Integration/data/avatar-valid.jpg", "apps/user_ldap/img/copy.png", "apps/user_ldap/img/copy.svg"] +path = ["apps/user_ldap/tests/Integration/data/avatar-invalid.gif", "apps/user_ldap/tests/Integration/data/avatar-valid.jpg", "apps/user_ldap/img/copy.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2015 ownCloud, Inc." SPDX-License-Identifier = "AGPL-3.0-only" @@ -45,12 +45,6 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2011-2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-only" -[[annotations]] -path = ["core/templates/filetemplates/template.odp", "core/templates/filetemplates/template.ods", "core/templates/filetemplates/template.odt"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2013-2016 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - [[annotations]] path = "core/skeleton/welcome.txt" precedence = "aggregate" @@ -70,53 +64,17 @@ SPDX-FileCopyrightText = "2015-2016 ownCloud, Inc., 2016-2021 Nextcloud GmbH and SPDX-License-Identifier = "AGPL-3.0-only" [[annotations]] -path = ["tests/data/integritycheck/htaccessUnmodified/.htaccess", "tests/data/integritycheck/htaccessUnmodified/subfolder/.htaccess", "tests/data/integritycheck/htaccessWithInvalidModifiedContent/.htaccess", "tests/data/integritycheck/htaccessWithValidModifiedContent/subfolder/.htaccess"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["tests/data/l10n/cs.json", "tests/data/l10n/de.json", "tests/data/l10n/ru.json"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2021 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["tests/data/lorem.txt", "apps/files/l10n/xgettextfiles"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2012 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["robots.txt", "tests/data/testavatar.png", "tests/data/testimage.gif", "tests/data/testimage.jpg", "tests/data/testimage.png"] +path = ["robots.txt"] precedence = "aggregate" SPDX-FileCopyrightText = "2013 ownCloud, Inc." SPDX-License-Identifier = "AGPL-3.0-only" [[annotations]] -path = [".tag", "tests/data/lorem-big.txt", "tests/data/strängé**filename**(duplicate**#2).txt", "tests/data/strõngÚ**filename**(duplicate**#2).txt"] +path = [".tag"] precedence = "aggregate" SPDX-FileCopyrightText = "2014 ownCloud, Inc." SPDX-License-Identifier = "AGPL-3.0-only" -[[annotations]] -path = "tests/data/testimagelarge.svg" -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2016 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["tests/data/setUploadLimit/htaccess", "tests/data/setUploadLimit/user.ini"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2018 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["tests/data/testimage-wide.png", "tests/data/testimage.eps", "tests/data/testimage.mp3", "tests/data/testimage.mp4", "tests/data/testimage.odt"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - [[annotations]] path = ["themes/example/core/img/favicon-touch.png", "themes/example/core/img/favicon-touch.svg", "themes/example/core/img/favicon.ico", "themes/example/core/img/favicon.png", "themes/example/core/img/favicon.svg", "themes/example/core/img/logo-icon.png", "themes/example/core/img/logo-icon.svg", "themes/example/core/img/logo-mail.gif", "themes/example/core/img/logo.png", "themes/example/core/img/logo.svg"] precedence = "aggregate" @@ -124,7 +82,7 @@ SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2016 Nextcloud GmbH and Nextcloud SPDX-License-Identifier = "AGPL-3.0-only" [[annotations]] -path = ["tests/data/testapp.zip", "tests/data/testapp2.zip", "tests/docker/mariadb/oc.cnf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/2.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/3.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/4.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/5.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/6.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf"] +path = ["tests/docker/mariadb/oc.cnf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/2.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/3.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/4.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/5.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/6.txt", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf", "apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf"] precedence = "aggregate" SPDX-FileCopyrightText = "2016 ownCloud, Inc." SPDX-License-Identifier = "AGPL-3.0-only" @@ -135,12 +93,6 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2016 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" -[[annotations]] -path = "apps/settings/js/templates.js" -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - [[annotations]] path = "tests/docker/mysqlmb4/mb4.cnf" precedence = "aggregate" @@ -148,13 +100,7 @@ SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2017 Nextcloud GmbH and Nextcloud SPDX-License-Identifier = "AGPL-3.0-only" [[annotations]] -path = "tests/data/integritycheck/htaccessWithValidModifiedContent/.htaccess" -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2019 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["core/img/favicon**.**", "core/img/logo/logo**.**", "tests/data/testimage.webp", "tests/data/broken-video.webm", "apps/testing/img/logo.png", "core/img/apps/spreed.svg"] +path = ["core/img/favicon**.**", "core/img/logo/logo**.**", "apps/testing/img/logo.png", "core/img/apps/spreed.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2016-2024 Nextcloud GmbH" SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks" @@ -178,7 +124,7 @@ SPDX-FileCopyrightText = "2020 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["cypress/tsconfig.json", "cypress/fixtures/appstore/apps.json", "dist/*.css"] +path = ["dist/*.css"] precedence = "aggregate" SPDX-FileCopyrightText = "2022 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" @@ -202,43 +148,43 @@ SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Next SPDX-License-Identifier = "AGPL-3.0-only" [[annotations]] -path = ["tests/data/testapp.0.8.tar.gz", "tests/data/testapp.tar.gz", "tests/data/testapp1.tar.gz", "core/img/filetypes/package-x-generic.svg"] +path = ["core/img/filetypes/package-x-generic.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2016 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["core/js/merged-template-prepend.json", "tests/data/certificates/goodCertificate.crt", "tests/data/emails/new-account-email-custom-text-alternative.txt", "tests/data/emails/new-account-email-custom.html", "tests/data/emails/new-account-email-custom.txt", "tests/data/emails/new-account-email-single-button.html", "tests/data/emails/new-account-email-single-button.txt", "tests/data/emails/new-account-email.html", "tests/data/emails/new-account-email.txt", "core/img/actions/checkbox-mixed.svg", "core/img/actions/checkmark-white.svg", "core/img/actions/checkbox-mark-dark.svg", "core/img/actions/checkbox-mark-white.svg", "core/img/actions/checkbox-mark.svg", "core/img/actions/checkbox-mixed-dark.svg", "core/img/actions/checkbox-mixed-white.svg", "core/img/actions/checkbox-mixed.svgcore/img/actions/checkmark-white.svg", "core/img/actions/checkmark.png", "core/img/actions/checkmark.svg"] +path = ["core/img/actions/checkbox-mixed.svg", "core/img/actions/checkmark-white.svg", "core/img/actions/checkbox-mark-dark.svg", "core/img/actions/checkbox-mark-white.svg", "core/img/actions/checkbox-mark.svg", "core/img/actions/checkbox-mixed-dark.svg", "core/img/actions/checkbox-mixed-white.svg", "core/img/actions/checkmark.png", "core/img/actions/checkmark.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2017 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["core/src/systemtags/templates/result.handlebars", "core/src/systemtags/templates/result_form.handlebars", "core/src/systemtags/templates/selection.handlebars", "apps/files/js/templates.js", "apps/files/js/templates/detailsview.handlebars", "apps/files/js/templates/favorite_mark.handlebars", "apps/files/js/templates/file_action_trigger.handlebars", "apps/files/js/templates/fileactionsmenu.handlebars", "apps/files/js/templates/filemultiselectmenu.handlebars", "apps/files/js/templates/filesummary.handlebars", "apps/files/js/templates/mainfileinfodetailsview.handlebars", "apps/files/js/templates/newfilemenu.handlebars", "apps/files/js/templates/newfilemenu_filename_form.handlebars", "apps/files/js/templates/operationprogressbar.handlebars", "apps/files/js/templates/operationprogressbarlabel.handlebars", "apps/files/js/templates/template_addbutton.handlebars", "apps/files_external/js/templates.js", "apps/files_external/js/templates/credentialsDialog.handlebars", "apps/files_external/js/templates/mountOptionsDropDown.handlebars", "apps/files_external/js/templates.js", "apps/files_sharing/js/templates/files_drop.handlebars", "apps/files_sharing/js/templates.js", "tests/data/testimage.heic", "package.json", "package-lock.json"] +path = ["package.json", "package-lock.json"] precedence = "aggregate" SPDX-FileCopyrightText = "2018 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["core/src/jquery/contactsmenu/jquery_entry.handlebars", "tests/data/integritycheck/mimetypeListModified/core/js/mimetypelist.js", "tests/data/integritycheck/mimetypeListModified/core/signature.json", "tests/data/guest_avatar_einstein_32.png", "tests/data/guest_avatar_einstein_32.svg", "tests/data/test.pdf", "apps/workflowengine/img/app.svg", "apps/workflowengine/img/app-dark.svg", "apps/settings/js/templates/federationscopemenu.handlebars"] +path = ["apps/workflowengine/img/app.svg", "apps/workflowengine/img/app-dark.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2019 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = [".npmignore", "tests/data/testimage-badheader.jpg"] +path = [".npmignore"] precedence = "aggregate" SPDX-FileCopyrightText = "2021 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["build/stubs/ftp.php", "tests/data/integritycheck/mimetypeListModified/coresignature.json", "apps/settings/tests/UserMigration/assets/account-complex-config.json", "apps/settings/tests/UserMigration/assets/account-complex.json", "apps/settings/tests/UserMigration/assets/account-config.json", "apps/settings/tests/UserMigration/assets/account.json"] +path = ["build/stubs/ftp.php", "apps/settings/tests/UserMigration/assets/account-complex-config.json", "apps/settings/tests/UserMigration/assets/account-complex.json", "apps/settings/tests/UserMigration/assets/account-config.json", "apps/settings/tests/UserMigration/assets/account.json"] precedence = "aggregate" SPDX-FileCopyrightText = "2022 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["build/stubs/app_api.php", "build/stubs/SensitiveParameter.phpstub", "build/stubs/psr_container.php", "tests/data/testimage-large.jpg", "apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf", "apps/dav/tests/integration/UserMigration/assets/address_books/contact-multiple-complex.vcf", "apps/dav/tests/integration/UserMigration/assets/address_books/contact-multiple.vcf", "apps/dav/tests/integration/UserMigration/assets/address_books/contact.vcf", "apps/dav/tests/integration/UserMigration/assets/calendars/event-alarms.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-attendees.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-categories.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-complex-alarm-recurring.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-complex-recurrence.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-custom-color.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-multiple-recurring.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-multiple.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-recurring.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-timed.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/journal-todo-event.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/journal.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/todo.ics", "apps/workflowengine/img/workflow-off.svg", "core/img/filetypes/font.svg", "core/img/actions/star-rounded.svg"] +path = ["build/stubs/app_api.php", "build/stubs/SensitiveParameter.phpstub", "build/stubs/psr_container.php", "apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf", "apps/dav/tests/integration/UserMigration/assets/address_books/contact-multiple-complex.vcf", "apps/dav/tests/integration/UserMigration/assets/address_books/contact-multiple.vcf", "apps/dav/tests/integration/UserMigration/assets/address_books/contact.vcf", "apps/dav/tests/integration/UserMigration/assets/calendars/event-alarms.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-attendees.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-categories.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-complex-alarm-recurring.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-complex-recurrence.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-custom-color.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-multiple-recurring.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-multiple.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-recurring.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/event-timed.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/journal-todo-event.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/journal.ics", "apps/dav/tests/integration/UserMigration/assets/calendars/todo.ics", "apps/workflowengine/img/workflow-off.svg", "core/img/filetypes/font.svg", "core/img/actions/star-rounded.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2023 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" @@ -250,13 +196,19 @@ SPDX-FileCopyrightText = "2023 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["apps/dav/lib/ExampleContentFiles/exampleContact.vcf", "tests/data/testvideo-remote-file.mp4", "tests/lib/AppFramework/Middleware/Mock/UseSessionController.php", "tests/lib/AppFramework/Middleware/Security/Mock/BruteForceMiddlewareController.php", "tests/lib/AppFramework/Middleware/Security/Mock/RateLimitingMiddlewareController.php"] +path = ["apps/dav/lib/ExampleContentFiles/exampleContact.vcf", "tests/lib/AppFramework/Middleware/Mock/UseSessionController.php", "tests/lib/AppFramework/Middleware/Security/Mock/BruteForceMiddlewareController.php", "tests/lib/AppFramework/Middleware/Security/Mock/RateLimitingMiddlewareController.php"] precedence = "aggregate" SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["composer.json", "composer.lock", ".github/CODEOWNERS", "__tests__/tsconfig.json", "tsconfig.json", "apps/**/composer/composer.json", "apps/**/composer/composer.lock", "apps/**/composer/composer/installed.json"] +path = ["tsconfig.json", "tests/playwright/tsconfig.json"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = ["composer.json", "composer.lock", ".github/CODEOWNERS", "apps/**/composer/composer.json", "apps/**/composer/composer.lock", "apps/**/composer/composer/installed.json"] precedence = "aggregate" SPDX-FileCopyrightText = "2011-2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-only OR AGPL-3.0-or-later" @@ -268,7 +220,7 @@ SPDX-FileCopyrightText = "2021 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["apps/encryption/img/app.svg", "apps/federation/img/app.svg", "apps/files/img/add-color.png", "apps/files/img/add-color.svg", "apps/files/img/app-dark.svg", "apps/files/img/app.svg", "apps/files/img/change.png", "apps/files/img/change.svg", "apps/files/img/delete-color.png", "apps/files/img/delete-color.svg", "apps/files/img/folder.svg", "apps/files/img/folder-move.svg", "apps/files_external/img/app-dark.svg", "apps/files_external/img/app.svg", "apps/user_ldap/img/app-dark.svg", "apps/user_ldap/img/app.svg", "apps/comments/img/comments-dark.svg", "apps/comments/img/comments.svg", "apps/files_reminders/img/alarm.svg", "apps/files_reminders/img/app-dark.svg", "apps/files_reminders/img/app.svg", "apps/files_versions/img/app-dark.svg", "apps/files_versions/img/app.svg", "apps/files_trashbin/img/app-dark.svg", "apps/files_trashbin/img/app.svg", "apps/theming/img/accessibility-dark.svg", "apps/testing/img/app.svg", "apps/sharebymail/img/app.svg", "apps/systemtags/img/app.svg", "apps/provisioning_api/img/app.svg", "apps/settings/img/apps.svg", "apps/settings/img/change.svg", "apps/settings/img/help.svg", "apps/settings/img/password.svg", "apps/settings/img/personal.png", "apps/settings/img/personal.svg", "apps/settings/img/share.svg", "apps/settings/img/users-white.svg", "apps/settings/img/users.svg", "apps/files_sharing/img/app.svg", "apps/settings/img/admin.svg", "apps/settings/img/library_add_check.svg", "apps/settings/img/settings.svg", "apps/settings/img/settings_apps.svg", "apps/settings/img/toggle-filelist.svg", "apps/webhook_listeners/img/app-dark.svg", "apps/webhook_listeners/img/app.svg", "core/img/filetypes/application-pdf.svg", "core/img/breadcrumb.svg", "core/img/filetypes/application.svg", "core/img/filetypes/audio.svg", "core/img/filetypes/file.svg", "core/img/filetypes/folder-drag-accept.svg", "core/img/filetypes/folder-encrypted.svg", "core/img/filetypes/folder-external.svg", "core/img/filetypes/folder-public.svg", "core/img/filetypes/folder-shared.svg", "core/img/filetypes/folder-starred.svg", "core/img/filetypes/folder.svg", "core/img/filetypes/image.svg", "core/img/filetypes/link.svg", "core/img/filetypes/location.svg", "core/img/filetypes/mindmap.svg", "core/img/filetypes/text-calendar.svg", "core/img/filetypes/text-code.svg", "core/img/filetypes/text-vcard.svg", "core/img/filetypes/text.svg", "core/img/filetypes/video.svg", "core/img/filetypes/x-office-document.svg", "core/img/filetypes/x-office-drawing.svg", "core/img/filetypes/x-office-form-template.svg", "core/img/filetypes/x-office-form.svg", "core/img/filetypes/x-office-presentation.svg", "core/img/filetypes/x-office-spreadsheet.svg", "core/img/places/calendar-dark.png", "core/img/places/calendar.png", "core/img/places/calendar.svg", "core/img/places/contacts-dark.png", "core/img/places/contacts.svg", "core/img/places/default-app-icon.svg", "core/img/places/files.svg", "core/img/places/home.svg", "core/img/places/link.svg", "core/img/places/music.svg", "core/img/places/picture.svg", "core/img/rating/s0.svg", "core/img/rating/s1.svg", "core/img/rating/s10.svg", "core/img/rating/s2.svg", "core/img/rating/s3.svg", "core/img/rating/s4.svg", "core/img/rating/s5.svg", "core/img/rating/s6.svg", "core/img/rating/s7.svg", "core/img/rating/s8.svg", "core/img/rating/s9.svg", "core/img/mail.svg", "core/img/rss.svg", "core/img/clients/desktop.svg", "core/img/clients/phone.svg", "core/img/clients/tablet.svg", "core/img/categories/auth.svg", "core/img/categories/bundles.svg", "core/img/categories/customization.svg", "core/img/categories/files.svg", "core/img/categories/games.svg", "core/img/categories/integration.svg", "core/img/categories/monitoring.svg", "core/img/categories/multimedia.svg", "core/img/categories/office.svg", "core/img/categories/organization.svg", "core/img/categories/social.svg", "core/img/categories/workflow.svg", "core/img/apps/circles.svg", "core/img/apps/notes.svg", "core/img/apps/richdocuments.svg", "core/img/caldav/attendees.png", "core/img/caldav/attendees.svg", "core/img/caldav/description.png", "core/img/caldav/description.svg", "core/img/caldav/link.png", "core/img/caldav/link.svg", "core/img/caldav/location.png", "core/img/caldav/location.svg", "core/img/caldav/organizer.png", "core/img/caldav/organizer.svg", "core/img/caldav/time.png", "core/img/caldav/time.svg", "core/img/caldav/title.png", "core/img/caldav/title.svg", "core/img/actions/add-folder-description.svg", "core/img/actions/add.svg", "core/img/actions/address.png", "core/img/actions/address.svg", "core/img/actions/alert-outline.svg", "core/img/actions/arrow-left.svg", "core/img/actions/arrow-right.svg", "core/img/actions/audio-off.svg", "core/img/actions/audio.svg", "core/img/actions/caret-white.svg", "core/img/actions/caret.svg", "core/img/actions/change.svg", "core/img/actions/clippy.svg", "core/img/actions/close.svg", "core/img/actions/comment.png", "core/img/actions/comment.svg", "core/img/actions/confirm-fade.svg", "core/img/actions/confirm-white.svg", "core/img/actions/confirm.svg", "core/img/actions/delete.png", "core/img/actions/delete.svg", "core/img/actions/details.svg", "core/img/actions/disabled-user.svg", "core/img/actions/disabled-users.svg", "core/img/actions/download.png", "core/img/actions/download.svg", "core/img/actions/edit.svg", "core/img/actions/error-white.svg", "core/img/actions/error.svg", "core/img/actions/external.svg", "core/img/actions/filter.svg", "core/img/actions/fullscreen.svg", "core/img/actions/group.svg", "core/img/actions/history.png", "core/img/actions/history.svg", "core/img/actions/info-white.svg", "core/img/actions/info.png", "core/img/actions/info.svg", "core/img/actions/logout.svg", "core/img/actions/mail.svg", "core/img/actions/menu-sidebar.svg", "core/img/actions/menu.svg", "core/img/actions/more-white.svg", "core/img/actions/more.png", "core/img/actions/more.svg", "core/img/actions/password-white.svg", "core/img/actions/password.png", "core/img/actions/password.svg", "core/img/actions/pause.svg", "core/img/actions/phone.svg", "core/img/actions/play-add.svg", "core/img/actions/play-next.svg", "core/img/actions/play-previous.svg", "core/img/actions/play.svg", "core/img/actions/profile.svg", "core/img/actions/projects.svg", "core/img/actions/public-white.svg", "core/img/actions/public.svg", "core/img/actions/quota.svg", "core/img/actions/recent.svg", "core/img/actions/rename.svg", "core/img/actions/screen-off.svg", "core/img/actions/screen.svg", "core/img/actions/search.svg", "core/img/actions/settings-dark.svg", "core/img/actions/settings.svg", "core/img/actions/share.png", "core/img/actions/share.svg", "core/img/actions/shared.svg", "core/img/actions/sound-off.svg", "core/img/actions/sound.svg", "core/img/actions/star-dark.svg", "core/img/actions/star.png", "core/img/actions/star.svg", "core/img/actions/starred.png", "core/img/actions/starred.svg", "core/img/actions/tag.png", "core/img/actions/tag.svg", "core/img/actions/template-add.svg", "core/img/actions/timezone.svg", "core/img/actions/toggle-background.svg", "core/img/actions/toggle-filelist.svg", "core/img/actions/toggle-pictures.svg", "core/img/actions/toggle.svg", "core/img/actions/triangle-e.svg", "core/img/actions/triangle-n.svg", "core/img/actions/triangle-s.svg", "core/img/actions/unshare.svg", "core/img/actions/upload.svg", "core/img/actions/user-admin.svg", "core/img/actions/user.svg", "core/img/actions/verified.svg", "core/img/actions/verify.svg", "core/img/actions/verifying.svg", "core/img/actions/video-off.svg", "core/img/actions/video-switch.svg", "core/img/actions/video.svg", "core/img/actions/view-close.svg", "core/img/actions/view-download.svg", "core/img/actions/view-next.svg", "core/img/actions/view-pause.svg", "core/img/actions/view-play.svg", "core/img/actions/view-previous.svg", "core/img/places/contacts-dark.png", "core/img/places/contacts.svg", "apps/user_status/img/app-dark.svg", "apps/user_status/img/app.svg", "apps/theming/img/app-dark.svg", "apps/theming/img/app.svg", "apps/updatenotification/img/app.svg", "apps/weather_status/img/app-dark.svg", "apps/weather_status/img/app.svg", "apps/updatenotification/img/notification.svg"] +path = ["apps/encryption/img/app.svg", "apps/federation/img/app.svg", "apps/files/img/add-color.png", "apps/files/img/add-color.svg", "apps/files/img/app-dark.svg", "apps/files/img/app.svg", "apps/files/img/change.png", "apps/files/img/change.svg", "apps/files/img/delete-color.png", "apps/files/img/delete-color.svg", "apps/files/img/folder.svg", "apps/files/img/folder-move.svg", "apps/files_external/img/app-dark.svg", "apps/files_external/img/app.svg", "apps/user_ldap/img/app-dark.svg", "apps/user_ldap/img/app.svg", "apps/comments/img/comments-dark.svg", "apps/comments/img/comments.svg", "apps/files_reminders/img/app-dark.svg", "apps/files_reminders/img/app.svg", "apps/files_versions/img/app-dark.svg", "apps/files_versions/img/app.svg", "apps/files_trashbin/img/app-dark.svg", "apps/files_trashbin/img/app.svg", "apps/theming/img/accessibility-dark.svg", "apps/testing/img/app.svg", "apps/sharebymail/img/app.svg", "apps/systemtags/img/app.svg", "apps/provisioning_api/img/app.svg", "apps/settings/img/change.svg", "apps/settings/img/help.svg", "apps/settings/img/password.svg", "apps/settings/img/personal.png", "apps/settings/img/personal.svg", "apps/settings/img/share.svg", "apps/settings/img/users-white.svg", "apps/settings/img/users.svg", "apps/files_sharing/img/app.svg", "apps/settings/img/admin.svg", "apps/settings/img/library_add_check.svg", "apps/settings/img/settings.svg", "apps/settings/img/settings_apps.svg", "apps/settings/img/toggle-filelist.svg", "apps/webhook_listeners/img/app-dark.svg", "apps/webhook_listeners/img/app.svg", "core/img/filetypes/application-pdf.svg", "core/img/breadcrumb.svg", "core/img/filetypes/application.svg", "core/img/filetypes/audio.svg", "core/img/filetypes/file.svg", "core/img/filetypes/folder-drag-accept.svg", "core/img/filetypes/folder-encrypted.svg", "core/img/filetypes/folder-external.svg", "core/img/filetypes/folder-public.svg", "core/img/filetypes/folder-shared.svg", "core/img/filetypes/folder-starred.svg", "core/img/filetypes/folder.svg", "core/img/filetypes/image.svg", "core/img/filetypes/link.svg", "core/img/filetypes/location.svg", "core/img/filetypes/mindmap.svg", "core/img/filetypes/text-calendar.svg", "core/img/filetypes/text-code.svg", "core/img/filetypes/text-vcard.svg", "core/img/filetypes/text.svg", "core/img/filetypes/video.svg", "core/img/filetypes/x-office-document.svg", "core/img/filetypes/x-office-drawing.svg", "core/img/filetypes/x-office-form-template.svg", "core/img/filetypes/x-office-form.svg", "core/img/filetypes/x-office-presentation.svg", "core/img/filetypes/x-office-spreadsheet.svg", "core/img/places/calendar-dark.png", "core/img/places/calendar.png", "core/img/places/calendar.svg", "core/img/places/contacts-dark.png", "core/img/places/contacts.svg", "core/img/places/default-app-icon.svg", "core/img/places/files.svg", "core/img/places/home.svg", "core/img/places/link.svg", "core/img/places/music.svg", "core/img/places/picture.svg", "core/img/rating/s0.svg", "core/img/rating/s1.svg", "core/img/rating/s10.svg", "core/img/rating/s2.svg", "core/img/rating/s3.svg", "core/img/rating/s4.svg", "core/img/rating/s5.svg", "core/img/rating/s6.svg", "core/img/rating/s7.svg", "core/img/rating/s8.svg", "core/img/rating/s9.svg", "core/img/mail.svg", "core/img/rss.svg", "core/img/clients/desktop.svg", "core/img/clients/phone.svg", "core/img/clients/tablet.svg", "core/img/categories/auth.svg", "core/img/categories/bundles.svg", "core/img/categories/customization.svg", "core/img/categories/files.svg", "core/img/categories/games.svg", "core/img/categories/integration.svg", "core/img/categories/monitoring.svg", "core/img/categories/multimedia.svg", "core/img/categories/office.svg", "core/img/categories/organization.svg", "core/img/categories/social.svg", "core/img/categories/workflow.svg", "core/img/apps/circles.svg", "core/img/apps/notes.svg", "core/img/apps/richdocuments.svg", "core/img/caldav/attendees.png", "core/img/caldav/attendees.svg", "core/img/caldav/description.png", "core/img/caldav/description.svg", "core/img/caldav/link.png", "core/img/caldav/link.svg", "core/img/caldav/location.png", "core/img/caldav/location.svg", "core/img/caldav/organizer.png", "core/img/caldav/organizer.svg", "core/img/caldav/time.png", "core/img/caldav/time.svg", "core/img/caldav/title.png", "core/img/caldav/title.svg", "core/img/actions/add-folder-description.svg", "core/img/actions/add.svg", "core/img/actions/address.png", "core/img/actions/address.svg", "core/img/actions/alert-outline.svg", "core/img/actions/arrow-left.svg", "core/img/actions/arrow-right.svg", "core/img/actions/audio-off.svg", "core/img/actions/audio.svg", "core/img/actions/caret-white.svg", "core/img/actions/caret.svg", "core/img/actions/change.svg", "core/img/actions/clippy.svg", "core/img/actions/close.svg", "core/img/actions/comment.png", "core/img/actions/comment.svg", "core/img/actions/confirm-fade.svg", "core/img/actions/confirm-white.svg", "core/img/actions/confirm.svg", "core/img/actions/delete.png", "core/img/actions/delete.svg", "core/img/actions/details.svg", "core/img/actions/disabled-user.svg", "core/img/actions/disabled-users.svg", "core/img/actions/download.png", "core/img/actions/download.svg", "core/img/actions/edit.svg", "core/img/actions/error-white.svg", "core/img/actions/error.svg", "core/img/actions/external.svg", "core/img/actions/filter.svg", "core/img/actions/fullscreen.svg", "core/img/actions/group.svg", "core/img/actions/history.png", "core/img/actions/history.svg", "core/img/actions/info-white.svg", "core/img/actions/info.png", "core/img/actions/info.svg", "core/img/actions/logout.svg", "core/img/actions/mail.svg", "core/img/actions/menu-sidebar.svg", "core/img/actions/menu.svg", "core/img/actions/more-white.svg", "core/img/actions/more.png", "core/img/actions/more.svg", "core/img/actions/password-white.svg", "core/img/actions/password.png", "core/img/actions/password.svg", "core/img/actions/pause.svg", "core/img/actions/phone.svg", "core/img/actions/play-add.svg", "core/img/actions/play-next.svg", "core/img/actions/play-previous.svg", "core/img/actions/play.svg", "core/img/actions/profile.svg", "core/img/actions/projects.svg", "core/img/actions/public-white.svg", "core/img/actions/public.svg", "core/img/actions/quota.svg", "core/img/actions/recent.svg", "core/img/actions/rename.svg", "core/img/actions/screen-off.svg", "core/img/actions/screen.svg", "core/img/actions/search.svg", "core/img/actions/settings-dark.svg", "core/img/actions/settings.svg", "core/img/actions/share.png", "core/img/actions/share.svg", "core/img/actions/shared.svg", "core/img/actions/sound-off.svg", "core/img/actions/sound.svg", "core/img/actions/star-dark.svg", "core/img/actions/star.png", "core/img/actions/star.svg", "core/img/actions/starred.png", "core/img/actions/starred.svg", "core/img/actions/tag.png", "core/img/actions/tag.svg", "core/img/actions/template-add.svg", "core/img/actions/timezone.svg", "core/img/actions/toggle-background.svg", "core/img/actions/toggle-filelist.svg", "core/img/actions/toggle-pictures.svg", "core/img/actions/toggle.svg", "core/img/actions/triangle-e.svg", "core/img/actions/triangle-n.svg", "core/img/actions/triangle-s.svg", "core/img/actions/unshare.svg", "core/img/actions/upload.svg", "core/img/actions/user-admin.svg", "core/img/actions/user.svg", "core/img/actions/verified.svg", "core/img/actions/verify.svg", "core/img/actions/verifying.svg", "core/img/actions/video-off.svg", "core/img/actions/video-switch.svg", "core/img/actions/video.svg", "core/img/actions/view-close.svg", "core/img/actions/view-download.svg", "core/img/actions/view-next.svg", "core/img/actions/view-pause.svg", "core/img/actions/view-play.svg", "core/img/actions/view-previous.svg", "core/img/places/contacts-dark.png", "core/img/places/contacts.svg", "apps/user_status/img/app-dark.svg", "apps/user_status/img/app.svg", "apps/theming/img/app-dark.svg", "apps/theming/img/app.svg", "apps/updatenotification/img/app.svg", "apps/weather_status/img/app-dark.svg", "apps/weather_status/img/app.svg", "apps/updatenotification/img/notification.svg"] precedence = "aggregate" SPDX-FileCopyrightText = "2018-2024 Google LLC" SPDX-License-Identifier = "Apache-2.0" @@ -292,23 +244,11 @@ SPDX-FileCopyrightText = "2019 Simran-B = {} - 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() - } - }) - - // Check if we are running the setup checks - if (process.env.SETUP_TESTING === 'true') { - console.log('Adding setup tests to specPattern 🧮') - config.specPattern = [join(__dirname, 'cypress/e2e/core/setup.ts')] - console.log('└─ Done') - } else { - // If we are not running the setup tests, we need to remove the setup tests from the specPattern - 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` - // if needed for the setup tests, connect to the actions network - await connectToActionsNetwork() - // 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']) - // Disable the unsupported-browser redirect so Cypress (Chrome 118 / Electron 27) - // does not hit the "Your browser is not supported" page on every visit. - runOcc(['config:system:set', 'no_unsupported_browser_warning', '--value', 'true', '--type', 'boolean']) - - // 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 - }, - }, -}) - -/** - * Connect the running test container to the GitHub Actions network - */ -async function connectToActionsNetwork() { - if (process.env.SETUP_TESTING !== 'true') { - console.log('├─ Not running setup tests, skipping actions network connection 🌐') - return - } - - console.log('├─ Looking for github actions network... 🔍') - const networks = await docker.listNetworks() - const network = networks.find((network) => network.Name.startsWith('github_network')) - if (!network) { - console.log('│ └─ No actions network found ⚠️') - return - } - - console.log('│ |─ Found actions network: ' + network.Name) - await docker.getNetwork(network.Id) - .connect({ Container: getContainer().id }) - console.log('│ └─ Connected to actions network 🌐') -} diff --git a/cypress/e2e/appstore/apps.cy.ts b/cypress/e2e/appstore/apps.cy.ts deleted file mode 100644 index 391ab163f9a4e..0000000000000 --- a/cypress/e2e/appstore/apps.cy.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023-2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { handlePasswordConfirmation } from '../core-utils.ts' - -const admin = new User('admin', 'admin') - -describe('Settings: App management', { testIsolation: true }, () => { - beforeEach(() => { - // disable QA if already enabled - cy.runOccCommand('app:disable -n testing') - // enable notification if already disabled - cy.runOccCommand('app:enable -n updatenotification') - - // I am logged in as the admin - cy.login(admin) - - // Intercept the apps list request - cy.intercept('GET', '/ocs/v2.php/apps/appstore/api/v1/apps').as('fetchAppsList') - - // I open the Apps management - cy.visit('/settings/apps/installed') - - // Wait for the apps list to load - cy.wait('@fetchAppsList') - }) - - it('Can enable an installed app', () => { - cy.intercept('POST', '/ocs/v2.php/apps/appstore/api/v1/apps/enable').as('enableApp') - - cy.findByRole('table').should('exist') - // Wait for the app list to load - .contains('tr', 'QA testing', { timeout: 10000 }) - .should('exist') - .findByRole('button', { name: 'Enable' }) - // I enable the "QA testing" app - .click({ force: true }) - - handlePasswordConfirmation(admin.password) - - cy.wait('@enableApp') - - // Wait until we see the disable button for the app - cy.findByRole('table').should('exist') - .contains('tr', 'QA testing') - .should('exist') - // I see the disable button for the app - .findByRole('button', { name: 'Disable' }) - .should('be.visible') - - // Change to enabled apps view - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Active apps' }) - .should('be.visible') - .click({ force: true }) - }) - - cy.url().should('match', /settings\/apps\/enabled$/) - // I see that the "QA testing" app has been enabled - cy.findByRole('table') - .contains('tr', 'QA testing') - }) - - it('Can disable an installed app', () => { - cy.intercept('POST', '/ocs/v2.php/apps/appstore/api/v1/apps/disable').as('disableApp') - - cy.findByRole('table') - .should('exist') - // Wait for the app list to load - .contains('tr', 'Update notification', { timeout: 10000 }) - .should('exist') - // I disable the "Update notification" app - .findByRole('button', { name: 'Disable' }) - .click({ force: true }) - - handlePasswordConfirmation(admin.password) - cy.wait('@disableApp') - - // Wait until we see the disable button for the app - cy.findByRole('table').should('exist') - .contains('tr', 'Update notification') - .should('exist') - // I see the enable button for the app - .findByRole('button', { name: 'Enable' }) - .should('exist') - - // Change to disabled apps view - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Disabled apps' }).click({ force: true }) - }) - cy.url().should('match', /settings\/apps\/disabled$/) - - // I see that the "Update notification" app has been disabled - cy.findByRole('table') - .contains('tr', 'Update notification') - }) - - it('Browse enabled apps', () => { - // When I open the "Active apps" section - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Active apps' }) - .should('be.visible') - .click({ force: true }) - }) - - // Then I see that the current section is "Active apps" - cy.url().should('match', /settings\/apps\/enabled$/) - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Active apps', current: 'page' }) - .should('be.visible') - }) - - // I see that there are only enabled apps - cy.findByRole('table') - .should('exist') - .find('tr button') - .each(($action) => { - cy.wrap($action).should('not.contain', 'Enable') - }) - }) - - it('Browse disabled apps', () => { - // When I open the "Active Disabled" section - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Disabled apps' }) - .as('disabledAppsLink') - .should('be.visible') - .and('not.have.attr', 'aria-current') - cy.get('@disabledAppsLink') - .click({ force: true }) - }) - - // Then I see that the current section is "Disabled apps" - cy.url().should('match', /settings\/apps\/disabled$/) - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Disabled apps', current: 'page' }) - .should('be.visible') - }) - - // I see that there are only disabled apps - cy.findByRole('table') - .should('exist') - .find('tr button') - .each(($action) => { - cy.wrap($action).should('not.contain', 'Disable') - }) - }) - - it('Browse app bundles', () => { - // When I open the "App bundles" section - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'App bundles' }) - .as('appBundlesLink') - .should('be.visible') - .and('not.have.attr', 'aria-current') - cy.get('@appBundlesLink') - .click({ force: true }) - }) - - // Then I see that the current section is "App bundles" - cy.url().should('match', /settings\/apps\/bundles$/) - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'App bundles', current: 'page' }) - .should('be.visible') - }) - - // I see the app bundles - cy.findByRole('heading', { name: 'Enterprise bundle' }) - .should('be.visible') - cy.findByRole('heading', { name: 'Education bundle' }) - .should('be.visible') - }) - - it('View app details', () => { - // When I click on the "QA testing" app - cy.findByRole('table') - .contains('a', 'QA testing') - .click({ force: true }) - // I see that the app details are shown - cy.get('#app-sidebar-vue') - .should('be.visible') - .find('.app-sidebar-header__info') - .should('contain', 'QA testing') - cy.get('#app-sidebar-vue').contains('a', 'View in store').should('exist') - cy.get('#app-sidebar-vue') - .findByRole('button', { name: 'Enable' }) - .should('be.visible') - cy.get('#app-sidebar-vue') - .findByRole('button', { name: 'Remove' }) - .should('be.visible') - cy.get('#app-sidebar-vue').contains(/Version \d+\.\d+\.\d+/).should('be.visible') - }) - - it('Limit app usage to group', () => { - // When I open the "Active apps" section - cy.findByRole('navigation', { name: 'Appstore categories' }) - .within(() => { - cy.findByRole('link', { name: 'Active apps' }) - .should('be.visible') - .click({ force: true }) - }) - - // Then I see that the current section is "Active apps" - cy.url().should('match', /settings\/apps\/enabled$/) - - // Then I select the app - cy.findByRole('table') - .should('exist') - .contains('tr a', 'Dashboard', { timeout: 10000 }) - .click() - - // Then I enable "limit app to group" - cy.findByRole('button', { name: 'Limit to groups' }) - .click() - - // Then I select a group - cy.findByRole('dialog') - .should('be.visible') - .within(() => { - cy.get('input') - .should('be.focused') - .type('admin') - }) - cy.findByRole('option', { name: /admin/ }) - .click() - cy.findByRole('button', { name: 'Save' }) - .click() - - handlePasswordConfirmation(admin.password) - - cy.get('#app-sidebar-vue') - .findByRole('list', { name: 'Limited to groups' }) - .findByRole('listitem', { name: /admin/ }) - .should('be.visible') - - // Then I disable the group limitation - cy.get('#app-sidebar-vue') - .findByRole('button', { name: 'Limit to groups' }) - .click() - cy.findByRole('dialog') - .should('be.visible') - .within(() => { - cy.findByRole('button', { name: 'Deselect admin' }) - .should('be.visible') - .click() - cy.findByRole('button', { name: 'Save' }) - .click() - }) - - handlePasswordConfirmation(admin.password) - - cy.get('#app-sidebar-vue') - .findByRole('list', { name: 'Limited to groups' }) - .should('not.exist') - }) - - /* - * TODO: Improve testing with app store as external API - * The following scenarios require the files_antivirus and calendar app - * being present in the app store with support for the current server version - * Ideally we would have either a dummy app store endpoint with some test apps - * or even an app store instance running somewhere to properly test this. - * This is also a requirement to properly test updates of apps - */ - // TODO: View app details for app store apps - // TODO: Install an app from the app store - // TODO: Show section from app store -}) 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/core/404-error.cy.ts b/cypress/e2e/core/404-error.cy.ts deleted file mode 100644 index b24562933e8bb..0000000000000 --- a/cypress/e2e/core/404-error.cy.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -describe('404 error page', { testIsolation: true }, () => { - it('renders 404 page', () => { - cy.visit('/doesnotexist', { failOnStatusCode: false }) - - cy.findByRole('heading', { name: /Page not found/ }) - .should('be.visible') - cy.findByRole('link', { name: /Back to Nextcloud/ }) - .should('be.visible') - .click() - - cy.url() - .should('match', /(\/index.php)\/login$/) - }) -}) diff --git a/cypress/e2e/core/header_access-levels.cy.ts b/cypress/e2e/core/header_access-levels.cy.ts deleted file mode 100644 index e04109bfb3b26..0000000000000 --- a/cypress/e2e/core/header_access-levels.cy.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState, getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Header: Ensure regular users do not have admin settings in the Settings menu', { testIsolation: true }, () => { - beforeEach(() => { - clearState() - }) - - it('Regular users can see basic items in the Settings menu', () => { - // Given I am logged in - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/') - }) - // I open the settings menu - getNextcloudUserMenuToggle().click() - - getNextcloudUserMenu().find('ul').within(($el) => { - // I see the settings menu is open - cy.wrap($el).should('be.visible') - - // I see that the Settings menu has only 6 items - cy.get('li').should('have.length', 6) - // I see that the "View profile" item in the Settings menu is shown - cy.contains('li', 'View profile').should('be.visible') - // I see that the "Set status" item in the Settings menu is shown - cy.contains('li', 'Set status').should('be.visible') - // I see that the "Appearance and accessibility" item in the Settings menu is shown - cy.contains('li', 'Appearance and accessibility').should('be.visible') - // I see that the "Settings" item in the Settings menu is shown - cy.contains('li', 'Settings').should('be.visible') - // I see that the "Help" item in the Settings menu is shown - cy.contains('li', 'Help').should('be.visible') - // I see that the "Log out" item in the Settings menu is shown - cy.contains('li', 'Log out').should('be.visible') - }) - }) - - it('Regular users cannot see admin-level items in the Settings menu', () => { - // Given I am logged in - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/') - }) - // I open the settings menu - getNextcloudUserMenuToggle().click() - - getNextcloudUserMenu().find('ul').within(($el) => { - // I see the settings menu is open - cy.wrap($el).should('be.visible') - - // I see that the "Users" item in the Settings menu is NOT shown - cy.contains('li', 'Users').should('not.exist') - // I see that the "Administration settings" item in the Settings menu is NOT shown - cy.contains('li', 'Administration settings').should('not.exist') - cy.get('#admin_settings').should('not.exist') - }) - }) - - it('Admin users can see admin-level items in the Settings menu', () => { - // Given I am logged in - cy.login(admin) - cy.visit('/') - - // I open the settings menu - getNextcloudUserMenuToggle().click() - - getNextcloudUserMenu().find('ul').within(($el) => { - // I see the settings menu is open - cy.wrap($el).should('be.visible') - - // I see that the Settings menu has only 9 items - cy.get('li').should('have.length', 9) - // I see that the "Set status" item in the Settings menu is shown - cy.contains('li', 'View profile').should('be.visible') - // I see that the "Set status" item in the Settings menu is shown - cy.contains('li', 'Set status').should('be.visible') - // I see that the "Appearance and accessibility" item in the Settings menu is shown - cy.contains('li', 'Appearance and accessibility').should('be.visible') - // I see that the "Personal Settings" item in the Settings menu is shown - cy.contains('li', 'Personal settings').should('be.visible') - // I see that the "Administration settings" item in the Settings menu is shown - cy.contains('li', 'Administration settings').should('be.visible') - // I see that the "Apps" item in the Settings menu is shown - cy.contains('li', 'Apps').should('be.visible') - // I see that the "Users" item in the Settings menu is shown - cy.contains('li', 'Accounts').should('be.visible') - // I see that the "Help" item in the Settings menu is shown - cy.contains('li', 'Help').should('be.visible') - // I see that the "Log out" item in the Settings menu is shown - cy.contains('li', 'Log out').should('be.visible') - }) - }) -}) diff --git a/cypress/e2e/core/header_app-menu.cy.ts b/cypress/e2e/core/header_app-menu.cy.ts deleted file mode 100644 index aad8a816678b3..0000000000000 --- a/cypress/e2e/core/header_app-menu.cy.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState, getNextcloudHeader } from '../../support/commonUtils.ts' - -const getAppMenu = () => getNextcloudHeader().find('.app-menu') -// Both triggers share aria-label="Open apps menu", so getByRole can't -// disambiguate them. BEM classes owned by the component under test are -// the next-best stable selectors. -const getWaffleTrigger = () => getAppMenu().find('.app-menu__waffle') - -describe('Header: App menu (waffle launcher)', { testIsolation: true }, () => { - beforeEach(() => { - clearState() - }) - - describe('Open and click', () => { - beforeEach(() => { - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/') - }) - }) - - it('opens the popover and navigates when a tile is clicked', () => { - getWaffleTrigger().click() - cy.get('.app-menu__popover').should('be.visible') - getWaffleTrigger().should('have.attr', 'aria-expanded', 'true') - - cy.findAllByRole('menuitem').first() - .should('be.visible') - .then(($tile) => { - const href = $tile.attr('href') - expect(href).to.match(/\/apps\//) - cy.wrap($tile).click() - cy.location('pathname').should('include', '/apps/') - }) - }) - }) - - describe('Admin gating: "More apps" tile', () => { - const admin = new User('admin', 'admin') - - beforeEach(() => { - cy.login(admin) - cy.visit('/') - }) - - it('shows the "More apps" tile for admins', () => { - getWaffleTrigger().click() - cy.get('.app-menu__popover').should('be.visible') - cy.findByRole('menuitem', { name: 'More apps' }).should('be.visible') - }) - }) -}) diff --git a/cypress/e2e/core/header_contacts-menu.cy.ts b/cypress/e2e/core/header_contacts-menu.cy.ts deleted file mode 100644 index 657a50bc85c5d..0000000000000 --- a/cypress/e2e/core/header_contacts-menu.cy.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState, getNextcloudHeader } from '../../support/commonUtils.ts' -import { randomString } from '../../support/utils/randomString.ts' - -const admin = new User('admin', 'admin') - -const getContactsMenu = () => getNextcloudHeader().find('#header-menu-contactsmenu') -const getContactsMenuToggle = () => getNextcloudHeader().find('#contactsmenu .header-menu__trigger') -const getContactsSearch = () => getContactsMenu().find('#contactsmenu__menu__search') - -describe('Header: Contacts menu', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - // clear user and group state - clearState() - // ensure the contacts menu is not restricted - cy.runOccCommand('config:app:set --value no core shareapi_restrict_user_enumeration_to_group') - // create a new user for testing the contacts - cy.createRandomUser().then(($user) => { - user = $user - }) - - // Given I am logged in as the admin - cy.login(admin) - cy.visit('/') - }) - - it('Other users are seen in the contacts menu', () => { - // When I open the Contacts menu - getContactsMenuToggle().click() - // I see that the Contacts menu is shown - getContactsMenu().should('exist') - // I see that the contact user in the Contacts menu is shown - getContactsMenu().contains('li.contact', user.userId).should('be.visible') - // I see that the contact "admin" in the Contacts menu is not shown - getContactsMenu().contains('li.contact', admin.userId).should('not.exist') - }) - - it('Just added users are seen in the contacts menu', () => { - // I create a new user - const newUserName = randomString(7) - // we can not use createRandomUser as it will invalidate the session - cy.runOccCommand(`user:add --password-from-env '${newUserName}'`, { env: { OC_PASS: '1234567' } }) - // I open the Contacts menu - getContactsMenuToggle().click() - // I see that the Contacts menu is shown - getContactsMenu().should('exist') - // I see that the contact user in the Contacts menu is shown - getContactsMenu().contains('li.contact', user.userId).should('be.visible') - // I see that the contact of the new user in the Contacts menu is shown - getContactsMenu().contains('li.contact', newUserName).should('be.visible') - // I see that the contact "admin" in the Contacts menu is not shown - getContactsMenu().contains('li.contact', admin.userId).should('not.exist') - }) - - it('Search for other users in the contacts menu', () => { - cy.createRandomUser().then((otherUser) => { - // Given I am logged in as the admin - cy.login(admin) - cy.visit('/') - - // I open the Contacts menu - getContactsMenuToggle().click() - // I see that the Contacts menu is shown - getContactsMenu().should('exist') - // I see that the contact user in the Contacts menu is shown - getContactsMenu().contains('li.contact', user.userId).should('be.visible') - // I see that the contact of the new user in the Contacts menu is shown - getContactsMenu().contains('li.contact', otherUser.userId).should('be.visible') - - // I see that the Contacts menu search input is shown - getContactsSearch().should('exist') - // I search for the otherUser - getContactsSearch().type(otherUser.userId) - // I see that the contact otherUser in the Contacts menu is shown - getContactsMenu().contains('li.contact', otherUser.userId).should('be.visible') - // I see that the contact user in the Contacts menu is not shown - getContactsMenu().contains('li.contact', user.userId).should('not.exist') - // I see that the contact "admin" in the Contacts menu is not shown - getContactsMenu().contains('li.contact', admin.userId).should('not.exist') - }) - }) - - it('Search for unknown users in the contacts menu', () => { - // I open the Contacts menu - getContactsMenuToggle().click() - // I see that the Contacts menu is shown - getContactsMenu().should('exist') - // I see that the contact user in the Contacts menu is shown - getContactsMenu().contains('li.contact', user.userId).should('be.visible') - - // I see that the Contacts menu search input is shown - getContactsSearch().should('exist') - // I search for an unknown user - getContactsSearch().type('surely-unknown-user') - // I see that the no results message in the Contacts menu is shown - getContactsMenu().find('ul li').should('have.length', 0) - // I see that the contact user in the Contacts menu is not shown - getContactsMenu().contains('li.contact', user.userId).should('not.exist') - // I see that the contact "admin" in the Contacts menu is not shown - getContactsMenu().contains('li.contact', admin.userId).should('not.exist') - }) - - it('Users from other groups are not seen in the contacts menu when autocompletion is restricted within the same group', () => { - // I enable restricting username autocompletion to groups - cy.runOccCommand('config:app:set --value yes core shareapi_restrict_user_enumeration_to_group') - // I open the Contacts menu - getContactsMenuToggle().click() - // I see that the Contacts menu is shown - getContactsMenu().should('exist') - // I see that the contact user in the Contacts menu is not shown - getContactsMenu().contains('li.contact', user.userId).should('not.exist') - // I see that the contact "admin" in the Contacts menu is not shown - getContactsMenu().contains('li.contact', admin.userId).should('not.exist') - - // I close the Contacts menu - getContactsMenuToggle().click() - // I disable restricting username autocompletion to groups - cy.runOccCommand('config:app:set --value no core shareapi_restrict_user_enumeration_to_group') - // I open the Contacts menu - getContactsMenuToggle().click() - // I see that the Contacts menu is shown - getContactsMenu().should('exist') - // I see that the contact user in the Contacts menu is shown - getContactsMenu().contains('li.contact', user.userId).should('be.visible') - // I see that the contact "admin" in the Contacts menu is not shown - getContactsMenu().contains('li.contact', admin.userId).should('not.exist') - }) -}) diff --git a/cypress/e2e/core/setup.ts b/cypress/e2e/core/setup.ts deleted file mode 100644 index 90391dbf70192..0000000000000 --- a/cypress/e2e/core/setup.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { randomString } from '../../support/utils/randomString.ts' -import { handlePasswordConfirmation } from '../core-utils.ts' - -type RecommendedAppsMode = 'skip' | 'install-success' | 'install-failure' - -/** - * DO NOT RENAME THIS FILE to .cy.ts ⚠️ - * This is not following the pattern of the other files in this folder - * because it is manually added to the tests by the cypress config. - */ -describe('Can install Nextcloud', { testIsolation: true, retries: 0 }, () => { - beforeEach(() => { - // Move the config file and data folder - cy.runCommand('rm /var/www/html/config/config.php', { failOnNonZeroExit: false }) - cy.runCommand('rm /var/www/html/data/owncloud.db', { failOnNonZeroExit: false }) - }) - - it('Sqlite', () => { - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminlogin"]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminpass"]').should('be.visible') - cy.get('[data-cy-setup-form-field="directory"]').should('have.value', '/var/www/html/data') - - // Select the SQLite database - cy.get('[data-cy-setup-form-field="dbtype-sqlite"] input').check({ force: true }) - - sharedSetup() - }) - - it('Sqlite - Install recommended apps (success)', () => { - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="dbtype-sqlite"] input').check({ force: true }) - - sharedSetup('install-success') - }) - - it('Sqlite - Install recommended apps (failure)', () => { - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="dbtype-sqlite"] input').check({ force: true }) - - sharedSetup('install-failure') - }) - - it('MySQL', () => { - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminlogin"]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminpass"]').should('be.visible') - cy.get('[data-cy-setup-form-field="directory"]').should('have.value', '/var/www/html/data') - - // Select the SQLite database - cy.get('[data-cy-setup-form-field="dbtype-mysql"] input').check({ force: true }) - - // Fill in the DB form - cy.get('[data-cy-setup-form-field="dbuser"]').type('{selectAll}oc_autotest') - cy.get('[data-cy-setup-form-field="dbpass"]').type('{selectAll}nextcloud') - cy.get('[data-cy-setup-form-field="dbname"]').type('{selectAll}oc_autotest') - cy.get('[data-cy-setup-form-field="dbhost"]').type('{selectAll}mysql:3306') - - sharedSetup() - }) - - it('MariaDB', () => { - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminlogin"]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminpass"]').should('be.visible') - cy.get('[data-cy-setup-form-field="directory"]').should('have.value', '/var/www/html/data') - - // Select the SQLite database - cy.get('[data-cy-setup-form-field="dbtype-mysql"] input').check({ force: true }) - - // Fill in the DB form - cy.get('[data-cy-setup-form-field="dbuser"]').type('{selectAll}oc_autotest') - cy.get('[data-cy-setup-form-field="dbpass"]').type('{selectAll}nextcloud') - cy.get('[data-cy-setup-form-field="dbname"]').type('{selectAll}oc_autotest') - cy.get('[data-cy-setup-form-field="dbhost"]').type('{selectAll}mariadb:3306') - - sharedSetup() - }) - - it('PostgreSQL', () => { - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminlogin"]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminpass"]').should('be.visible') - cy.get('[data-cy-setup-form-field="directory"]').should('have.value', '/var/www/html/data') - - // Select the SQLite database - cy.get('[data-cy-setup-form-field="dbtype-pgsql"] input').check({ force: true }) - - // Fill in the DB form - cy.get('[data-cy-setup-form-field="dbuser"]').type('{selectAll}root') - cy.get('[data-cy-setup-form-field="dbpass"]').type('{selectAll}rootpassword') - cy.get('[data-cy-setup-form-field="dbname"]').type('{selectAll}nextcloud') - cy.get('[data-cy-setup-form-field="dbhost"]').type('{selectAll}postgres:5432') - - sharedSetup() - }) - - it('Oracle', () => { - Cypress.config('pageLoadTimeout', 200000) - cy.runCommand('cp /var/www/html/tests/databases-all-config.php /var/www/html/config/config.php') - cy.visit('/') - cy.get('[data-cy-setup-form]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminlogin"]').should('be.visible') - cy.get('[data-cy-setup-form-field="adminpass"]').should('be.visible') - cy.get('[data-cy-setup-form-field="directory"]').should('have.value', '/var/www/html/data') - - // Select the SQLite database - cy.get('[data-cy-setup-form-field="dbtype-oci"] input').check({ force: true }) - - // Fill in the DB form - cy.get('[data-cy-setup-form-field="dbuser"]').type('{selectAll}system') - cy.get('[data-cy-setup-form-field="dbpass"]').type('{selectAll}oracle') - cy.get('[data-cy-setup-form-field="dbname"]').type('{selectAll}FREE') - cy.get('[data-cy-setup-form-field="dbhost"]').type('{selectAll}oracle:1521') - - sharedSetup() - }) -}) - -/** - * Shared admin setup function for the Nextcloud setup - * - * @param mode How to handle the recommended apps screen at the end of the - * install assistant: skip it, exercise the install button with a - * stubbed success response, or stub a failure response. - */ -function sharedSetup(mode: RecommendedAppsMode = 'skip') { - const randAdmin = 'admin-' + randomString(10) - - // Mock the app store listing. The recommended-apps view fetches the apps via - // the appstore OCS API (`GET …/apps/appstore/api/v1/apps`), so the fixture - // must be OCS-shaped (`ocs.data`). Keep this in sync with the fixture, which - // currently exposes two recommended apps (calendar, contacts). - cy.intercept('GET', '**/apps/appstore/api/v1/apps', { fixture: 'appstore/apps.json' }) - - // Fill in the form - cy.get('[data-cy-setup-form-field="adminlogin"]').type(randAdmin) - cy.get('[data-cy-setup-form-field="adminpass"]').type(randAdmin) - - // Nothing more to do on sqlite, let's continue - cy.get('[data-cy-setup-form-submit]').click() - - // Wait for the setup to finish - cy.location('pathname', { timeout: 10000 }) - .should('include', '/core/apps/recommended') - - // See the apps setup - cy.get('[data-cy-setup-recommended-apps]') - .should('be.visible') - .within(() => { - cy.findByRole('heading', { name: 'Recommended apps' }) - .should('be.visible') - cy.findByRole('button', { name: 'Skip' }) - .should('be.visible') - cy.findByRole('button', { name: 'Install recommended apps' }) - .should('be.visible') - }) - - if (mode === 'skip') { - // Skip the setup apps - cy.get('[data-cy-setup-recommended-apps-skip]').click() - - // Go to files - cy.visit('/apps/files/') - cy.get('[data-cy-files-content]').should('be.visible') - return - } - - // The recommended apps are installed one after another, each via a single - // OCS enable request (`POST …/apps/appstore/api/v1/apps/enable`). Stub it so - // we exercise the frontend flow without hitting the real app store. - cy.intercept('POST', '**/apps/appstore/api/v1/apps/enable', mode === 'install-success' - ? { statusCode: 200, body: { ocs: { meta: { status: 'ok', statuscode: 200, message: 'OK' }, data: { update_required: false } } } } - : { statusCode: 500, body: { ocs: { meta: { status: 'failure', statuscode: 500, message: 'Forced failure' }, data: [] } } }).as('enableApp') - - cy.get('[data-cy-setup-recommended-apps-install]').click() - - // Each app is enabled with a strict password confirmation, so one dialog is - // shown per app (there is no longer a single bulk request). Confirm every - // dialog and assert each enable request carries a Basic auth header. - // Keep RECOMMENDED_APP_COUNT in sync with the appstore/apps.json fixture. - const RECOMMENDED_APP_COUNT = 2 - for (let i = 0; i < RECOMMENDED_APP_COUNT; i++) { - cy.findByRole('dialog', { name: 'Authentication required' }) - .should('be.visible') - handlePasswordConfirmation(randAdmin) - cy.wait('@enableApp') - .its('request.headers.authorization') - .should('match', /^Basic /) - } - - // The frontend no longer redirects after installing; it stays on the - // recommended-apps page and reflects the per-app result inline. - cy.location('pathname').should('include', '/core/apps/recommended') - if (mode === 'install-success') { - cy.get('[data-cy-setup-recommended-apps]') - .should('contain.text', 'App already installed') - } else { - cy.get('[data-cy-setup-recommended-apps]') - .should('contain.text', 'App download or installation failed') - } -} 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/dav/availability.cy.ts b/cypress/e2e/dav/availability.cy.ts deleted file mode 100644 index c5acff281c4f3..0000000000000 --- a/cypress/e2e/dav/availability.cy.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { clearState } from '../../support/commonUtils.ts' - -describe('Calendar: Availability', { testIsolation: true }, () => { - before(() => { - clearState() - }) - - it('User can see the availability section in settings', () => { - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/settings/user') - }) - - // can see the section - cy.findAllByRole('link', { name: /Availability/ }) - .should('be.visible') - .click() - - cy.url().should('match', /settings\/user\/availability$/) - cy.findByRole('heading', { name: /Availability/, level: 2 }) - .should('be.visible') - }) - - it('Users can set their availability status', () => { - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/settings/user/availability') - }) - - // can see the settings - cy.findByRole('list', { name: 'Weekdays' }) - .should('be.visible') - .within(() => { - cy.contains('li', 'Friday') - .should('be.visible') - .should('contain.text', 'No working hours set') - .as('fridayItem') - .findByRole('button', { name: 'Add slot' }) - .click() - }) - - cy.get('@fridayItem') - .findByLabelText(/start time/i) - .type('09:00') - - cy.get('@fridayItem') - .findByLabelText(/end time/i) - .type('18:00') - - cy.intercept('PROPPATCH', '**/remote.php/dav/calendars/*/inbox').as('saveAvailability') - cy.get('#availability') - .findByRole('button', { name: 'Save' }) - .click() - cy.wait('@saveAvailability') - - cy.reload() - - cy.findByRole('list', { name: 'Weekdays' }) - .should('be.visible') - .within(() => { - cy.contains('li', 'Friday') - .should('be.visible') - .should('not.contain.text', 'No working hours set') - }) - }) - - it('Users can set their absence', () => { - cy.createUser({ language: 'en', password: 'password', userId: 'replacement-user' }) - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/settings/user/availability') - }) - - cy.findByRole('heading', { name: /absence/i }).scrollIntoView() - - cy.findByLabelText(/First day/) - .should('be.visible') - .type('2024-12-24') - - cy.findByLabelText(/Last day/) - .should('be.visible') - .type('2024-12-28') - - cy.findByRole('textbox', { name: /Short absence/ }) - .should('be.visible') - .type('Vacation') - cy.findByRole('textbox', { name: /Long absence/ }) - .should('be.visible') - .type('Happy holidays!') - - cy.intercept('GET', '**/ocs/v2.php/apps/files_sharing/api/v1/sharees?*search=replacement*').as('userSearch') - cy.get('#absence') - .findByLabelText('Out of office replacement (optional)') - .as('userSearchBox') - .click() - cy.get('@userSearchBox') - .type('replacement') - cy.wait('@userSearch') - - cy.findByRole('option', { name: 'replacement-user' }) - .click() - - cy.intercept('POST', '**/ocs/v2.php/apps/dav/api/v1/outOfOffice/*').as('saveAbsence') - cy.get('#absence') - .findByRole('button', { name: 'Save' }) - .click() - cy.wait('@saveAbsence') - - cy.reload() - - // see its saved - cy.findByLabelText(/First day/) - .should('have.value', '2024-12-24') - cy.findByLabelText(/Last day/) - .should('have.value', '2024-12-28') - cy.findByRole('textbox', { name: /Short absence/ }) - .should('have.value', 'Vacation') - cy.findByRole('textbox', { name: /Long absence/ }) - .should('have.value', 'Happy holidays!') - cy.get('#absence') - .find('.vs__selected') - .should('contain.text', 'replacement-user') - }) -}) 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/LivePhotosUtils.ts b/cypress/e2e/files/LivePhotosUtils.ts deleted file mode 100644 index b551cbbdd9db3..0000000000000 --- a/cypress/e2e/files/LivePhotosUtils.ts +++ /dev/null @@ -1,116 +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' - -type SetupInfo = { - snapshot: string - jpgFileId: number - movFileId: number - fileName: string - user: User -} - -/** - * @param user - * @param fileName - * @param requesttoken - * @param metadata - */ -function setMetadata(user: User, fileName: string, requesttoken: string, metadata: object) { - const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?/, '') - cy.request({ - method: 'PROPPATCH', - url: `${base}/remote.php/dav/files/${user.userId}/${fileName}`, - auth: { user: user.userId, pass: user.password }, - headers: { - requesttoken, - }, - body: ` - - - - ${Object.entries(metadata).map(([key, value]) => `<${key}>${value}`).join('\n')} - - - `, - }) -} - -/** - * - * @param enable - */ -export function setShowHiddenFiles(enable: boolean) { - cy.request('/csrftoken').then(({ body }) => { - const requestToken = body.token - const url = `${Cypress.config('baseUrl')}/apps/files/api/v1/config/show_hidden` - cy.request({ - method: 'PUT', - url, - headers: { - 'Content-Type': 'application/json', - requesttoken: requestToken, - }, - body: { value: enable }, - }) - }) - cy.reload() -} - -/** - * - */ -export function setupLivePhotos(): Cypress.Chainable { - return cy.task('getVariable', { key: 'live-photos-data' }) - .then((_setupInfo) => { - const setupInfo = _setupInfo as SetupInfo || {} - if (setupInfo.snapshot) { - cy.restoreState(setupInfo.snapshot) - } else { - let requesttoken: string - - setupInfo.fileName = randomString(10) - - cy.createRandomUser().then((_user) => { - setupInfo.user = _user - }) - - cy.then(() => { - cy.uploadContent(setupInfo.user, new Blob(['jpg file'], { type: 'image/jpg' }), 'image/jpg', `/${setupInfo.fileName}.jpg`) - .then((response) => { setupInfo.jpgFileId = parseInt(response.headers['oc-fileid']) }) - cy.uploadContent(setupInfo.user, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/${setupInfo.fileName}.mov`) - .then((response) => { setupInfo.movFileId = parseInt(response.headers['oc-fileid']) }) - - cy.login(setupInfo.user) - }) - - cy.visit('/apps/files') - - cy.get('head').invoke('attr', 'data-requesttoken').then((_requesttoken) => { - requesttoken = _requesttoken as string - }) - - cy.then(() => { - setMetadata(setupInfo.user, `${setupInfo.fileName}.jpg`, requesttoken, { 'nc:metadata-files-live-photo': setupInfo.movFileId }) - setMetadata(setupInfo.user, `${setupInfo.fileName}.mov`, requesttoken, { 'nc:metadata-files-live-photo': setupInfo.jpgFileId }) - }) - - cy.then(() => { - cy.saveState().then((value) => { - setupInfo.snapshot = value - }) - cy.task('setVariable', { key: 'live-photos-data', value: setupInfo }) - }) - } - return cy.then(() => { - cy.login(setupInfo.user) - cy.visit('/apps/files') - return cy.wrap(setupInfo) - }) - }) -} diff --git a/cypress/e2e/files/drag-n-drop.cy.ts b/cypress/e2e/files/drag-n-drop.cy.ts deleted file mode 100644 index 6f648aca8d238..0000000000000 --- a/cypress/e2e/files/drag-n-drop.cy.ts +++ /dev/null @@ -1,246 +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, navigateToFolder } from './FilesUtils.ts' - -describe('files: Drag and Drop', { testIsolation: true }, () => { - beforeEach(() => { - cy.createRandomUser().then((user) => { - cy.login(user) - }) - cy.visit('/apps/files') - }) - - it('can drop a file', () => { - const dataTransfer = new DataTransfer() - dataTransfer.items.add(new File([], 'single-file.txt')) - - cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') - - // Make sure the drop notice is not visible - cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible') - - // Trigger the drop notice - cy.get('main.app-content').trigger('dragover', { dataTransfer }) - cy.get('[data-cy-files-drag-drop-area]').should('be.visible') - - // Upload drop a file - cy.get('[data-cy-files-drag-drop-area]').selectFile({ - fileName: 'single-file.txt', - contents: ['hello '.repeat(1024)], - }, { action: 'drag-drop' }) - - cy.wait('@uploadFile') - - // Make sure the upload is finished - cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible') - cy.get('[data-cy-upload-picker] progress').should('not.be.visible') - cy.get('@uploadFile.all').should('have.length', 1) - - getRowForFile('single-file.txt').should('be.visible') - getRowForFile('single-file.txt').find('[data-cy-files-list-row-size]').should('contain', '6 KB') - }) - - it('can drop multiple files', () => { - const dataTransfer = new DataTransfer() - dataTransfer.items.add(new File([], 'first.txt')) - dataTransfer.items.add(new File([], 'second.txt')) - - cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') - - // Make sure the drop notice is not visible - cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible') - - // Trigger the drop notice - cy.get('main.app-content').trigger('dragover', { dataTransfer }) - cy.get('[data-cy-files-drag-drop-area]').should('be.visible') - - // Upload drop a file - cy.get('[data-cy-files-drag-drop-area]').selectFile([ - { - fileName: 'first.txt', - contents: ['Hello'], - }, - { - fileName: 'second.txt', - contents: ['World'], - }, - ], { action: 'drag-drop' }) - - cy.wait('@uploadFile') - - // Make sure the upload is finished - cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible') - cy.get('[data-cy-upload-picker] progress').should('not.be.visible') - cy.get('@uploadFile.all').should('have.length', 2) - - getRowForFile('first.txt').should('be.visible') - getRowForFile('second.txt').should('be.visible') - }) - - it('will ignore legacy Folders', () => { - cy.window().then((win) => { - // Remove the Filesystem API to force the legacy File API - // See how cypress mocks the Filesystem API in https://github.com/cypress-io/cypress/blob/74109094a92df3bef073dda15f17194f31850d7d/packages/driver/src/cy/commands/actions/selectFile.ts#L24-L37 - Object.defineProperty(win.DataTransferItem.prototype, 'getAsEntry', { get: undefined }) - Object.defineProperty(win.DataTransferItem.prototype, 'webkitGetAsEntry', { get: undefined }) - }) - - const dataTransfer = new DataTransfer() - dataTransfer.items.add(new File([], 'first.txt')) - dataTransfer.items.add(new File([], 'second.txt')) - - // Legacy File API (not FileSystem API), will treat Folders as Files - // with empty type and empty content - dataTransfer.items.add(new File([], 'Foo', { type: 'httpd/unix-directory' })) - dataTransfer.items.add(new File([], 'Bar')) - - cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') - - // Make sure the drop notice is not visible - cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible') - - // Trigger the drop notice - cy.get('main.app-content').trigger('dragover', { dataTransfer }) - cy.get('[data-cy-files-drag-drop-area]').should('be.visible') - - // Upload drop a file - cy.get('[data-cy-files-drag-drop-area]').selectFile([ - { - fileName: 'first.txt', - contents: ['Hello'], - }, - { - fileName: 'second.txt', - contents: ['World'], - }, - { - fileName: 'Foo', - contents: {}, - }, - { - fileName: 'Bar', - contents: { mimeType: 'httpd/unix-directory' }, - }, - ], { action: 'drag-drop' }) - - cy.wait('@uploadFile') - - // Make sure the upload is finished - cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible') - cy.get('[data-cy-upload-picker] progress').should('not.be.visible') - cy.get('@uploadFile.all').should('have.length', 2) - - // see the warning - cy.get('.toast-warning').should('exist') - - // close all toasts - cy.get('.toastify') - .findAllByRole('button', { name: 'Close' }) - .click({ multiple: true }) - - getRowForFile('first.txt').should('be.visible') - getRowForFile('second.txt').should('be.visible') - getRowForFile('Foo').should('not.exist') - getRowForFile('Bar').should('not.exist') - }) -}) - -// Regression coverage for https://github.com/nextcloud/server/issues/60139 -// The per-row drop handler in FileEntryMixin used to pass raw FileSystemEntry -// objects to @nextcloud/upload's batchUpload; on some Chromium builds the -// instanceof-based conversion silently failed and the chunk uploader crashed -// with "e.slice is not a function". The fix routes the per-row drop through -// the same dataTransferToFileTree pipeline as the main file-list drop. -// -// Sibling describe (not nested) so the outer suite's `beforeEach` doesn't -// spin up an unused user before each test in this block. -describe('files: Drag and Drop onto a folder row', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - cy.createRandomUser().then((u) => { - user = u - cy.mkdir(user, '/subfolder') - cy.login(user) - }) - cy.visit('/apps/files') - getRowForFile('subfolder').should('be.visible') - }) - - it('can drop a single file onto a subfolder row', () => { - cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') - - getRowForFile('subfolder').selectFile({ - fileName: 'dropped-into-subfolder.txt', - contents: ['hello '.repeat(1024)], - }, { action: 'drag-drop' }) - - cy.wait('@uploadFile').its('request.url') - .should('match', /\/subfolder\/dropped-into-subfolder\.txt$/) - - cy.get('[data-cy-upload-picker] progress').should('not.be.visible') - - navigateToFolder('/subfolder') - getRowForFile('dropped-into-subfolder.txt').should('be.visible') - }) - - it('can drop multiple files onto a subfolder row', () => { - cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') - - getRowForFile('subfolder').selectFile([ - { fileName: 'one.txt', contents: ['A'.repeat(1024)] }, - { fileName: 'two.txt', contents: ['B'.repeat(1024)] }, - ], { action: 'drag-drop' }) - - // Both files must land under the subfolder, not the current dir. - cy.wait(['@uploadFile', '@uploadFile']).then((intercepts) => { - const urls = intercepts.map((i) => i.request.url).sort() - expect(urls).to.have.length(2) - urls.forEach((url) => { - expect(url).to.match(/\/subfolder\/(one|two)\.txt$/) - }) - }) - - cy.get('[data-cy-upload-picker] progress').should('not.be.visible') - - navigateToFolder('/subfolder') - getRowForFile('one.txt').should('be.visible') - getRowForFile('two.txt').should('be.visible') - }) - - it('opens the conflict picker when dropping a colliding name onto a subfolder row', () => { - // Pre-populate the subfolder with a file the drop will collide with. - // cy.uploadContent internally clears session cookies, so re-login - // before revisiting so it matches the uploadContent > login > visit - // pattern used elsewhere in the suite. - cy.uploadContent(user, new Blob(['original']), 'text/plain', '/subfolder/collide.txt') - cy.login(user) - - // Reload so the pre-populated file lands in the store before the drop. - // The drop handler reads filesStore.getNodesByPath first and only - // fetches fresh contents when the cache is empty, so a stale cache - // from the beforeEach visit would let the upload proceed without - // triggering the conflict picker. If this ever flaps on CI, replace - // the visit with cy.reload() + an explicit wait on store settlement. - cy.visit('/apps/files') - getRowForFile('subfolder').should('be.visible') - - cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') - - getRowForFile('subfolder').selectFile({ - fileName: 'collide.txt', - contents: ['replacement '.repeat(1024)], - }, { action: 'drag-drop' }) - - // Wait for the conflict picker to appear, then assert no PUT has - // fired yet — chained so the upload-count check happens *after* the - // dialog is visible, enforcing the "dialog blocks upload" invariant. - cy.findByRole('dialog').should('be.visible').then(() => { - cy.get('@uploadFile.all').should('have.length', 0) - }) - }) -}) diff --git a/cypress/e2e/files/duplicated-node-regression.cy.ts b/cypress/e2e/files/duplicated-node-regression.cy.ts deleted file mode 100644 index 14355a62b9d93..0000000000000 --- a/cypress/e2e/files/duplicated-node-regression.cy.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { createFolder, getRowForFile, triggerActionForFile } from './FilesUtils.ts' - -before(() => { - cy.createRandomUser() - .then((user) => { - cy.mkdir(user, '/only once') - cy.login(user) - cy.visit('/apps/files') - }) -}) - -/** - * Regression test for https://github.com/nextcloud/server/issues/47904 - */ -it('Ensure nodes are not duplicated in the file list', () => { - // See the folder - getRowForFile('only once').should('be.visible') - // Delete the folder - cy.intercept('DELETE', '**/remote.php/dav/**').as('deleteFolder') - triggerActionForFile('only once', 'delete') - cy.wait('@deleteFolder') - getRowForFile('only once').should('not.exist') - // Create the folder again - createFolder('only once') - // See folder exists only once - getRowForFile('only once') - .should('have.length', 1) -}) diff --git a/cypress/e2e/files/favorites.cy.ts b/cypress/e2e/files/favorites.cy.ts deleted file mode 100644 index e992b7e578318..0000000000000 --- a/cypress/e2e/files/favorites.cy.ts +++ /dev/null @@ -1,163 +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, getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts' - -describe('files: Favorites', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.mkdir(user, '/new folder') - cy.login(user) - cy.visit('/apps/files') - }) - }) - - it('Mark file as favorite', () => { - // See file exists - getRowForFile('file.txt') - .should('exist') - - cy.intercept('POST', '**/apps/files/api/v1/files/file.txt').as('addToFavorites') - // Click actions - getActionButtonForFile('file.txt').click({ force: true }) - // See action is called 'Add to favorites' - cy.get('[data-cy-files-list-row-action="favorite"] > button').last() - .should('exist') - .and('contain.text', 'Add to favorites') - .click({ force: true }) - cy.wait('@addToFavorites') - // See favorites star - getRowForFile('file.txt') - .findByRole('img', { name: 'Favorite' }) - .should('exist') - }) - - it('Un-mark file as favorite', () => { - // See file exists - getRowForFile('file.txt') - .should('exist') - - cy.intercept('POST', '**/apps/files/api/v1/files/file.txt').as('addToFavorites') - // toggle favorite - triggerActionForFile('file.txt', 'favorite') - cy.wait('@addToFavorites') - - // See favorites star - getRowForFile('file.txt') - .findByRole('img', { name: 'Favorite' }) - .should('be.visible') - - // Remove favorite - // click action button - getActionButtonForFile('file.txt').click({ force: true }) - // See action is called 'Remove from favorites' - cy.get('[data-cy-files-list-row-action="favorite"] > button').last() - .should('exist') - .and('have.text', 'Remove from favorites') - .click({ force: true }) - cy.wait('@addToFavorites') - // See no favorites star anymore - getRowForFile('file.txt') - .findByRole('img', { name: 'Favorite' }) - .should('not.exist') - }) - - it('See favorite folders in navigation', () => { - cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites') - - // see navigation has no entry - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('not.exist') - - // toggle favorite - triggerActionForFile('new folder', 'favorite') - cy.wait('@addToFavorites') - - // See in navigation - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('exist') - - // toggle favorite - triggerActionForFile('new folder', 'favorite') - cy.wait('@addToFavorites') - - // See no longer in navigation - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('not.exist') - }) - - it('Mark file as favorite using the sidebar', () => { - // See file exists - getRowForFile('new folder') - .should('exist') - // see navigation has no entry - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('not.exist') - - cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites') - // open sidebar - triggerActionForFile('new folder', 'details') - cy.get('[data-cy-sidebar]') - .should('be.visible') - - // open sidebar actions - cy.get('[data-cy-sidebar]') - .findByRole('button', { name: 'Actions' }) - .click() - // trigger menu button - cy.findAllByRole('menu') - .findByRole('menuitem', { name: 'Favorite' }) - .should('be.visible') - .click() - cy.wait('@addToFavorites') - - // close sidebar - closeSidebar() - - // See favorites star - getRowForFile('new folder') - .findByRole('img', { name: 'Favorite' }) - .should('be.visible') - - cy.reload() - getRowForFile('new folder') - .should('be.visible') - - // can unfavorite - triggerActionForFile('new folder', 'details') - cy.get('[data-cy-sidebar]') - .should('be.visible') - - cy.get('[data-cy-sidebar]') - .findByRole('button', { name: 'Actions' }) - .click() - // trigger menu button - cy.findAllByRole('menu') - .findByRole('menuitem', { name: 'Unfavorite' }) - .should('be.visible') - .click() - - cy.wait('@addToFavorites') - closeSidebar() - - getRowForFile('new folder') - .findByRole('img', { name: 'Favorite' }) - .should('not.exist') - }) -}) diff --git a/cypress/e2e/files/files-actions.cy.ts b/cypress/e2e/files/files-actions.cy.ts deleted file mode 100644 index f717213c15a8c..0000000000000 --- a/cypress/e2e/files/files-actions.cy.ts +++ /dev/null @@ -1,70 +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 { getActionButtonForFileId, getActionEntryForFileId, getRowForFile, getSelectionActionButton, getSelectionActionEntry, selectRowForFile } from './FilesUtils.ts' - -const ACTION_DELETE = 'delete' -const ACTION_COPY_MOVE = 'move-copy' -const ACTION_DETAILS = 'details' - -// Those two arrays doesn't represent the full list of actions -// the goal is to test a few, we're not trying to match the full feature set -const expectedDefaultActionsIDs = [ - ACTION_COPY_MOVE, - ACTION_DELETE, - ACTION_DETAILS, -] -const expectedDefaultSelectionActionsIDs = [ - ACTION_COPY_MOVE, - ACTION_DELETE, -] - -describe('Files: Actions', { testIsolation: true }, () => { - let user: User - let fileId: number = 0 - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.uploadContent(user, new Blob([]), 'image/jpeg', '/image.jpg').then((response) => { - fileId = Number.parseInt(response.headers['oc-fileid'] ?? '0') - }) - cy.login(user) - })) - - it('Show some standard actions', () => { - cy.visit('/apps/files') - getRowForFile('image.jpg').should('be.visible') - - expectedDefaultActionsIDs.forEach((actionId) => { - // Open the menu - getActionButtonForFileId(fileId).click({ force: true }) - // Check the action is visible - getActionEntryForFileId(fileId, actionId).should('be.visible') - // Close the menu - cy.get('body').click({ force: true }) - }) - }) - - it('Show some actions for a selection', () => { - cy.visit('/apps/files') - getRowForFile('image.jpg').should('be.visible') - - selectRowForFile('image.jpg') - - cy.get('[data-cy-files-list-selection-actions]').should('be.visible') - getSelectionActionButton().should('be.visible') - - // Open the menu - getSelectionActionButton().click({ force: true }) - - // Check the action is visible - expectedDefaultSelectionActionsIDs.forEach((actionId) => { - getSelectionActionEntry(actionId).should('be.visible') - }) - }) -}) diff --git a/cypress/e2e/files/files-copy-move.cy.ts b/cypress/e2e/files/files-copy-move.cy.ts deleted file mode 100644 index abd0b9598cb6a..0000000000000 --- a/cypress/e2e/files/files-copy-move.cy.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { copyFile, getRowForFile, moveFile, navigateToFolder } from './FilesUtils.ts' - -describe('Files: Move or copy files', { testIsolation: true }, () => { - let currentUser - beforeEach(() => { - cy.createRandomUser().then((user) => { - currentUser = user - cy.login(user) - }) - }) - afterEach(() => { - // nice to have cleanup - cy.deleteUser(currentUser) - }) - - it('Can copy a file to new folder', () => { - // Prepare initial state - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt') - .mkdir(currentUser, '/new-folder') - cy.login(currentUser) - cy.visit('/apps/files') - - copyFile('original.txt', 'new-folder') - - navigateToFolder('new-folder') - - cy.url().should('contain', 'dir=/new-folder') - getRowForFile('original.txt').should('be.visible') - getRowForFile('new-folder').should('not.exist') - }) - - it('Can move a file to new folder', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt') - .mkdir(currentUser, '/new-folder') - cy.login(currentUser) - cy.visit('/apps/files') - - moveFile('original.txt', 'new-folder') - - // wait until visible again - getRowForFile('new-folder').should('be.visible') - - // original should be moved -> not exist anymore - getRowForFile('original.txt').should('not.exist') - navigateToFolder('new-folder') - - cy.url().should('contain', 'dir=/new-folder') - getRowForFile('original.txt').should('be.visible') - getRowForFile('new-folder').should('not.exist') - }) - - /** - * Test for https://github.com/nextcloud/server/issues/41768 - */ - it('Can move a file to folder with similar name', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original') - .mkdir(currentUser, '/original folder') - cy.login(currentUser) - cy.visit('/apps/files') - - moveFile('original', 'original folder') - - // wait until visible again - getRowForFile('original folder').should('be.visible') - - // original should be moved -> not exist anymore - getRowForFile('original').should('not.exist') - navigateToFolder('original folder') - - cy.url().should('contain', 'dir=/original%20folder') - getRowForFile('original').should('be.visible') - getRowForFile('original folder').should('not.exist') - }) - - it('Can move a file to its parent folder', () => { - cy.mkdir(currentUser, '/new-folder') - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/new-folder/original.txt') - cy.login(currentUser) - cy.visit('/apps/files') - - navigateToFolder('new-folder') - cy.url().should('contain', 'dir=/new-folder') - - moveFile('original.txt', '/') - - // wait until visible again - cy.get('main').contains('No files in here').should('be.visible') - - // original should be moved -> not exist anymore - getRowForFile('original.txt').should('not.exist') - - cy.visit('/apps/files') - getRowForFile('new-folder').should('be.visible') - getRowForFile('original.txt').should('be.visible') - }) - - it('Can copy a file to same folder', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt') - cy.login(currentUser) - cy.visit('/apps/files') - - copyFile('original.txt', '.') - - getRowForFile('original.txt').should('be.visible') - getRowForFile('original (1).txt').should('be.visible') - }) - - it('Can copy a file multiple times to same folder', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt') - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original (1).txt') - cy.login(currentUser) - cy.visit('/apps/files') - - copyFile('original.txt', '.') - - getRowForFile('original.txt').should('be.visible') - getRowForFile('original (2).txt').should('be.visible') - }) - - /** - * Test that a copied folder with a dot will be renamed correctly ('foo.bar' -> 'foo.bar (1)') - * Test for: https://github.com/nextcloud/server/issues/43843 - */ - it('Can copy a folder to same folder', () => { - cy.mkdir(currentUser, '/foo.bar') - cy.login(currentUser) - cy.visit('/apps/files') - - copyFile('foo.bar', '.') - - getRowForFile('foo.bar').should('be.visible') - getRowForFile('foo.bar (1)').should('be.visible') - }) - - /** Test for https://github.com/nextcloud/server/issues/43329 */ - context('escaping file and folder names', () => { - it('Can handle files with special characters', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt') - .mkdir(currentUser, '/can\'t say') - cy.login(currentUser) - cy.visit('/apps/files') - - copyFile('original.txt', 'can\'t say') - - navigateToFolder('can\'t say') - - cy.url().should('contain', 'dir=/can%27t%20say') - getRowForFile('original.txt').should('be.visible') - getRowForFile('can\'t say').should('not.exist') - }) - - /** - * If escape is set to false (required for test above) then "foo" would result in "foo" if sanitizing is not disabled - * We should disable it as vue already escapes the text when using v-text - */ - it('does not incorrectly sanitize file names', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt') - .mkdir(currentUser, '/foo') - cy.login(currentUser) - cy.visit('/apps/files') - - copyFile('original.txt', 'foo') - - navigateToFolder('foo') - - cy.url().should('contain', 'dir=/%3Ca%20href%3D%22%23%22%3Efoo') - getRowForFile('original.txt').should('be.visible') - getRowForFile('foo').should('not.exist') - }) - }) -}) diff --git a/cypress/e2e/files/files-delete.cy.ts b/cypress/e2e/files/files-delete.cy.ts deleted file mode 100644 index b1af310d9b6dd..0000000000000 --- a/cypress/e2e/files/files-delete.cy.ts +++ /dev/null @@ -1,70 +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 { getRowForFile, navigateToFolder, selectAllFiles, triggerActionForFile, triggerSelectionAction } from './FilesUtils.ts' - -describe('files: Delete files using file actions', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - }) - }) - - it('can delete file', () => { - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.login(user) - cy.visit('/apps/files') - - // The file must exist and the preview loaded as it locks the file - getRowForFile('file.txt') - .should('be.visible') - .find('.files-list__row-icon-preview--loaded') - .should('exist') - - cy.intercept('DELETE', '**/remote.php/dav/files/**').as('deleteFile') - - triggerActionForFile('file.txt', 'delete') - cy.wait('@deleteFile').its('response.statusCode').should('eq', 204) - }) - - it('can delete multiple files', () => { - cy.mkdir(user, '/root') - for (let i = 0; i < 5; i++) { - cy.uploadContent(user, new Blob([]), 'text/plain', `/root/file${i}.txt`) - } - cy.login(user) - cy.visit('/apps/files') - navigateToFolder('/root') - - // The file must exist and the preview loaded as it locks the file - cy.get('.files-list__row-icon-preview--loaded') - .should('have.length', 5) - - cy.intercept('DELETE', '**/remote.php/dav/files/**').as('deleteFile') - - // select all - selectAllFiles() - triggerSelectionAction('delete') - - // see dialog for confirmation - cy.findByRole('dialog', { name: 'Confirm deletion' }) - .findByRole('button', { name: 'Delete files' }) - .click() - - cy.wait('@deleteFile') - cy.get('@deleteFile.all') - .should('have.length', 5) - - .should((all: any) => { - for (const call of all) { - expect(call.response.statusCode).to.equal(204) - } - }) - }) -}) diff --git a/cypress/e2e/files/files-download.cy.ts b/cypress/e2e/files/files-download.cy.ts deleted file mode 100644 index c811a4a4a8ed4..0000000000000 --- a/cypress/e2e/files/files-download.cy.ts +++ /dev/null @@ -1,333 +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 { zipFileContains } from '../../support/utils/assertions.ts' -import { deleteDownloadsFolderBeforeEach } from '../../support/utils/deleteDownloadsFolder.ts' -import { randomString } from '../../support/utils/randomString.ts' -import { getRowForFile, navigateToFolder, triggerActionForFile, triggerSelectionAction } from './FilesUtils.ts' - -describe('files: Download files using file actions', { testIsolation: true }, () => { - let user: User - - deleteDownloadsFolderBeforeEach() - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - }) - }) - - it('can download file', () => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/file.txt') - cy.login(user) - cy.visit('/apps/files') - - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'download') - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) - - it('can download folder', () => { - cy.mkdir(user, '/subfolder') - cy.uploadContent(user, new Blob(['']), 'text/plain', '/subfolder/file.txt') - - cy.login(user) - cy.visit('/apps/files') - getRowForFile('subfolder') - .should('be.visible') - - 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/file.txt', - ])) - }) - - /** - * Regression test of https://github.com/nextcloud/server/issues/44855 - */ - it('can download file with hash name', () => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/#file.txt') - cy.login(user) - cy.visit('/apps/files') - - triggerActionForFile('#file.txt', 'download') - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/#file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) - - /** - * Regression test of https://github.com/nextcloud/server/issues/44855 - */ - it('can download file from folder with hash name', () => { - cy.mkdir(user, '/#folder') - .uploadContent(user, new Blob(['']), 'text/plain', '/#folder/file.txt') - cy.login(user) - cy.visit('/apps/files') - - navigateToFolder('#folder') - // All are visible by default - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'download') - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) -}) - -describe('files: Download files using default action', { testIsolation: true }, () => { - let user: User - - deleteDownloadsFolderBeforeEach() - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - }) - }) - - it('can download file', () => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/file.txt') - cy.login(user) - cy.visit('/apps/files') - - getRowForFile('file.txt') - .should('be.visible') - .findByRole('button', { name: 'Download' }) - .click() - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) - - /** - * Regression test of https://github.com/nextcloud/server/issues/44855 - */ - it('can download file with hash name', () => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/#file.txt') - cy.login(user) - cy.visit('/apps/files') - - getRowForFile('#file.txt') - .should('be.visible') - .findByRole('button', { name: 'Download' }) - .click() - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/#file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) - - /** - * Regression test of https://github.com/nextcloud/server/issues/44855 - */ - it('can download file from folder with hash name', () => { - cy.mkdir(user, '/#folder') - .uploadContent(user, new Blob(['']), 'text/plain', '/#folder/file.txt') - cy.login(user) - cy.visit('/apps/files') - - navigateToFolder('#folder') - // All are visible by default - getRowForFile('file.txt') - .should('be.visible') - .findByRole('button', { name: 'Download' }) - .click() - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) -}) - -describe('files: Download files using selection', () => { - deleteDownloadsFolderBeforeEach() - - it('can download selected files', () => { - cy.createRandomUser().then((user) => { - cy.mkdir(user, '/subfolder') - cy.uploadContent(user, new Blob(['']), 'text/plain', '/subfolder/file.txt') - cy.login(user) - cy.visit('/apps/files') - }) - - getRowForFile('subfolder') - .should('be.visible') - - getRowForFile('subfolder') - .findByRole('checkbox') - .check({ force: true }) - - // see that two files are selected - cy.get('[data-cy-files-list]').within(() => { - cy.contains('1 selected').should('be.visible') - }) - - // click download - triggerSelectionAction('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/file.txt', - ])) - }) - - it('can download multiple selected files', () => { - cy.createRandomUser().then((user) => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/file.txt') - cy.uploadContent(user, new Blob(['']), 'text/plain', '/other file.txt') - cy.login(user) - cy.visit('/apps/files') - }) - - getRowForFile('file.txt') - .should('be.visible') - .findByRole('checkbox') - .check({ force: true }) - - getRowForFile('other file.txt') - .should('be.visible') - .findByRole('checkbox') - .check({ force: true }) - - cy.get('[data-cy-files-list]').within(() => { - // see that two files are selected - cy.contains('2 selected').should('be.visible') - }) - - // click download - triggerSelectionAction('download') - - // check a file is downloaded - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/download.zip`, null, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 30) - // Check all files are included - .and(zipFileContains([ - 'file.txt', - 'other file.txt', - ])) - }) - - /** - * Regression test of https://help.nextcloud.com/t/unable-to-download-files-on-nextcloud-when-multiple-files-selected/221327/5 - */ - it('can download selected files with special characters', () => { - cy.createRandomUser().then((user) => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/1+1.txt') - cy.uploadContent(user, new Blob(['']), 'text/plain', '/some@other.txt') - cy.login(user) - cy.visit('/apps/files') - }) - - getRowForFile('some@other.txt') - .should('be.visible') - .findByRole('checkbox') - .check({ force: true }) - - getRowForFile('1+1.txt') - .should('be.visible') - .findByRole('checkbox') - .check({ force: true }) - - cy.get('[data-cy-files-list]').within(() => { - // see that two files are selected - cy.contains('2 selected').should('be.visible') - }) - - // click download - triggerSelectionAction('download') - - // check a file is downloaded - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/download.zip`, null, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 30) - // Check all files are included - .and(zipFileContains([ - '1+1.txt', - 'some@other.txt', - ])) - }) - - /** - * Regression test of https://help.nextcloud.com/t/unable-to-download-files-on-nextcloud-when-multiple-files-selected/221327/5 - */ - it('can download selected files with email uid', () => { - const name = `${randomString(5)}@${randomString(3)}` - const user: User = { userId: name, password: name, language: 'en' } - - cy.createUser(user).then(() => { - cy.uploadContent(user, new Blob(['']), 'text/plain', '/file.txt') - cy.uploadContent(user, new Blob(['']), 'text/plain', '/other file.txt') - cy.login(user) - cy.visit('/apps/files') - }) - - getRowForFile('file.txt') - .should('be.visible') - .findByRole('checkbox') - .check({ force: true }) - - getRowForFile('other file.txt') - .should('be.visible') - .findByRole('checkbox') - .check({ force: true }) - - cy.get('[data-cy-files-list]').within(() => { - // see that two files are selected - cy.contains('2 selected').should('be.visible') - }) - - // click download - triggerSelectionAction('download') - - // check a file is downloaded - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/download.zip`, null, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 30) - // Check all files are included - .and(zipFileContains([ - 'file.txt', - 'other file.txt', - ])) - }) -}) diff --git a/cypress/e2e/files/files-filtering.cy.ts b/cypress/e2e/files/files-filtering.cy.ts deleted file mode 100644 index 0b6397a2b73a7..0000000000000 --- a/cypress/e2e/files/files-filtering.cy.ts +++ /dev/null @@ -1,280 +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 { FilesFilterPage } from '../../pages/FilesFilters.ts' -import { FilesNavigationPage } from '../../pages/FilesNavigation.ts' -import { getRowForFile, navigateToFolder } from './FilesUtils.ts' - -describe('files: Filter in files list', { testIsolation: true }, () => { - const appNavigation = new FilesNavigationPage() - const filesFilters = new FilesFilterPage() - let user: User - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.mkdir(user, '/folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.uploadContent(user, new Blob([]), 'text/csv', '/spreadsheet.csv') - cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/text.txt') - cy.login(user) - cy.visit('/apps/files') - })) - - it('filters current view by name', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - // Set up a search query - appNavigation.searchInput() - .type('folder') - - // See that only the folder is visible - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('not.exist') - getRowForFile('spreadsheet.csv').should('not.exist') - }) - - it('can reset name filter', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - // Set up a search query - appNavigation.searchInput() - .type('folder') - - // See that only the folder is visible - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('not.exist') - - // reset the filter - appNavigation.searchInput().should('have.value', 'folder') - appNavigation.searchClearButton().should('exist').click() - appNavigation.searchInput().should('have.value', '') - - // All are visible again - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - }) - - it('filters current view by type', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - getRowForFile('spreadsheet.csv').should('be.visible') - - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Spreadsheets' }) - .should('be.visible') - .and('have.attr', 'aria-pressed', 'false') - .as('spreadsheetsFilterButton') - .click() - cy.get('@spreadsheetsFilterButton') - .should('have.attr', 'aria-pressed', 'true') - - filesFilters.closeFilterMenu() - - // See that only the spreadsheet is visible - getRowForFile('spreadsheet.csv').should('be.visible') - getRowForFile('file.txt').should('not.exist') - getRowForFile('folder').should('not.exist') - }) - - it('can reset filter by type', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Spreadsheets' }) - .should('be.visible') - .as('spreadsheetsFilterButton') - .click() - cy.get('@spreadsheetsFilterButton') - .should('have.attr', 'aria-pressed', 'true') - - filesFilters.closeFilterMenu() - - // See folder is not visible - getRowForFile('folder').should('not.exist') - - // clear filter - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Spreadsheets' }) - .should('be.visible') - .and('have.attr', 'aria-pressed', 'true') - .as('spreadsheetsFilterButton') - .click() - cy.get('@spreadsheetsFilterButton') - .should('have.attr', 'aria-pressed', 'false') - - filesFilters.closeFilterMenu() - - // See folder is visible again - getRowForFile('folder').should('be.visible') - }) - - it('can reset filter by clicking chip button', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Spreadsheets' }) - .should('be.visible') - .as('spreadsheetsFilterButton') - .click() - cy.get('@spreadsheetsFilterButton') - .should('have.attr', 'aria-pressed', 'true') - - filesFilters.closeFilterMenu() - - // See folder is not visible - getRowForFile('folder').should('not.exist') - - // clear filter - filesFilters.removeFilter('Spreadsheets') - - // See folder is visible again - getRowForFile('folder').should('be.visible') - }) - - it('keeps type filter when changing the directory', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Folders' }) - .should('be.visible') - .as('spreadsheetsFilterButton') - .click() - cy.get('@spreadsheetsFilterButton') - .should('have.attr', 'aria-pressed', 'true') - - filesFilters.closeFilterMenu() - - // See that only the folder is visible - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('not.exist') - - // see filter is active - filesFilters.activeFilters().contains(/Folder/).should('be.visible') - - // go to that folder - navigateToFolder('folder') - - // see filter is still active - filesFilters.activeFilters().contains(/Folder/).should('be.visible') - - // see that the folder is filtered - getRowForFile('text.txt').should('not.exist') - }) - - /** Regression test of https://github.com/nextcloud/server/issues/47251 */ - it('keeps filter state when changing the directory', () => { - // files are visible - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - // enable type filter for folders - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Folders' }) - .should('be.visible') - .as('spreadsheetsFilterButton') - .click() - cy.get('@spreadsheetsFilterButton') - .should('have.attr', 'aria-pressed', 'true') - - filesFilters.closeFilterMenu() - - // See the chips are active - filesFilters.activeFilters() - .should('have.length', 1) - .contains(/Folder/).should('be.visible') - - // See that folder is visible but file not - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('not.exist') - - // Change the directory - navigateToFolder('folder') - getRowForFile('folder').should('not.exist') - - // See that the chip is still active - filesFilters.activeFilters() - .should('have.length', 1) - .contains(/Folder/).should('be.visible') - // And also the button should be active - filesFilters.triggerFilter('Type') - - cy.findByRole('button', { name: 'Folders' }) - .should('be.visible') - .should('have.attr', 'aria-pressed', 'true') - - filesFilters.closeFilterMenu() - }) - - /** Regression test of https://github.com/nextcloud/server/issues/53038 */ - it('resets name filter when changing the directory', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - // Set up a search query - appNavigation.searchInput() - .type('folder') - - // See that only the folder is visible - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('not.exist') - - // go to that folder - navigateToFolder('folder') - - // see the search is cleared - appNavigation.searchInput() - .should('have.value', '') - - // see that the folder content is showed - getRowForFile('text.txt').should('be.visible') - }) - - it('resets filter when changing the view', () => { - // All are visible by default - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - // Set up a search query - appNavigation.searchInput() - .type('folder') - - // See that only the folder is visible - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('not.exist') - - // go to other view - appNavigation.views() - .findByRole('link', { name: /personal files/i }) - .click() - // wait for view changed - cy.url().should('match', /apps\/files\/personal/) - - // see that the folder is not filtered - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').should('be.visible') - - // see the filter bar is gone - appNavigation.searchInput().should('have.value', '') - }) -}) diff --git a/cypress/e2e/files/files-navigation.cy.ts b/cypress/e2e/files/files-navigation.cy.ts deleted file mode 100644 index 9fd74097debb4..0000000000000 --- a/cypress/e2e/files/files-navigation.cy.ts +++ /dev/null @@ -1,56 +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 { getRowForFile, navigateToFolder } from './FilesUtils.ts' - -describe('files: Navigate through folders and observe behavior', () => { - let user: User - - before(() => { - cy.createRandomUser().then(($user) => { - user = $user - cy.mkdir(user, '/foo') - cy.mkdir(user, '/foo/bar') - cy.mkdir(user, '/foo/bar/baz') - }) - }) - - it('Shows root folder and we can navigate to the last folder', () => { - cy.login(user) - cy.visit('/apps/files/') - - getRowForFile('foo').should('be.visible') - navigateToFolder('/foo/bar/baz') - - // Last folder is empty - cy.get('[data-cy-files-list-row-fileid]').should('not.exist') - }) - - it('Highlight the previous folder when navigating back', () => { - cy.go('back') - getRowForFile('baz').should('be.visible') - .invoke('attr', 'class').should('contain', 'active') - - cy.go('back') - getRowForFile('bar').should('be.visible') - .invoke('attr', 'class').should('contain', 'active') - - cy.go('back') - getRowForFile('foo').should('be.visible') - .invoke('attr', 'class').should('contain', 'active') - }) - - it('Can navigate forward again', () => { - cy.go('forward') - getRowForFile('bar').should('be.visible') - .invoke('attr', 'class').should('contain', 'active') - - cy.go('forward') - getRowForFile('baz').should('be.visible') - .invoke('attr', 'class').should('contain', 'active') - }) -}) diff --git a/cypress/e2e/files/files-renaming.cy.ts b/cypress/e2e/files/files-renaming.cy.ts deleted file mode 100644 index 200bf6779dc7e..0000000000000 --- a/cypress/e2e/files/files-renaming.cy.ts +++ /dev/null @@ -1,288 +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 { calculateViewportHeight, createFolder, getRowForFile, haveValidity, renameFile, triggerActionForFile } from './FilesUtils.ts' - -describe('files: Rename nodes', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - - // remove welcome file - cy.rm(user, '/welcome.txt') - // create a file called "file.txt" - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - - // login and visit files app - cy.login(user) - }) - cy.visit('/apps/files') - }) - - it('can rename a file', () => { - // All are visible by default - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - - getRowForFile('file.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') - }) - - /** - * If this test gets flaky than we have a problem: - * It means that the selection is not reliable set to the basename - */ - it('only selects basename of file', () => { - // All are visible by default - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .should((el) => { - const input = el.get(0) as HTMLInputElement - expect(input.selectionStart).to.equal(0) - expect(input.selectionEnd).to.equal('file'.length) - }) - }) - - it('show validation error on file rename', () => { - // All are visible by default - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{selectAll}.htaccess') - // See validity - .should(haveValidity(/reserved name/i)) - }) - - it('shows accessible loading information', () => { - const { resolve, promise } = Promise.withResolvers() - - getRowForFile('file.txt').should('be.visible') - - // intercept the rename (MOVE) - // the callback will wait until the promise resolve (so we have time to check the loading state) - cy.intercept( - 'MOVE', - /\/remote.php\/dav\/files\//, - (request) => { - // we need to wait in the onResponse handler as the intercept handler times out otherwise - request.on('response', async () => { - await promise - }) - }, - ).as('moveFile') - - // Start the renaming - triggerActionForFile('file.txt', 'rename') - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{selectAll}new-name.txt{enter}') - - // Loading state is visible - getRowForFile('new-name.txt') - .findByRole('img', { name: 'File is loading' }) - .should('be.visible') - // checkbox is not visible - getRowForFile('new-name.txt') - .findByRole('checkbox', { name: /^Toggle selection/ }) - .should('not.exist') - - cy.log('Resolve promise to preoceed with MOVE request') - .then(() => resolve()) - - // Ensure the request is done (file renamed) - cy.wait('@moveFile') - - // checkbox visible again - getRowForFile('new-name.txt') - .findByRole('checkbox', { name: /^Toggle selection/ }) - .should('exist') - // see the loading state is gone - getRowForFile('new-name.txt') - .findByRole('img', { name: 'File is loading' }) - .should('not.exist') - }) - - it('cancel renaming on esc press', () => { - // All are visible by default - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{selectAll}other.txt') - .should(haveValidity('')) - .type('{esc}') - - // See it is not renamed - getRowForFile('other.txt').should('not.exist') - getRowForFile('file.txt') - .should('be.visible') - .find('input[type="text"]') - .should('not.exist') - }) - - it('cancel on enter if no new name is entered', () => { - // All are visible by default - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{enter}') - - // See it is not renamed - getRowForFile('file.txt') - .should('be.visible') - .find('input[type="text"]') - .should('not.exist') - }) - - /** - * This is a regression test of: https://github.com/nextcloud/server/issues/47438 - * The issue was that the renaming state was not reset when the new name moved the file out of the view of the current files list - * due to virtual scrolling the renaming state was not changed then by the UI events (as the component was taken out of DOM before any event handling). - */ - it('correctly resets renaming state', () => { - // Create 19 additional files - for (let i = 1; i <= 19; i++) { - cy.uploadContent(user, new Blob([]), 'text/plain', `/file${i}.txt`) - } - - // Calculate and setup a viewport where only the first 4 files are visible, causing 6 rows to be rendered - cy.viewport(768, 500) - cy.login(user) - calculateViewportHeight(4) - .then((height) => cy.viewport(768, height)) - - cy.visit('/apps/files') - - getRowForFile('file.txt') - .should('be.visible') - // Z so it is shown last - renameFile('file.txt', 'zzz.txt') - // not visible any longer - getRowForFile('zzz.txt') - .should('not.exist') - // scroll file list to bottom - cy.get('[data-cy-files-list]') - .scrollTo('bottom') - cy.screenshot() - // The file is no longer in rename state - getRowForFile('zzz.txt') - .should('be.visible') - .findByRole('textbox', { name: 'Filename' }) - .should('not.exist') - }) - - it('shows warning on extension change - select new extension', () => { - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{selectAll}file.md') - .type('{enter}') - - // See warning dialog - cy.findByRole('dialog', { name: 'Change file extension' }) - .should('be.visible') - .findByRole('button', { name: 'Use .md' }) - .click() - - // See it is renamed - getRowForFile('file.md').should('be.visible') - }) - - it('shows warning on extension change - select old extension', () => { - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{selectAll}document.md') - .type('{enter}') - - // See warning dialog - cy.findByRole('dialog', { name: 'Change file extension' }) - .should('be.visible') - .findByRole('button', { name: 'Keep .txt' }) - .click() - - // See it is renamed - getRowForFile('document.txt').should('be.visible') - }) - - it('shows warning on extension removal', () => { - getRowForFile('file.txt').should('be.visible') - - triggerActionForFile('file.txt', 'rename') - getRowForFile('file.txt') - .findByRole('textbox', { name: 'Filename' }) - .should('be.visible') - .type('{selectAll}file') - .type('{enter}') - - cy.findByRole('dialog', { name: 'Change file extension' }) - .should('be.visible') - .findByRole('button', { name: 'Keep .txt' }) - .should('be.visible') - cy.findByRole('dialog', { name: 'Change file extension' }) - .findByRole('button', { name: 'Remove extension' }) - .should('be.visible') - .click() - - // See it is renamed - getRowForFile('file').should('be.visible') - getRowForFile('file.txt').should('not.exist') - }) - - it('does not show warning on folder renaming with a dot', () => { - createFolder('folder.2024') - - getRowForFile('folder.2024').should('be.visible') - - triggerActionForFile('folder.2024', 'rename') - getRowForFile('folder.2024') - .findByRole('textbox', { name: 'Folder name' }) - .should('be.visible') - .type('{selectAll}folder.2025') - .should(haveValidity('')) - .type('{enter}') - - // See warning dialog - cy.get('[role=dialog]').should('not.exist') - - // See it is not renamed - getRowForFile('folder.2025').should('be.visible') - }) -}) diff --git a/cypress/e2e/files/files-selection.cy.ts b/cypress/e2e/files/files-selection.cy.ts deleted file mode 100644 index 7600d2c78f8f7..0000000000000 --- a/cypress/e2e/files/files-selection.cy.ts +++ /dev/null @@ -1,77 +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 { deselectAllFiles, selectAllFiles, selectRowForFile } from './FilesUtils.ts' - -const files = { - 'image.jpg': 'image/jpeg', - 'document.pdf': 'application/pdf', - 'archive.zip': 'application/zip', - 'audio.mp3': 'audio/mpeg', - 'video.mp4': 'video/mp4', - 'readme.md': 'text/markdown', - 'welcome.txt': 'text/plain', -} -const filesCount = Object.keys(files).length - -describe('files: Select all files', { testIsolation: true }, () => { - let user: User - - before(() => { - cy.createRandomUser().then(($user) => { - user = $user - Object.keys(files).forEach((file) => { - cy.uploadContent(user, new Blob(), files[file], '/' + file) - }) - }) - }) - - beforeEach(() => { - cy.login(user) - cy.visit('/apps/files') - }) - - it('Can select and unselect all files', () => { - cy.get('[data-cy-files-list-row-fileid]').should('have.length', filesCount) - cy.get('[data-cy-files-list-row-checkbox]').should('have.length', filesCount) - - selectAllFiles() - - cy.get('.files-list__selected').should('contain.text', '7 selected') - cy.get('[data-cy-files-list-row-checkbox]').findByRole('checkbox').should('be.checked') - - deselectAllFiles() - - cy.get('.files-list__selected').should('not.exist') - cy.get('[data-cy-files-list-row-checkbox]').findByRole('checkbox').should('not.be.checked') - }) - - it('Can select some files randomly', () => { - const randomFiles = Object.keys(files).reduce((acc, file) => { - if (Math.random() > 0.1) { - acc.push(file) - } - return acc - }, [] as string[]) - - randomFiles.forEach((name) => selectRowForFile(name)) - - cy.get('.files-list__selected').should('contain.text', `${randomFiles.length} selected`) - cy.get('[data-cy-files-list-row-checkbox] input[type="checkbox"]:checked').should('have.length', randomFiles.length) - }) - - it('Can select range of files with shift key', () => { - cy.get('[data-cy-files-list-row-checkbox]').should('have.length', filesCount) - selectRowForFile('audio.mp3') - cy.window().trigger('keydown', { key: 'ShiftLeft', shiftKey: true }) - selectRowForFile('readme.md') - cy.window().trigger('keyup', { key: 'ShiftLeft', shiftKey: true }) - - cy.get('.files-list__selected').should('contain.text', '4 selected') - cy.get('[data-cy-files-list-row-checkbox] input[type="checkbox"]:checked').should('have.length', 4) - }) -}) diff --git a/cypress/e2e/files/files-settings.cy.ts b/cypress/e2e/files/files-settings.cy.ts deleted file mode 100644 index 299609e9d691b..0000000000000 --- a/cypress/e2e/files/files-settings.cy.ts +++ /dev/null @@ -1,159 +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 './FilesUtils.ts' - -describe('files: Set default view', { testIsolation: true }, () => { - beforeEach(() => { - cy.createRandomUser().then(($user) => { - cy.login($user) - }) - }) - - it('Defaults to the "files" view', () => { - cy.visit('/apps/files') - - // See URL and current view - cy.url().should('match', /\/apps\/files\/files/) - cy.findByRole('navigation', { name: 'Current directory path' }) - .findAllByRole('button') - .first() - .should('have.text', 'All files') - - // See the option is also selected - // Open the files settings - cy.findByRole('link', { name: 'Files settings' }).click({ force: true }) - // Toggle the setting - cy.findByRole('dialog', { name: 'Files settings' }) - .should('be.visible') - .within(() => { - cy.findByRole('group', { name: 'Default view' }) - .findByRole('radio', { name: 'All files' }) - .should('be.checked') - }) - }) - - it('Can set it to personal files', () => { - cy.visit('/apps/files') - - // Open the files settings - cy.findByRole('link', { name: 'Files settings' }).click({ force: true }) - // Toggle the setting - cy.findByRole('dialog', { name: 'Files settings' }) - .should('be.visible') - .within(() => { - cy.findByRole('group', { name: 'Default view' }) - .findByRole('radio', { name: 'Personal files' }) - .check({ force: true }) - }) - - cy.visit('/apps/files') - cy.url().should('match', /\/apps\/files\/personal/) - cy.findByRole('navigation', { name: 'Current directory path' }) - .findAllByRole('button') - .first() - .should('have.text', 'Personal files') - }) -}) - -describe('files: Hide or show hidden files', { testIsolation: true }, () => { - let user: User - - const setupFiles = () => cy.createRandomUser().then(($user) => { - user = $user - - cy.uploadContent(user, new Blob([]), 'text/plain', '/.file') - cy.uploadContent(user, new Blob([]), 'text/plain', '/visible-file') - cy.mkdir(user, '/.folder') - cy.login(user) - }) - - context('view: All files', { testIsolation: false }, () => { - before(setupFiles) - - it('hides dot-files by default', () => { - cy.visit('/apps/files') - - getRowForFile('visible-file').should('be.visible') - getRowForFile('.file').should('not.exist') - getRowForFile('.folder').should('not.exist') - }) - - it('can show hidden files', () => { - showHiddenFiles() - // Now the files should be visible - getRowForFile('.file').should('be.visible') - getRowForFile('.folder').should('be.visible') - }) - }) - - context('view: Personal files', { testIsolation: false }, () => { - before(setupFiles) - - it('hides dot-files by default', () => { - cy.visit('/apps/files/personal') - - getRowForFile('visible-file').should('be.visible') - getRowForFile('.file').should('not.exist') - getRowForFile('.folder').should('not.exist') - }) - - it('can show hidden files', () => { - showHiddenFiles() - // Now the files should be visible - getRowForFile('.file').should('be.visible') - getRowForFile('.folder').should('be.visible') - }) - }) - - context('view: Recent files', { testIsolation: false }, () => { - before(() => { - setupFiles().then(() => { - // also add hidden file in hidden folder - cy.uploadContent(user, new Blob([]), 'text/plain', '/.folder/other-file') - cy.login(user) - }) - }) - - it('hides dot-files by default', () => { - cy.visit('/apps/files/recent') - - getRowForFile('visible-file').should('be.visible') - getRowForFile('.file').should('not.exist') - getRowForFile('.folder').should('not.exist') - getRowForFile('other-file').should('not.exist') - }) - - it('can show hidden files', () => { - showHiddenFiles() - - getRowForFile('visible-file').should('be.visible') - // Now the files should be visible - getRowForFile('.file').should('be.visible') - getRowForFile('.folder').should('be.visible') - getRowForFile('other-file').should('be.visible') - }) - }) -}) - -/** - * Helper to toggle the hidden files settings - */ -function showHiddenFiles() { - // Open the files settings - cy.get('[data-cy-files-navigation-settings-button] a').click({ force: true }) - // Toggle the hidden files setting - cy.findByRole('switch', { name: /show hidden files/i }) - .as('hiddenFiles') - .scrollIntoView() - cy.get('@hiddenFiles') - .should('not.be.checked') - .check({ force: true }) - - // Close the dialog - cy.get('[data-cy-files-navigation-settings] button[aria-label="Close"]').click() -} diff --git a/cypress/e2e/files/files-sidebar.cy.ts b/cypress/e2e/files/files-sidebar.cy.ts deleted file mode 100644 index 69be3473a808d..0000000000000 --- a/cypress/e2e/files/files-sidebar.cy.ts +++ /dev/null @@ -1,137 +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 { assertNotExistOrNotVisible } from '../settings/usersUtils.ts' -import { getRowForFile, navigateToFolder, triggerActionForFile } from './FilesUtils.ts' - -describe('Files: Sidebar', { testIsolation: true }, () => { - let user: User - let fileId: number = 0 - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.mkdir(user, '/folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/file').then((response) => { - fileId = Number.parseInt(response.headers['oc-fileid'] ?? '0') - }) - cy.login(user) - })) - - it('opens the sidebar', () => { - cy.visit('/apps/files') - getRowForFile('file').should('be.visible') - - triggerActionForFile('file', 'details') - - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('heading', { name: 'file' }) - .should('be.visible') - }) - - it('changes the current fileid', () => { - cy.visit('/apps/files') - getRowForFile('file').should('be.visible') - - triggerActionForFile('file', 'details') - - cy.get('[data-cy-sidebar]').should('be.visible') - cy.url().should('contain', `apps/files/files/${fileId}`) - }) - - it('changes the sidebar content on other file', () => { - cy.visit('/apps/files') - getRowForFile('file').should('be.visible') - - triggerActionForFile('file', 'details') - - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('heading', { name: 'file' }) - .should('be.visible') - - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(600) // wait for a bit to avoid flakiness - - triggerActionForFile('folder', 'details') - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('heading', { name: 'folder' }) - .should('be.visible') - }) - - it('closes the sidebar on navigation', () => { - cy.visit('/apps/files') - - getRowForFile('file').should('be.visible') - getRowForFile('folder').should('be.visible') - - // open the sidebar - triggerActionForFile('file', 'details') - // validate it is open - cy.get('[data-cy-sidebar]') - .should('be.visible') - - // if we navigate to the folder - navigateToFolder('folder') - // the sidebar should not be visible anymore - cy.get('[data-cy-sidebar]') - .should(assertNotExistOrNotVisible) - }) - - it('closes the sidebar on delete', () => { - cy.intercept('DELETE', `**/remote.php/dav/files/${user.userId}/file`).as('deleteFile') - // visit the files app - cy.visit('/apps/files') - getRowForFile('file').should('be.visible') - // open the sidebar - triggerActionForFile('file', 'details') - // validate it is open - cy.get('[data-cy-sidebar]') - .should('be.visible') - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(600) // wait for a bit to avoid flakiness - - // delete the file - triggerActionForFile('file', 'delete') - cy.wait('@deleteFile', { timeout: 10000 }) - // see the sidebar is closed - cy.get('[data-cy-sidebar]') - .should(assertNotExistOrNotVisible) - }) - - it('changes the fileid on delete', () => { - cy.intercept('DELETE', `**/remote.php/dav/files/${user.userId}/folder/other`).as('deleteFile') - - cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/other').then((response) => { - const otherFileId = Number.parseInt(response.headers['oc-fileid'] ?? '0') - cy.login(user) - cy.visit('/apps/files') - - getRowForFile('folder').should('be.visible') - navigateToFolder('folder') - getRowForFile('other').should('be.visible') - - // open the sidebar - triggerActionForFile('other', 'details') - // validate it is open - cy.get('[data-cy-sidebar]').should('be.visible') - cy.url().should('contain', `apps/files/files/${otherFileId}`) - - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(600) // wait for a bit to avoid flakiness - - triggerActionForFile('other', 'delete') - cy.wait('@deleteFile') - - cy.get('[data-cy-sidebar]').should('not.be.visible') - // Ensure the URL is changed - cy.url().should('not.contain', `apps/files/files/${otherFileId}`) - }) - }) -}) diff --git a/cypress/e2e/files/files-sorting.cy.ts b/cypress/e2e/files/files-sorting.cy.ts deleted file mode 100644 index a7e699f055083..0000000000000 --- a/cypress/e2e/files/files-sorting.cy.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -describe('Files: Sorting the file list', { testIsolation: true }, () => { - let currentUser - beforeEach(() => { - cy.createRandomUser().then((user) => { - currentUser = user - cy.login(user) - }) - }) - - it('Files are sorted by name ascending by default', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1 first.txt') - .uploadContent(currentUser, new Blob(), 'text/plain', '/z last.txt') - .uploadContent(currentUser, new Blob(), 'text/plain', '/A.txt') - .uploadContent(currentUser, new Blob(), 'text/plain', '/Ä.txt') - .mkdir(currentUser, '/m') - .mkdir(currentUser, '/4') - cy.login(currentUser) - cy.visit('/apps/files') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('4') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('m') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 first.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('A.txt') - break - case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('Ä.txt') - break - case 5: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 6: expect($row.attr('data-cy-files-list-row-name')).to.eq('z last.txt') - break - } - }) - }) - - /** - * Regression test of https://github.com/nextcloud/server/issues/45829 - */ - it('Filesnames with numbers are sorted by name ascending by default', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/name.txt') - .uploadContent(currentUser, new Blob(), 'text/plain', '/name_03.txt') - .uploadContent(currentUser, new Blob(), 'text/plain', '/name_02.txt') - .uploadContent(currentUser, new Blob(), 'text/plain', '/name_01.txt') - cy.login(currentUser) - cy.visit('/apps/files') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('name.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('name_01.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('name_02.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('name_03.txt') - break - } - }) - }) - - it('Can sort by size', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1 tiny.txt') - .uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z big.txt') - .uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a medium.txt') - .mkdir(currentUser, '/folder') - cy.login(currentUser) - cy.visit('/apps/files') - - // click sort button - cy.get('th').contains('button', 'Size').click() - // sorting is set - cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'ascending') - // Files are sorted - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('folder') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 tiny.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('a medium.txt') - break - case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('z big.txt') - break - } - }) - - // click sort button - cy.get('th').contains('button', 'Size').click() - // sorting is set - cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'descending') - // Files are sorted - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('folder') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z big.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('a medium.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 tiny.txt') - break - } - }) - }) - - it('Can sort by mtime', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1.txt', Date.now() / 1000 - 86400 - 1000) - .uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z.txt', Date.now() / 1000 - 86400) - .uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a.txt', Date.now() / 1000 - 86400 - 500) - cy.login(currentUser) - cy.visit('/apps/files') - - // click sort button - cy.get('th').contains('button', 'Modified').click() - // sorting is set - cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'ascending') - // Files are sorted - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') // uploaded right now - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') // fake time of yesterday - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') // fake time of yesterday and few minutes - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') // fake time of yesterday and ~15 minutes ago - break - } - }) - - // reverse order - cy.get('th').contains('button', 'Modified').click() - cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'descending') - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') // uploaded right now - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') // fake time of yesterday - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') // fake time of yesterday and few minutes - break - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') // fake time of yesterday and ~15 minutes ago - break - } - }) - }) - - it('Favorites are sorted first', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1.txt', Date.now() / 1000 - 86400 - 1000) - .uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z.txt', Date.now() / 1000 - 86400) - .uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a.txt', Date.now() / 1000 - 86400 - 500) - .setFileAsFavorite(currentUser, '/a.txt') - cy.login(currentUser) - cy.visit('/apps/files') - - cy.log('By name - ascending') - cy.contains('th', 'Name').should('have.attr', 'aria-sort', 'ascending') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') - break - } - }) - - cy.log('By name - descending') - cy.get('th').contains('button', 'Name').click() - cy.contains('th', 'Name').should('have.attr', 'aria-sort', 'descending') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') - break - } - }) - - cy.log('By size - ascending') - cy.get('th').contains('button', 'Size').click() - cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'ascending') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') - break - } - }) - - cy.log('By size - descending') - cy.get('th').contains('button', 'Size').click() - cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'descending') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') - break - } - }) - - cy.log('By mtime - ascending') - cy.get('th').contains('button', 'Modified').click() - cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'ascending') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') - break - } - }) - - cy.log('By mtime - descending') - cy.get('th').contains('button', 'Modified').click() - cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'descending') - - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - } - }) - }) - - it('Sorting works after switching view twice', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1 tiny.txt') - .uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z big.txt') - .uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a medium.txt') - .mkdir(currentUser, '/folder') - cy.login(currentUser) - cy.visit('/apps/files') - - // click sort button twice - cy.get('th').contains('button', 'Size').click() - cy.get('th').contains('button', 'Size').click() - - // switch to personal and click sort button twice again - cy.get('[data-cy-files-navigation-item="personal"]').click() - cy.get('th').contains('button', 'Size').click() - cy.get('th').contains('button', 'Size').click() - - // switch back to files view and do actual assertions - cy.get('[data-cy-files-navigation-item="files"]').click() - - // click sort button - cy.get('th').contains('button', 'Size').click() - // sorting is set - cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'ascending') - // Files are sorted - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('folder') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 tiny.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('a medium.txt') - break - case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('z big.txt') - break - } - }) - - // click sort button - cy.get('th').contains('button', 'Size').click() - // sorting is set - cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'descending') - // Files are sorted - cy.get('[data-cy-files-list-row]').each(($row, index) => { - switch (index) { - case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('folder') - break - case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z big.txt') - break - case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('a medium.txt') - break - case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') - break - case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 tiny.txt') - break - } - }) - }) -}) diff --git a/cypress/e2e/files/files-xml-regression.cy.ts b/cypress/e2e/files/files-xml-regression.cy.ts deleted file mode 100644 index a961b78e2f493..0000000000000 --- a/cypress/e2e/files/files-xml-regression.cy.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { getRowForFile, triggerActionForFile } from './FilesUtils.ts' - -/** - * This is a regression test for https://github.com/nextcloud/server/issues/43331 - * Where files with XML entities in their names were wrongly displayed and could no longer be renamed / deleted etc. - */ -describe('Files: Can handle XML entities in file names', { testIsolation: false }, () => { - before(() => { - cy.createRandomUser().then((user) => { - cy.uploadContent(user, new Blob(), 'text/plain', '/and.txt') - cy.login(user) - cy.visit('/apps/files/') - }) - }) - - it('Can reanme to a file name containing XML entities', () => { - cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('renameFile') - triggerActionForFile('and.txt', 'rename') - getRowForFile('and.txt') - .find('form[aria-label="Rename file"] input') - .type('{selectAll}&.txt{enter}') - - cy.wait('@renameFile') - getRowForFile('&.txt').should('be.visible') - }) - - it('After a reload the filename is preserved', () => { - cy.reload() - getRowForFile('&.txt').should('be.visible') - getRowForFile('&.txt').should('not.exist') - }) - - it('Can delete the file', () => { - cy.intercept('DELETE', /\/remote.php\/dav\/files\//).as('deleteFile') - triggerActionForFile('&.txt', 'delete') - cy.wait('@deleteFile') - - cy.contains('.toast-success', /Delete .* done/) - .should('be.visible') - getRowForFile('&.txt').should('not.exist') - - cy.reload() - getRowForFile('&.txt').should('not.exist') - getRowForFile('&.txt').should('not.exist') - }) -}) diff --git a/cypress/e2e/files/files.cy.ts b/cypress/e2e/files/files.cy.ts deleted file mode 100644 index 745c330c54319..0000000000000 --- a/cypress/e2e/files/files.cy.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { User } from '@nextcloud/e2e-test-server/cypress' - -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -describe('Files', { testIsolation: true }, () => { - let currentUser: User - - beforeEach(() => { - cy.createRandomUser().then((user) => { - currentUser = user - }) - }) - - it('Login with a user and open the files app', () => { - cy.login(currentUser) - cy.visit('/apps/files') - cy.get('[data-cy-files-list] [data-cy-files-list-row-name="welcome.txt"]').should('be.visible') - }) - - it('Opens a valid file shows it as active', () => { - cy.uploadContent(currentUser, new Blob(), 'text/plain', '/original.txt').then((response) => { - const fileId = Number.parseInt(response.headers['oc-fileid'] ?? '0') - - cy.login(currentUser) - cy.visit('/apps/files/files/' + fileId) - - cy.get(`[data-cy-files-list-row-fileid=${fileId}]`) - .should('be.visible') - cy.get(`[data-cy-files-list-row-fileid=${fileId}]`) - .invoke('attr', 'data-cy-files-list-row-name').should('eq', 'original.txt') - cy.get(`[data-cy-files-list-row-fileid=${fileId}]`) - .invoke('attr', 'class').should('contain', 'active') - cy.contains('The file could not be found').should('not.exist') - }) - }) - - it('Opens a valid folder shows its content', () => { - cy.mkdir(currentUser, '/folder').then(() => { - cy.login(currentUser) - cy.visit('/apps/files/files?dir=/folder') - - cy.get('[data-cy-files-content-breadcrumbs]').contains('folder').should('be.visible') - cy.contains('The file could not be found').should('not.exist') - }) - }) - - it('Opens an unknown file show an error', () => { - cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind') - cy.login(currentUser) - cy.visit('/apps/files/files/123456') - - cy.wait('@propfind') - // The toast should be visible - cy.contains('The file could not be found', { timeout: 5000 }).should('be.visible') - }) -}) diff --git a/cypress/e2e/files/hotkeys.cy.ts b/cypress/e2e/files/hotkeys.cy.ts deleted file mode 100644 index c68c006db7dda..0000000000000 --- a/cypress/e2e/files/hotkeys.cy.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { getRowForFileId } from './FilesUtils.ts' - -describe('Files hotkey handling', () => { - before(() => { - cy.createRandomUser().then((user) => { - cy.mkdir(user, '/abcd') - cy.mkdir(user, '/zyx') - cy.rm(user, '/welcome.txt') - cy.login(user) - }) - }) - - beforeEach(() => cy.visit('/apps/files')) - - it('Pressing "arrow down" should go to first file', () => { - cy.get('[data-cy-files-list]') - .press(Cypress.Keyboard.Keys.DOWN) - - cy.url() - .should('match', /\/apps\/files\/files\/\d+/) - .then((url) => new URL(url).pathname.split('/').at(-1)) - .then((fileId) => getRowForFileId(fileId) - .should('exist') - .and('have.attr', 'data-cy-files-list-row-name', 'abcd')) - }) - - it('Pressing "arrow up" should go to first file', () => { - cy.get('[data-cy-files-list]') - .press(Cypress.Keyboard.Keys.UP) - - cy.url() - .should('match', /\/apps\/files\/files\/\d+/) - .then((url) => new URL(url).pathname.split('/').at(-1)) - .then((fileId) => getRowForFileId(fileId) - .should('exist') - .and('have.attr', 'data-cy-files-list-row-name', 'zyx')) - }) - - it('Pressing D should open the sidebar once', () => { - activateFirstRow() - cy.get('[data-cy-files-list]') - .press('d') - - cy.get('[data-cy-sidebar]') - .should('exist') - .and('be.visible') - }) - - it('Pressing F2 should rename the file', () => { - activateFirstRow() - cy.get('[data-cy-files-list]') - .should('exist') - .then(($el) => { - const el = $el.get(0) - // manually dispatch as Cypress refuses to press F-keys for "security reasons" - cy.log('Dispatching F2 keydown/keyup events') - el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', bubbles: true })) - el.dispatchEvent(new KeyboardEvent('keyup', { key: 'F2', code: 'F2', bubbles: true })) - el.dispatchEvent(new KeyboardEvent('keypress', { key: 'F2', code: 'F2', bubbles: true })) - }) - - cy.get('[data-cy-files-list-row-name]') - .first() - .findByRole('textbox', { name: /Folder name/ }) - .should('exist') - }) - - it('Pressing S should toggle favorite', () => { - activateFirstRow() - cy.get('[data-cy-files-list]') - .press('s') - - cy.get('[data-cy-files-list-row-name]') - .first() - .as('firstRow') - .findByRole('img', { name: /Favorite/ }) - .should('exist') - - cy.get('[data-cy-files-list]') - .press('s') - - cy.get('@firstRow') - .findByRole('img', { name: /Favorite/ }) - .should('not.exist') - }) - - it('Pressing DELETE should delete the folder', () => { - activateFirstRow() - cy.get('td[data-cy-files-list-row-name]') - .should('have.length', 2) - - cy.get('[data-cy-files-list]') - .press(Cypress.Keyboard.Keys.DELETE) - - cy.get('td[data-cy-files-list-row-name]') - .should('have.length', 1) - }) -}) - -/** - * Activates the first row in the files list by simulating a press of the down arrow key. - */ -function activateFirstRow() { - cy.get('[data-cy-files-list]') - .press(Cypress.Keyboard.Keys.DOWN) - cy.url() - .should('match', /\/apps\/files\/files\/\d+/) -} diff --git a/cypress/e2e/files/live_photos.cy.ts b/cypress/e2e/files/live_photos.cy.ts deleted file mode 100644 index 2e7556f676ba1..0000000000000 --- a/cypress/e2e/files/live_photos.cy.ts +++ /dev/null @@ -1,175 +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 { - copyFile, - createFolder, - getRowForFile, - getRowForFileId, - moveFile, - navigateToFolder, - reloadCurrentFolder, - renameFile, - triggerActionForFile, - triggerInlineActionForFileId, -} from './FilesUtils.ts' -import { setShowHiddenFiles, setupLivePhotos } from './LivePhotosUtils.ts' - -describe('Files: Live photos', { testIsolation: true }, () => { - let user: User - let randomFileName: string - let jpgFileId: number - let movFileId: number - - beforeEach(() => { - setupLivePhotos() - .then((setupInfo) => { - user = setupInfo.user - randomFileName = setupInfo.fileName - jpgFileId = setupInfo.jpgFileId - movFileId = setupInfo.movFileId - }) - }) - - it('Only renders the .jpg file', () => { - getRowForFileId(jpgFileId).should('have.length', 1) - getRowForFileId(movFileId).should('have.length', 0) - }) - - context("'Show hidden files' is enabled", () => { - beforeEach(() => { - setShowHiddenFiles(true) - }) - - it("Shows both files when 'Show hidden files' is enabled", () => { - getRowForFileId(jpgFileId).should('have.length', 1).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`) - getRowForFileId(movFileId).should('have.length', 1).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`) - }) - - it('Copies both files when copying the .jpg', () => { - copyFile(`${randomFileName}.jpg`, '.') - reloadCurrentFolder() - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 1) - getRowForFile(`${randomFileName}.mov`).should('have.length', 1) - getRowForFile(`${randomFileName} (1).jpg`).should('have.length', 1) - getRowForFile(`${randomFileName} (1).mov`).should('have.length', 1) - }) - - it('Copies both files when copying the .mov', () => { - copyFile(`${randomFileName}.mov`, '.') - reloadCurrentFolder() - - getRowForFile(`${randomFileName}.mov`).should('have.length', 1) - getRowForFile(`${randomFileName} (1).jpg`).should('have.length', 1) - getRowForFile(`${randomFileName} (1).mov`).should('have.length', 1) - }) - - it('Keeps live photo link when copying folder', () => { - createFolder('folder') - moveFile(`${randomFileName}.jpg`, 'folder') - copyFile('folder', '.') - navigateToFolder('folder (1)') - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 1) - getRowForFile(`${randomFileName}.mov`).should('have.length', 1) - - setShowHiddenFiles(false) - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 1) - getRowForFile(`${randomFileName}.mov`).should('have.length', 0) - }) - - it('Block copying live photo in a folder containing a mov file with the same name', () => { - createFolder('folder') - cy.uploadContent(user, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/folder/${randomFileName}.mov`) - cy.login(user) - cy.visit('/apps/files') - copyFile(`${randomFileName}.jpg`, 'folder') - navigateToFolder('folder') - - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 1) - getRowForFile(`${randomFileName}.mov`).should('have.length', 1) - getRowForFile(`${randomFileName}.jpg`).should('have.length', 0) - getRowForFile(`${randomFileName} (1).jpg`).should('have.length', 0) - }) - - it('Moves files when moving the .jpg', () => { - renameFile(`${randomFileName}.jpg`, `${randomFileName}_moved.jpg`) - reloadCurrentFolder() - - getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`) - getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`) - }) - - it('Moves files when moving the .mov', () => { - renameFile(`${randomFileName}.mov`, `${randomFileName}_moved.mov`) - reloadCurrentFolder() - - getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`) - getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`) - }) - - it('Deletes files when deleting the .jpg', () => { - triggerActionForFile(`${randomFileName}.jpg`, 'delete') - reloadCurrentFolder() - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 0) - getRowForFile(`${randomFileName}.mov`).should('have.length', 0) - - cy.visit('/apps/files/trashbin') - - getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('to.match', new RegExp(`^${randomFileName}.jpg\\.d[0-9]+$`)) - getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('to.match', new RegExp(`^${randomFileName}.mov\\.d[0-9]+$`)) - }) - - it('Block deletion when deleting the .mov', () => { - triggerActionForFile(`${randomFileName}.mov`, 'delete') - reloadCurrentFolder() - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 1) - getRowForFile(`${randomFileName}.mov`).should('have.length', 1) - - cy.visit('/apps/files/trashbin') - - getRowForFileId(jpgFileId).should('have.length', 0) - getRowForFileId(movFileId).should('have.length', 0) - }) - - it('Restores files when restoring the .jpg', () => { - triggerActionForFile(`${randomFileName}.jpg`, 'delete') - cy.visit('/apps/files/trashbin') - - triggerInlineActionForFileId(jpgFileId, 'restore') - reloadCurrentFolder() - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 0) - getRowForFile(`${randomFileName}.mov`).should('have.length', 0) - - cy.visit('/apps/files') - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 1) - getRowForFile(`${randomFileName}.mov`).should('have.length', 1) - }) - - it('Blocks restoration when restoring the .mov', () => { - triggerActionForFile(`${randomFileName}.jpg`, 'delete') - cy.visit('/apps/files/trashbin') - - triggerInlineActionForFileId(movFileId, 'restore') - reloadCurrentFolder() - - getRowForFileId(jpgFileId).should('have.length', 1) - getRowForFileId(movFileId).should('have.length', 1) - - cy.visit('/apps/files') - - getRowForFile(`${randomFileName}.jpg`).should('have.length', 0) - getRowForFile(`${randomFileName}.mov`).should('have.length', 0) - }) - }) -}) diff --git a/cypress/e2e/files/new-menu.cy.ts b/cypress/e2e/files/new-menu.cy.ts deleted file mode 100644 index 554b023bef093..0000000000000 --- a/cypress/e2e/files/new-menu.cy.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { createFolder, getRowForFile, haveValidity, navigateToFolder } from './FilesUtils.ts' - -describe('"New"-menu', { testIsolation: true }, () => { - beforeEach(() => { - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/apps/files') - }) - }) - - it('Create new folder', () => { - // Click the "new" button - cy.get('[data-cy-upload-picker]') - .findByRole('button', { name: 'New' }) - .should('be.visible') - .click() - // Click the "new folder" menu entry - cy.findByRole('menuitem', { name: 'New folder' }) - .should('be.visible') - .click() - // Create a folder - cy.intercept('MKCOL', '**/remote.php/dav/files/**').as('mkdir') - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .type('A new folder{enter}') - cy.wait('@mkdir') - // See the folder is visible - getRowForFile('A new folder') - .should('be.visible') - }) - - it('Does not allow creating forbidden folder names', () => { - // Click the "new" button - cy.get('[data-cy-upload-picker]') - .findByRole('button', { name: 'New' }) - .should('be.visible') - .click() - // Click the "new folder" menu entry - cy.findByRole('menuitem', { name: 'New folder' }) - .should('be.visible') - .click() - // enter folder name - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .type('.htaccess') - // See that input has invalid state set - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .should(haveValidity(/reserved name/i)) - // See that it can not create - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('button', { name: 'Create' }) - .should('be.disabled') - }) - - it('Does not allow creating folders with already existing names', () => { - createFolder('already exists') - // Click the "new" button - cy.get('[data-cy-upload-picker]') - .findByRole('button', { name: 'New' }) - .should('be.visible') - .click() - // Click the "new folder" menu entry - cy.findByRole('menuitem', { name: 'New folder' }) - .should('be.visible') - .click() - // enter folder name - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .type('already exists') - // See that input has invalid state set - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .should(haveValidity(/already in use/i)) - // See that it can not create - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('button', { name: 'Create' }) - .should('be.disabled') - }) - - /** - * Regression test of https://github.com/nextcloud/server/issues/47530 - */ - it('Create same folder in child folder', () => { - // setup other folders - createFolder('folder') - createFolder('other folder') - navigateToFolder('folder') - - // Click the "new" button - cy.get('[data-cy-upload-picker]') - .findByRole('button', { name: 'New' }) - .should('be.visible') - .click() - // Click the "new folder" menu entry - cy.findByRole('menuitem', { name: 'New folder' }) - .should('be.visible') - .click() - // enter folder name - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .type('other folder') - // See that creating is allowed - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('textbox', { name: 'Folder name' }) - .should(haveValidity('')) - // can create - cy.intercept('MKCOL', '**/remote.php/dav/files/**').as('mkdir') - cy.findByRole('dialog', { name: /create new folder/i }) - .findByRole('button', { name: 'Create' }) - .click() - cy.wait('@mkdir') - // see it is created - getRowForFile('other folder') - .should('be.visible') - }) -}) diff --git a/cypress/e2e/files/recent-view.cy.ts b/cypress/e2e/files/recent-view.cy.ts deleted file mode 100644 index 3dd6fb2a8a63b..0000000000000 --- a/cypress/e2e/files/recent-view.cy.ts +++ /dev/null @@ -1,45 +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, triggerActionForFile } from './FilesUtils.ts' - -describe('files: Recent view', { testIsolation: true }, () => { - let user: User - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.login(user) - })) - - it('see the recently created file in the recent view', () => { - cy.visit('/apps/files/recent') - // All are visible by default - getRowForFile('file.txt').should('be.visible') - }) - - /** - * Regression test: There was a bug that the files were correctly loaded but with invalid source - * so the delete action failed. - */ - it('can delete a file in the recent view', () => { - cy.intercept('DELETE', '**/remote.php/dav/files/**').as('deleteFile') - - cy.visit('/apps/files/recent') - // See the row - getRowForFile('file.txt').should('be.visible') - // delete the file - triggerActionForFile('file.txt', 'delete') - cy.wait('@deleteFile') - // See it is not visible anymore - getRowForFile('file.txt').should('not.exist') - // also not existing in default view after reload - cy.visit('/apps/files') - getRowForFile('file.txt').should('not.exist') - }) -}) diff --git a/cypress/e2e/files/router-query.cy.ts b/cypress/e2e/files/router-query.cy.ts deleted file mode 100644 index a7c4aae546de9..0000000000000 --- a/cypress/e2e/files/router-query.cy.ts +++ /dev/null @@ -1,181 +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 { join } from '@nextcloud/paths' -import { getRowForFileId } from './FilesUtils.ts' - -/** - * Check that the sidebar is opened for a specific file - * @param name The name of the file - */ -function sidebarIsOpen(name: string): void { - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('heading', { name }) - .should('be.visible') -} - -/** - * Skip a test without viewer installed - */ -function skipIfViewerDisabled(this: Mocha.Context): void { - cy.runOccCommand('app:list --enabled --output json') - .then((exec) => exec.stdout) - .then((output) => JSON.parse(output)) - .then((obj) => 'viewer' in obj.enabled) - .then((enabled) => { - if (!enabled) { - this.skip() - } - }) -} - -/** - * Check a file was not downloaded - * @param filename The expected filename - */ -function fileNotDownloaded(filename: string): void { - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(join(downloadsFolder, filename)).should('not.exist') -} - -describe('Check router query flags:', function() { - let user: User - let imageId: number - let archiveId: number - let folderId: number - - before(() => { - cy.createRandomUser().then(($user) => { - user = $user - cy.uploadFile(user, 'image.jpg') - .then((response) => { imageId = Number.parseInt(response.headers['oc-fileid']) }) - cy.mkdir(user, '/folder') - .then((response) => { folderId = Number.parseInt(response.headers['oc-fileid']) }) - cy.uploadContent(user, new Blob([]), 'application/zstd', '/archive.zst') - .then((response) => { archiveId = Number.parseInt(response.headers['oc-fileid']) }) - cy.login(user) - }) - }) - - describe('"opendetails"', () => { - it('open details for known file type', () => { - cy.visit(`/apps/files/files/${imageId}?opendetails`) - - // see sidebar - sidebarIsOpen('image.jpg') - - // but no viewer - cy.findByRole('dialog', { name: 'image.jpg' }) - .should('not.exist') - - // and no download - fileNotDownloaded('image.jpg') - }) - - it('open details for unknown file type', () => { - cy.visit(`/apps/files/files/${archiveId}?opendetails`) - - // see sidebar - sidebarIsOpen('archive.zst') - - // but no viewer - cy.findByRole('dialog', { name: 'archive.zst' }) - .should('not.exist') - - // and no download - fileNotDownloaded('archive.zst') - }) - - it('open details for folder', () => { - cy.visit(`/apps/files/files/${folderId}?opendetails`) - - // see sidebar - sidebarIsOpen('folder') - - // but no viewer - cy.findByRole('dialog', { name: 'folder' }) - .should('not.exist') - - // and no download - fileNotDownloaded('folder') - }) - }) - - describe('"openfile"', function() { - /** Check the viewer is open and shows the image */ - function viewerShowsImage(): void { - cy.findByRole('dialog', { name: 'image.jpg' }) - .should('be.visible') - .find(`img[src*="fileId=${imageId}"]`) - .should('be.visible') - } - - it('opens files with default action', function() { - skipIfViewerDisabled.call(this) - - cy.visit(`/apps/files/files/${imageId}?openfile`) - viewerShowsImage() - }) - - it('opens files with default action using explicit query state', function() { - skipIfViewerDisabled.call(this) - - cy.visit(`/apps/files/files/${imageId}?openfile=true`) - viewerShowsImage() - }) - - it('does not open files with default action when using explicitly query value `false`', function() { - skipIfViewerDisabled.call(this) - - cy.visit(`/apps/files/files/${imageId}?openfile=false`) - getRowForFileId(imageId) - .should('be.visible') - .and('have.class', 'files-list__row--active') - - cy.findByRole('dialog', { name: 'image.jpg' }) - .should('not.exist') - }) - - it('does not open folders but shows details', () => { - cy.visit(`/apps/files/files/${folderId}?openfile`) - - // See the URL was replaced - cy.url() - .should('match', /[?&]opendetails(&|=|$)/) - .and('not.match', /openfile/) - - // See the sidebar is correctly opened - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('heading', { name: 'folder' }) - .should('be.visible') - - // see the folder was not changed - getRowForFileId(imageId).should('exist') - }) - - it('does not open unknown file types but shows details', () => { - cy.visit(`/apps/files/files/${archiveId}?openfile`) - - // See the URL was replaced - cy.url() - .should('match', /[?&]opendetails(&|=|$)/) - .and('not.match', /openfile/) - - // See the sidebar is correctly opened - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('heading', { name: 'archive.zst' }) - .should('be.visible') - - // See no file was downloaded - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(join(downloadsFolder, 'archive.zst')).should('not.exist') - }) - }) -}) diff --git a/cypress/e2e/files/scrolling.cy.ts b/cypress/e2e/files/scrolling.cy.ts deleted file mode 100644 index d1647e6df3ec2..0000000000000 --- a/cypress/e2e/files/scrolling.cy.ts +++ /dev/null @@ -1,215 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { beFullyInViewport, notBeFullyInViewport } from '../core-utils.ts' -import { calculateViewportHeight, enableGridMode, getRowForFile } from './FilesUtils.ts' - -describe('files: Scrolling to selected file in file list', () => { - const fileIds = new Map() - let viewportHeight: number - - before(() => { - initFilesAndViewport(fileIds) - .then((_viewportHeight) => { - cy.log(`Saving viewport height to ${_viewportHeight}px`) - viewportHeight = _viewportHeight - }) - }) - - beforeEach(() => { - cy.viewport(1200, viewportHeight) - }) - - it('Can see first file in list', () => { - cy.visit(`/apps/files/files/${fileIds.get(1)}`) - - // See file is visible - getRowForFile('1.txt') - .should('be.visible') - - // we expect also element 6 to be visible - getRowForFile('6.txt') - .should('be.visible') - // but not element 7 - though it should exist (be buffered) - getRowForFile('7.txt') - .should('exist') - .and('not.be.visible') - }) - - // For files already in the visible buffer, scrolling is skipped to prevent jumping - // So we only verify the file exists and is in the DOM - for (let i = 2; i <= 5; i++) { - it(`correctly scrolls to row ${i}`, () => { - cy.visit(`/apps/files/files/${fileIds.get(i)}`) - - // File should exist in the DOM (scroll is skipped when already in visible buffer) - getRowForFile(`${i}.txt`) - .should('exist') - }) - } - - // Row 6 is at the edge of the initial visible buffer, scroll may be skipped - it('correctly scrolls to row 6', () => { - cy.visit(`/apps/files/files/${fileIds.get(6)}`) - - // File should exist in the DOM (scroll may be skipped when in visible buffer) - getRowForFile('6.txt') - .should('exist') - }) - - // For the last "page" of entries we can not scroll further - // so we show all of the last 4 entries - for (let i = 7; i <= 10; i++) { - it(`correctly scrolls to row ${i}`, () => { - cy.visit(`/apps/files/files/${fileIds.get(i)}`) - - // See file is visible - getRowForFile(`${i}.txt`) - .should('be.visible') - .and(notBeOverlappedByTableHeader) - - // there are only max. 4 rows left so also row 6+ should be visible - getRowForFile('6.txt') - .should('be.visible') - getRowForFile('10.txt') - .should('be.visible') - // Also the footer is visible - cy.get('tfoot') - .contains('10 files') - .should(beFullyInViewport) - }) - } -}) - -describe('files: Scrolling to selected file in file list (GRID MODE)', () => { - const fileIds = new Map() - let viewportHeight: number - - before(() => { - initFilesAndViewport(fileIds, true) - .then((_viewportHeight) => { viewportHeight = _viewportHeight }) - }) - - beforeEach(() => { - cy.viewport(768, viewportHeight) - }) - - // First row - for (let i = 1; i <= 3; i++) { - it(`Can see files in first row (file ${i})`, () => { - cy.visit(`/apps/files/files/${fileIds.get(i)}`) - - for (let j = 1; j <= 3; j++) { - // See all files of that row are visible - getRowForFile(`${j}.txt`) - .should('be.visible') - // we expect also the second row to be visible - getRowForFile(`${j + 3}.txt`) - .should('be.visible') - // Because there is no half row on top we also see the third row - getRowForFile(`${j + 6}.txt`) - .should('be.visible') - // But not the forth row - getRowForFile(`${j + 9}.txt`) - .should('exist') - .and(notBeFullyInViewport) - } - }) - } - - // Second row - files already in visible buffer, scroll is skipped - for (let i = 4; i <= 6; i++) { - it(`correctly scrolls to second row (file ${i})`, () => { - cy.visit(`/apps/files/files/${fileIds.get(i)}`) - - // File should exist in the DOM (scroll is skipped when in visible buffer) - getRowForFile(`${i}.txt`) - .should('exist') - }) - } - - // Third row - files may be in visible buffer, scroll may be skipped - for (let i = 7; i <= 9; i++) { - it(`correctly scrolls to third row (file ${i})`, () => { - cy.visit(`/apps/files/files/${fileIds.get(i)}`) - - // File should exist in the DOM (scroll may be skipped when in visible buffer) - getRowForFile(`${i}.txt`) - .should('exist') - }) - } - - // Forth row - scrolling happens for files outside initial visible buffer - for (let i = 10; i <= 12; i++) { - it(`correctly scrolls to forth row (file ${i})`, () => { - cy.visit(`/apps/files/files/${fileIds.get(i)}`) - - // File should be visible after scrolling - getRowForFile(`${i}.txt`) - .should('be.visible') - }) - } -}) - -/// Some helpers - -/** - * Assert that an element is overlapped by the table header - * @param $el The element - * @param expected if it should be overlapped or NOT - */ -function beOverlappedByTableHeader($el: JQuery, expected = true) { - const headerRect = Cypress.$('thead').get(0)!.getBoundingClientRect() - const elementRect = $el.get(0)!.getBoundingClientRect() - const overlap = !(headerRect.right < elementRect.left - || headerRect.left > elementRect.right - || headerRect.bottom < elementRect.top - || headerRect.top > elementRect.bottom) - - if (expected) { - expect(overlap, 'Overlapped by table header').to.be.true - } else { - expect(overlap, 'Not overlapped by table header').to.be.false - } -} - -/** - * Assert that an element is not overlapped by the table header - * @param $el The element - */ -function notBeOverlappedByTableHeader($el: JQuery) { - return beOverlappedByTableHeader($el, false) -} - -function initFilesAndViewport(fileIds: Map, gridMode = false): Cypress.Chainable { - return cy.createRandomUser().then((user) => { - cy.rm(user, '/welcome.txt') - - // Create files with names 1.txt, 2.txt, ..., 10.txt - const count = gridMode ? 12 : 10 - for (let i = 1; i <= count; i++) { - cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`) - .then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString())) - } - - cy.login(user) - cy.viewport(1200, 800) - - cy.visit('/apps/files') - - // If grid mode is requested, enable it - if (gridMode) { - enableGridMode() - } - - // Calculate height to ensure that those 10 elements can not be rendered in one list (only 6 will fit the screen, 3 in grid mode) - return calculateViewportHeight(gridMode ? 3 : 6) - .then((height) => { - // Set viewport height to the calculated height - cy.log(`Setting viewport height to ${height}px`) - cy.wrap(height) - }) - }) -} diff --git a/cypress/e2e/files/search.cy.ts b/cypress/e2e/files/search.cy.ts deleted file mode 100644 index 2800a71a539ee..0000000000000 --- a/cypress/e2e/files/search.cy.ts +++ /dev/null @@ -1,217 +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 { FilesNavigationPage } from '../../pages/FilesNavigation.ts' -import { getRowForFile, navigateToFolder, reloadCurrentFolder } from './FilesUtils.ts' - -describe('files: search', () => { - let user: User - - const navigation = new FilesNavigationPage() - - before(() => { - cy.createRandomUser().then(($user) => { - user = $user - cy.mkdir(user, '/some folder') - cy.mkdir(user, '/some folder/nested folder') - cy.mkdir(user, '/other folder') - cy.mkdir(user, '/12345') - cy.uploadContent(user, new Blob(['content']), 'text/plain', '/file.txt') - cy.uploadContent(user, new Blob(['content']), 'text/plain', '/some folder/a file.txt') - cy.uploadContent(user, new Blob(['content']), 'text/plain', '/some folder/a second file.txt') - cy.uploadContent(user, new Blob(['content']), 'text/plain', '/some folder/nested folder/deep file.txt') - cy.uploadContent(user, new Blob(['content']), 'text/plain', '/other folder/another file.txt') - cy.login(user) - }) - }) - - beforeEach(() => { - cy.visit('/apps/files') - }) - - it('updates the query on the URL', () => { - navigation.searchScopeTrigger().click() - navigation.searchScopeMenu() - .should('be.visible') - .findByRole('menuitem', { name: /search everywhere/i }) - .should('be.visible') - .click() - - navigation.searchInput().type('file') - cy.url().should('match', /query=file($|&)/) - }) - - it('can search globally', () => { - navigation.searchScopeTrigger().click() - navigation.searchScopeMenu() - .should('be.visible') - .findByRole('menuitem', { name: /search everywhere/i }) - .should('be.visible') - .click() - navigation.searchInput().type('file') - - getRowForFile('file.txt').should('be.visible') - getRowForFile('a file.txt').should('be.visible') - getRowForFile('a second file.txt').should('be.visible') - getRowForFile('another file.txt').should('be.visible') - }) - - it('filter does also search locally', () => { - navigateToFolder('some folder') - getRowForFile('a file.txt').should('be.visible') - - navigation.searchInput().type('file') - - getRowForFile('a file.txt').should('be.visible') - getRowForFile('a second file.txt').should('be.visible') - getRowForFile('deep file.txt').should('be.visible') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 3) - }) - - it('See "search everywhere" button', () => { - // Not visible initially - cy.get('.files-list__filters') - .findByRole('button', { name: /Search everywhere/i }) - .should('not.to.exist') - - // add a filter - navigation.searchInput().type('file') - - // see its visible - cy.get('.files-list__filters') - .findByRole('button', { name: /Search everywhere/i }) - .should('be.visible') - - // clear the filter - navigation.searchClearButton().click() - - // see its not visible again - cy.get('.files-list__filters') - .findByRole('button', { name: /Search everywhere/i }) - .should('not.to.exist') - }) - - it('can make local search a global search', () => { - navigateToFolder('some folder') - getRowForFile('a file.txt').should('be.visible') - - navigation.searchInput().type('file') - - // see local results - getRowForFile('a file.txt').should('be.visible') - getRowForFile('a second file.txt').should('be.visible') - getRowForFile('deep file.txt').should('be.visible') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 3) - - // toggle global search - cy.get('.files-list__filters') - .findByRole('button', { name: /Search everywhere/i }) - .should('be.visible') - .click() - - // see global results - getRowForFile('file.txt').should('be.visible') - getRowForFile('a file.txt').should('be.visible') - getRowForFile('deep file.txt').should('be.visible') - getRowForFile('a second file.txt').should('be.visible') - getRowForFile('another file.txt').should('be.visible') - }) - - it('shows empty content when there are no results', () => { - navigateToFolder('some folder') - getRowForFile('a file.txt').should('be.visible') - - navigation.searchScopeTrigger().click() - navigation.searchScopeMenu() - .should('be.visible') - .findByRole('menuitem', { name: /search everywhere/i }) - .should('be.visible') - .click() - navigation.searchInput().type('xyz') - - // see the empty content message - cy.contains('[role="note"]', /No search results for .xyz./) - .should('be.visible') - .within(() => { - // see within there is a search box with the same value - cy.findByRole('searchbox', { name: /search for files/i }) - .should('be.visible') - .and('have.value', 'xyz') - }) - }) - - it('can alter search', () => { - navigation.searchScopeTrigger().click() - navigation.searchScopeMenu() - .should('be.visible') - .findByRole('menuitem', { name: /search everywhere/i }) - .should('be.visible') - .click() - navigation.searchInput().type('other') - - getRowForFile('another file.txt').should('be.visible') - getRowForFile('other folder').should('be.visible') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2) - - navigation.searchInput().type(' file') - navigation.searchInput().should('have.value', 'other file') - getRowForFile('another file.txt').should('be.visible') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 1) - }) - - it('returns to file list if search is cleared', () => { - navigation.searchScopeTrigger().click() - navigation.searchScopeMenu() - .should('be.visible') - .findByRole('menuitem', { name: /search everywhere/i }) - .should('be.visible') - .click() - navigation.searchInput().type('other') - - getRowForFile('another file.txt').should('be.visible') - getRowForFile('other folder').should('be.visible') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2) - - navigation.searchClearButton().click() - navigation.searchInput().should('have.value', '') - getRowForFile('file.txt').should('be.visible') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 5) - }) - - /** - * Problem: - * 1. Being on the search view - * 2. Press the refresh button (name of the current view) - * 3. See that the router link does not preserve the query - * - * We fix this with a navigation guard and need to verify that it works - */ - it('keeps the query in the URL', () => { - navigation.searchScopeTrigger().click() - navigation.searchScopeMenu() - .should('be.visible') - .findByRole('menuitem', { name: /search everywhere/i }) - .should('be.visible') - .click() - navigation.searchInput().type('file') - - // see that the search view is loaded - getRowForFile('a file.txt').should('be.visible') - // see the correct url - cy.url().should('match', /query=file($|&)/) - - cy.intercept('SEARCH', '**/remote.php/dav/').as('search') - // refresh the view - reloadCurrentFolder(false) // no PROPFIND intercept here as we want to wait for SEARCH - // wait for the request - cy.wait('@search') - // see that the search view is reloaded - getRowForFile('a file.txt').should('be.visible') - // see the correct url - cy.url().should('match', /query=file($|&)/) - }) -}) diff --git a/cypress/e2e/files_external/StorageUtils.ts b/cypress/e2e/files_external/StorageUtils.ts deleted file mode 100644 index 49b3f582d51bb..0000000000000 --- a/cypress/e2e/files_external/StorageUtils.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { User } from '@nextcloud/e2e-test-server/cypress' - -export type StorageConfig = { - [key: string]: string -} - -export type StorageMountOption = { - readonly: boolean -} - -export enum StorageBackend { - DAV = 'dav', - SMB = 'smb', - SFTP = 'sftp', - LOCAL = 'local', -} - -export enum AuthBackend { - GlobalAuth = 'password::global', - LoginCredentials = 'password::logincredentials', - Password = 'password::password', - SessionCredentials = 'password::sessioncredentials', - UserGlobalAuth = 'password::global::user', - UserProvided = 'password::userprovided', - Null = 'null::null', -} - -/** - * Create a storage via occ - * - * @param mountPoint - * @param storageBackend - * @param authBackend - * @param configs - * @param user - */ -export function createStorageWithConfig(mountPoint: string, storageBackend: StorageBackend, authBackend: AuthBackend, configs: StorageConfig, user?: User): Cypress.Chainable { - const configsFlag = Object.keys(configs).map((key) => `--config "${key}=${configs[key]}"`).join(' ') - const userFlag = user ? `--user ${user.userId}` : '' - - const command = `files_external:create "${mountPoint}" "${storageBackend}" "${authBackend}" ${configsFlag} ${userFlag}` - - cy.log(`Creating storage with command: ${command}`) - return cy.runOccCommand(command) - .then(({ stdout }) => { - return stdout.replace('Storage created with id ', '') - }) -} - -/** - * - * @param mountId - * @param options - */ -export function setStorageMountOptions(mountId: string, options: StorageMountOption) { - for (const [key, value] of Object.entries(options)) { - cy.runOccCommand(`files_external:option ${mountId} ${key} ${value}`) - } -} - -/** - * - */ -export function deleteAllExternalStorages() { - cy.runOccCommand('files_external:list --all --output=json').then(({ stdout }) => { - const list = JSON.parse(stdout) - list.forEach((storage) => cy.runOccCommand(`files_external:delete --yes ${storage.mount_id}`), { failOnNonZeroExit: false }) - }) -} diff --git a/cypress/e2e/files_external/files-external-failed.cy.ts b/cypress/e2e/files_external/files-external-failed.cy.ts deleted file mode 100644 index 099365bc95b2e..0000000000000 --- a/cypress/e2e/files_external/files-external-failed.cy.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 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 { AuthBackend, createStorageWithConfig, StorageBackend } from './StorageUtils.ts' - -describe('Files user credentials', { testIsolation: true }, () => { - let currentUser: User - - before(() => { - cy.runOccCommand('app:enable files_external') - cy.createRandomUser().then((user) => { - currentUser = user - }) - cy.runCommand('php ./cron.php') - }) - - afterEach(() => { - // Cleanup global storages - cy.runOccCommand('files_external:list --output=json').then(({ stdout }) => { - const list = JSON.parse(stdout) - list.forEach((storage) => cy.runOccCommand(`files_external:delete --yes ${storage.mount_id}`), { failOnNonZeroExit: false }) - }) - }) - - after(() => { - cy.runOccCommand('app:disable files_external') - }) - - it('Create a failed user storage with invalid url', () => { - const url = 'http://cloud.domain.com/remote.php/dav/files/abcdef123456' - createStorageWithConfig('Storage1', StorageBackend.DAV, AuthBackend.LoginCredentials, { host: url.replace('index.php/', ''), secure: 'false' }).then((id) => { - cy.runOccCommand(`files_external:verify ${id}`) - }) - - cy.login(currentUser) - cy.visit('/apps/files') - - // TODO: Why does the first PROPFIND does not return it? - getRowForFile('Storage1') - .if('not.exist') - .reload() - - // Ensure the row is visible and marked as unavailable - getRowForFile('Storage1').as('row').should('be.visible') - cy.get('@row').find('[data-cy-files-list-row-name-link]') - .should('have.attr', 'title', / \(unavailable\)$/) - - // Ensure clicking on the location does not open the folder - cy.location().then((loc) => { - cy.get('@row').find('[data-cy-files-list-row-name-link]').click() - cy.location('href').should('eq', loc.href) - }) - }) - - it('Create a failed user storage with invalid login credentials', () => { - const url = 'http://cloud.domain.com/remote.php/dav/files/abcdef123456' - createStorageWithConfig('Storage2', StorageBackend.DAV, AuthBackend.Password, { - host: url.replace('index.php/', ''), - user: 'invaliduser', - password: 'invalidpassword', - secure: 'false', - }).then((id) => { - cy.runOccCommand(`files_external:verify ${id}`) - }) - - cy.login(currentUser) - cy.visit('/apps/files') - - // Ensure the row is visible and marked as unavailable - getRowForFile('Storage2').as('row').should('be.visible') - cy.get('@row').find('[data-cy-files-list-row-name-link]') - .should('have.attr', 'title', / \(unavailable\)$/) - - // Ensure clicking on the location does not open the folder - cy.location().then((loc) => { - cy.get('@row').find('[data-cy-files-list-row-name-link]').click() - cy.location('href').should('eq', loc.href) - }) - }) -}) diff --git a/cypress/e2e/files_external/files-user-credentials.cy.ts b/cypress/e2e/files_external/files-user-credentials.cy.ts deleted file mode 100644 index ac44cdb0c4f86..0000000000000 --- a/cypress/e2e/files_external/files-user-credentials.cy.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { User } from '@nextcloud/e2e-test-server/cypress' - -import { handlePasswordConfirmation } from '../core-utils.ts' -import { getInlineActionEntryForFile, getRowForFile, navigateToFolder, triggerInlineActionForFile } from '../files/FilesUtils.ts' -import { AuthBackend, createStorageWithConfig, StorageBackend } from './StorageUtils.ts' - -const ACTION_CREDENTIALS_EXTERNAL_STORAGE = 'credentials-external-storage' - -describe('Files user credentials', { testIsolation: true }, () => { - let user1: User - let user2: User - let storageUser: User - - before(() => { - cy.runOccCommand('app:enable files_external') - - // Create some users - cy.createRandomUser().then((user) => { - user1 = user - }) - cy.createRandomUser().then((user) => { - user2 = user - }) - - // This user will hold the webdav storage - cy.createRandomUser().then((user) => { - storageUser = user - cy.uploadFile(user, 'image.jpg') - }) - }) - - after(() => { - // Cleanup global storages - cy.runOccCommand('files_external:list --output=json').then(({ stdout }) => { - const list = JSON.parse(stdout) - list.forEach((storage) => cy.runOccCommand(`files_external:delete --yes ${storage.mount_id}`), { failOnNonZeroExit: false }) - }) - - cy.runOccCommand('app:disable files_external') - }) - - it('Create a user storage with user credentials', () => { - // Its not the public server address but the address so the server itself can connect to it - const base = 'http://localhost' - const host = `${base}/remote.php/dav/files/${storageUser.userId}` - createStorageWithConfig(storageUser.userId, StorageBackend.DAV, AuthBackend.UserProvided, { host, secure: 'false' }) - - cy.login(user1) - cy.visit('/apps/files/extstoragemounts') - getRowForFile(storageUser.userId).should('be.visible') - - cy.intercept('PUT', '**/apps/files_external/userglobalstorages/*').as('setCredentials') - - triggerInlineActionForFile(storageUser.userId, ACTION_CREDENTIALS_EXTERNAL_STORAGE) - - // See credentials dialog - cy.findByRole('dialog', { name: 'Storage credentials' }).as('storageDialog') - cy.get('@storageDialog').should('be.visible') - cy.get('@storageDialog').findByRole('textbox', { name: 'Login' }).type(storageUser.userId) - cy.get('@storageDialog').get('input[type="password"]').type(storageUser.password) - cy.get('@storageDialog').get('button').contains('Confirm').click() - cy.get('@storageDialog').should('not.exist') - - // Storage dialog now closed, the user auth dialog should be visible - cy.findByRole('dialog', { name: 'Authentication required' }).as('authDialog') - cy.get('@authDialog').should('be.visible') - handlePasswordConfirmation(user1.password) - - // Wait for the credentials to be set - cy.wait('@setCredentials') - - // Auth dialog should be closed and the set credentials button should be gone - cy.get('@authDialog').should('not.exist', { timeout: 2000 }) - - getInlineActionEntryForFile(storageUser.userId, ACTION_CREDENTIALS_EXTERNAL_STORAGE) - .should('not.exist') - - // Finally, the storage should be accessible - cy.visit('/apps/files') - navigateToFolder(storageUser.userId) - getRowForFile('image.jpg').should('be.visible') - }) - - it('Create a user storage with GLOBAL user credentials', () => { - // Its not the public server address but the address so the server itself can connect to it - const base = 'http://localhost' - const host = `${base}/remote.php/dav/files/${storageUser.userId}` - createStorageWithConfig('storage1', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host, secure: 'false' }) - - cy.login(user2) - cy.visit('/apps/files/extstoragemounts') - getRowForFile('storage1').should('be.visible') - - cy.intercept('PUT', '**/apps/files_external/userglobalstorages/*').as('setCredentials') - - triggerInlineActionForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE) - - // See credentials dialog - cy.findByRole('dialog', { name: 'Storage credentials' }).as('storageDialog') - cy.get('@storageDialog').should('be.visible') - cy.get('@storageDialog').findByRole('textbox', { name: 'Login' }).type(storageUser.userId) - cy.get('@storageDialog').get('input[type="password"]').type(storageUser.password) - cy.get('@storageDialog').get('button').contains('Confirm').click() - cy.get('@storageDialog').should('not.exist') - - // Storage dialog now closed, the user auth dialog should be visible - cy.findByRole('dialog', { name: 'Authentication required' }).as('authDialog') - cy.get('@authDialog').should('be.visible') - handlePasswordConfirmation(user2.password) - - // Wait for the credentials to be set - cy.wait('@setCredentials') - - // Auth dialog should be closed and the set credentials button should be gone - cy.get('@authDialog').should('not.exist', { timeout: 2000 }) - getInlineActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist') - - // Finally, the storage should be accessible - cy.visit('/apps/files') - navigateToFolder('storage1') - getRowForFile('image.jpg').should('be.visible') - }) - - it('Create another user storage while reusing GLOBAL user credentials', () => { - // Its not the public server address but the address so the server itself can connect to it - const base = 'http://localhost' - const host = `${base}/remote.php/dav/files/${storageUser.userId}` - createStorageWithConfig('storage2', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host, secure: 'false' }) - - cy.login(user2) - cy.visit('/apps/files/extstoragemounts') - getRowForFile('storage2').should('be.visible') - - // Since we already have set the credentials, the action should not be present - getInlineActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist') - getInlineActionEntryForFile('storage2', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist') - - // Finally, the storage should be accessible - cy.visit('/apps/files') - navigateToFolder('storage2') - getRowForFile('image.jpg').should('be.visible') - }) -}) diff --git a/cypress/e2e/files_external/home-folder-root-mount-permissions.cy.ts b/cypress/e2e/files_external/home-folder-root-mount-permissions.cy.ts deleted file mode 100644 index 731f37ac41ed9..0000000000000 --- a/cypress/e2e/files_external/home-folder-root-mount-permissions.cy.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { User } from '@nextcloud/e2e-test-server/cypress' - -import { AuthBackend, createStorageWithConfig, deleteAllExternalStorages, setStorageMountOptions, StorageBackend } from './StorageUtils.ts' - -describe('Home folder root mount permissions', { testIsolation: true }, () => { - let user1: User - - before(() => { - cy.runOccCommand('app:enable files_external') - cy.createRandomUser().then((user) => { - user1 = user - }) - }) - - after(() => { - deleteAllExternalStorages() - cy.runOccCommand('app:disable files_external') - }) - - it('Does not show write actions on read-only storage mounted at the root of the user\'s home folder', () => { - cy.login(user1) - cy.visit('/apps/files/') - cy.runOccCommand('config:app:get files overwrites_home_folders --default-value=[]') - .then(({ stdout }) => assert.equal(stdout.trim(), '[]')) - - cy.get('[data-cy-upload-picker=""]').should('exist') - - createStorageWithConfig('/', StorageBackend.LOCAL, AuthBackend.Null, { datadir: '/tmp' }) - .then((id) => setStorageMountOptions(id, { readonly: true })) - // HACK: somehow, we need to create an external folder targeting a subpath for the previous one to show. - createStorageWithConfig('/a', StorageBackend.LOCAL, AuthBackend.Null, { datadir: '/tmp' }) - cy.visit('/apps/files/') - cy.visit('/apps/files/') - cy.runOccCommand('config:app:get files overwrites_home_folders') - .then(({ stdout }) => assert.equal(stdout.trim(), '["files_external"]')) - cy.get('[data-cy-upload-picker=""]').should('not.exist') - - deleteAllExternalStorages() - cy.visit('/apps/files/') - cy.runOccCommand('config:app:get files overwrites_home_folders') - .then(({ stdout }) => assert.equal(stdout.trim(), '[]')) - cy.get('[data-cy-upload-picker=""]').should('exist') - }) -}) diff --git a/cypress/e2e/files_external/settings.cy.ts b/cypress/e2e/files_external/settings.cy.ts deleted file mode 100644 index ed47a85d4a3ea..0000000000000 --- a/cypress/e2e/files_external/settings.cy.ts +++ /dev/null @@ -1,157 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { handlePasswordConfirmation } from '../core-utils.ts' - -describe('files_external settings', () => { - before(() => { - cy.runOccCommand('app:enable files_external') - cy.login({ language: 'en', password: 'admin', userId: 'admin' }) - }) - - beforeEach(() => { - cy.runOccCommand('files_external:list --output json') - .then((exec) => { - const list = JSON.parse(exec.stdout) - for (const { mount_id: mountId } of list) { - cy.runOccCommand('files_external:delete ' + mountId + ' --yes') - } - }) - cy.visit('/settings/admin/externalstorages') - }) - - it('can see the settings section', () => { - cy.findByRole('heading', { name: /External storage/, level: 2 }) - .should('be.visible') - cy.findByRole('table', { name: 'External storages' }) - .should('be.visible') - }) - - it('can see the dialog', () => { - openDialog() - - cy.findByRole('dialog', { name: 'Add storage' }) - .within(() => { - cy.findByRole('textbox', { name: 'Folder name' }) - .should('be.visible') - - getComboBox(/External storage/) - .should('be.visible') - getComboBox(/Authentication/) - .should('be.visible') - getComboBox(/Restrict to/) - .should('be.visible') - cy.findByRole('button', { name: 'Create' }) - .should('be.visible') - .and('have.attr', 'type', 'submit') - }) - }) - - it('can create storage using the dialog', () => { - openDialog() - - cy.findByRole('dialog', { name: 'Add storage' }) - .within(() => { - cy.findByRole('textbox', { name: 'Folder name' }) - .should('be.visible') - .type('My Storage') - - getComboBox(/External storage/) - .should('be.visible') - .click() - cy.root().closest('body') - .findByRole('option', { name: 'WebDAV' }) - .click({ force: true }) // forced due to ordering + `position: fixed` usage - - getComboBox(/Authentication/) - .should('be.visible') - .as('authComboBox') - .click() - cy.root().closest('body') - .findByRole('option', { name: /Login and password/ }) - .should('be.visible') - .click() - - cy.findByRole('textbox', { name: 'Login' }).as('login') - cy.get('@login').scrollIntoView() - cy.get('@login').should('be.visible') - .type('admin') - - cy.get('input[type="password"]').as('password') - cy.get('@password').scrollIntoView() - cy.get('@password').should('be.visible') - .type('admin') - - cy.findByRole('button', { name: 'Create' }) - .should('be.visible') - .click() - - cy.findByRole('textbox', { name: 'URL' }) - .should('be.visible') - .and((el) => el.is(':invalid')) - .type('http://localhost/remote.php/dav/files/admin') - - cy.findByRole('checkbox', { name: /Secure/ }) - .uncheck({ force: true }) - - cy.findByRole('button', { name: 'Create' }) - .should('be.visible') - .click() - }) - handlePasswordConfirmation('admin') - - cy.findAllByRole('dialog').should('not.exist') - - getTable() - .findAllByRole('row') - .should('have.length', 1) - getTable() - .findByRole('row') - .as('storageRow') - .findByRole('cell', { name: /My Storage/ }) - .should('be.visible') - - cy.get('@storageRow') - .findByRole('cell', { name: /WebDAV/ }) - .should('be.visible') - cy.get('@storageRow') - .findByRole('cell', { name: /Login and password/ }) - .should('be.visible') - cy.get('@storageRow') - .findByRole('button', { name: /Edit/ }) - .should('be.visible') - cy.get('@storageRow') - .findByRole('button', { name: /Delete/ }) - .should('be.visible') - .as('deleteButton') - - cy.get('@deleteButton') - .click() - handlePasswordConfirmation('admin') - - getTable() - .findByRole('row') - .should('not.exist') - }) -}) - -/** - * Get the external storages table - */ -function getTable() { - return cy.findByRole('table', { name: 'External storages' }) - .find('tbody') -} - -function openDialog() { - cy.findByRole('button', { name: 'Add external storage' }).click() - cy.findByRole('dialog', { name: 'Add storage' }).should('be.visible') -} - -function getComboBox(match: RegExp) { - return cy.contains('label', match) - .should('be.visible') - .then((el) => Cypress.$(`#${el.attr('for')}`)) -} 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-copy-move.cy.ts b/cypress/e2e/files_sharing/files-copy-move.cy.ts deleted file mode 100644 index 16d07c3d40c20..0000000000000 --- a/cypress/e2e/files_sharing/files-copy-move.cy.ts +++ /dev/null @@ -1,156 +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 { - copyFile, - getRowForFile, - navigateToFolder, - triggerActionForFile, -} from '../files/FilesUtils.ts' -import { createShare } from './FilesSharingUtils.ts' - -const ACTION_COPY_MOVE = 'move-copy' - -export function copyFileForbidden(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') - - const directories = dirPath.split('/') - directories.forEach((directory) => { - // select the folder - cy.get(`[data-filename="${CSS.escape(directory)}"]`).should('be.visible').click() - }) - - // check copy button - cy.contains('button', `Copy to ${directories.at(-1)}`).should('be.disabled') - }) -} - -export function moveFileForbidden(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') - - // select home folder - cy.get('.breadcrumb') - .findByRole('button', { name: 'All files' }) - .should('be.visible') - .click() - - 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('not.exist') - }) -} - -describe('files_sharing: Move or copy files', { testIsolation: true }, () => { - let user: User - let sharee: User - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - }) - cy.createRandomUser().then(($user) => { - sharee = $user - }) - }) - - it('can create a file in a shared folder', () => { - // share the folder - cy.mkdir(user, '/folder') - cy.login(user) - cy.visit('/apps/files') - createShare('folder', sharee.userId, { read: true, download: true }) - cy.logout() - - // Now for the sharee - cy.uploadContent(sharee, new Blob([]), 'text/plain', '/folder/file.txt') - cy.login(sharee) - // visit shared files view - cy.visit('/apps/files') - // see the shared folder - getRowForFile('folder').should('be.visible') - navigateToFolder('folder') - // Content of the shared folder - getRowForFile('file.txt').should('be.visible') - }) - - it('can copy a file to a shared folder', () => { - // share the folder - cy.mkdir(user, '/folder') - cy.login(user) - cy.visit('/apps/files') - createShare('folder', sharee.userId, { read: true, download: true }) - cy.logout() - - // Now for the sharee - cy.uploadContent(sharee, new Blob([]), 'text/plain', '/file.txt') - cy.login(sharee) - // visit shared files view - cy.visit('/apps/files') - // see the shared folder - getRowForFile('folder').should('be.visible') - // copy file to a shared folder - copyFile('file.txt', 'folder') - // click on the folder should open it in files - navigateToFolder('folder') - // Content of the shared folder - getRowForFile('file.txt').should('be.visible') - }) - - it('can not copy a file to a shared folder with no create permissions', () => { - // share the folder - cy.mkdir(user, '/folder') - cy.login(user) - cy.visit('/apps/files') - createShare('folder', sharee.userId, { read: true, download: true, create: false }) - cy.logout() - - // Now for the sharee - cy.uploadContent(sharee, new Blob([]), 'text/plain', '/file.txt') - cy.login(sharee) - // visit shared files view - cy.visit('/apps/files') - // see the shared folder - getRowForFile('folder').should('be.visible') - copyFileForbidden('file.txt', 'folder') - }) - - it('can not move a file from a shared folder with no delete permissions', () => { - // share the folder - cy.mkdir(user, '/folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/file.txt') - cy.login(user) - cy.visit('/apps/files') - createShare('folder', sharee.userId, { read: true, download: true, delete: false }) - cy.logout() - - // Now for the sharee - cy.mkdir(sharee, '/folder-own') - cy.login(sharee) - // visit shared files view - cy.visit('/apps/files') - // see the shared folder - getRowForFile('folder').should('be.visible') - navigateToFolder('folder') - getRowForFile('file.txt').should('be.visible') - moveFileForbidden('file.txt', 'folder-own') - }) -}) 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/files_trashbin/files-trash-action.cy.ts b/cypress/e2e/files_trashbin/files-trash-action.cy.ts deleted file mode 100644 index fff87776c91ff..0000000000000 --- a/cypress/e2e/files_trashbin/files-trash-action.cy.ts +++ /dev/null @@ -1,69 +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 { deleteFileWithRequest, triggerFileListAction } from '../files/FilesUtils.ts' - -const FILE_COUNT = 5 -describe('files_trashbin: Empty trashbin action', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - // create 5 fake files and move them to trash - for (let index = 0; index < FILE_COUNT; index++) { - cy.uploadContent(user, new Blob(['']), 'text/plain', `/file${index}.txt`) - deleteFileWithRequest(user, `/file${index}.txt`) - } - // login - cy.login(user) - }) - }) - - it('Can empty trashbin', () => { - cy.visit('/apps/files') - // Home have no files (or the default welcome file) - cy.get('[data-cy-files-list-row-fileid]').should('have.length', 1) - cy.get('[data-cy-files-list-action="empty-trash"]').should('not.exist') - - // Go to trashbin, and see our deleted files - cy.visit('/apps/files/trashbin') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', FILE_COUNT) - - // Empty trashbin - cy.intercept('DELETE', '**/remote.php/dav/trashbin/**').as('emptyTrash') - triggerFileListAction('empty-trash') - - // Confirm dialog - cy.get('[role=dialog]').should('be.visible') - .findByRole('button', { name: 'Empty deleted files' }).click() - - // Wait for the request to finish - cy.wait('@emptyTrash').its('response.statusCode').should('eq', 204) - cy.get('@emptyTrash.all').should('have.length', 1) - - // Trashbin should be empty - cy.get('[data-cy-files-list-row-fileid]').should('not.exist') - }) - - it('Cancelling empty trashbin action does not delete anything', () => { - // Go to trashbin, and see our deleted files - cy.visit('/apps/files/trashbin') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', FILE_COUNT) - - // Empty trashbin - cy.intercept('DELETE', '**/remote.php/dav/trashbin/**').as('emptyTrash') - triggerFileListAction('empty-trash') - - // Cancel dialog - cy.get('[role=dialog]').should('be.visible') - .findByRole('button', { name: 'Cancel' }).click() - - // request was never sent - cy.get('@emptyTrash').should('not.exist') - cy.get('[data-cy-files-list-row-fileid]').should('have.length', FILE_COUNT) - }) -}) diff --git a/cypress/e2e/files_trashbin/files.cy.ts b/cypress/e2e/files_trashbin/files.cy.ts deleted file mode 100644 index d5290b6d6a2a6..0000000000000 --- a/cypress/e2e/files_trashbin/files.cy.ts +++ /dev/null @@ -1,137 +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 { ShareType } from '@nextcloud/sharing' -import { deleteDownloadsFolderBeforeEach } from '../../support/utils/deleteDownloadsFolder.ts' -import { randomString } from '../../support/utils/randomString.ts' -import { deleteFileWithRequest, getRowForFileId, selectAllFiles, triggerActionForFileId } from '../files/FilesUtils.ts' - -describe('files_trashbin: download files', { testIsolation: true }, () => { - let user: User - const fileids: [number, number] = [0, 0] - - deleteDownloadsFolderBeforeEach() - - before(() => { - cy.createRandomUser().then(($user) => { - user = $user - - cy.uploadContent(user, new Blob(['']), 'text/plain', '/file.txt') - .then(({ headers }) => fileids[0] = Number.parseInt(headers['oc-fileid'])) - .then(() => deleteFileWithRequest(user, '/file.txt')) - cy.uploadContent(user, new Blob(['']), 'text/plain', '/other-file.txt') - .then(({ headers }) => fileids[1] = Number.parseInt(headers['oc-fileid'])) - .then(() => deleteFileWithRequest(user, '/other-file.txt')) - }) - }) - - beforeEach(() => { - cy.login(user) - cy.visit('/apps/files/trashbin') - }) - - it('can download file', () => { - getRowForFileId(fileids[0]).should('be.visible') - getRowForFileId(fileids[1]).should('be.visible') - - triggerActionForFileId(fileids[0], 'download') - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) - - it('can download a file using default action', () => { - getRowForFileId(fileids[0]) - .should('be.visible') - .findByRole('button', { name: 'Download' }) - .click({ force: true }) - - const downloadsFolder = Cypress.config('downloadsFolder') - cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 }) - .should('exist') - .and('have.length.gt', 8) - .and('equal', '') - }) - - // TODO: Fix this as this dependens on the webdav zip folder plugin not working for trashbin (and never worked with old NC legacy download ajax as well) - it('does not offer bulk download', () => { - cy.get('[data-cy-files-list-row-checkbox]').should('have.length', 2) - selectAllFiles() - cy.get('.files-list__selected').should('contain.text', '2 selected') - cy.get('[data-cy-files-list-selection-action="restore"]').should('be.visible') - cy.get('[data-cy-files-list-selection-action="download"]').should('not.exist') - }) -}) - -describe('files_trashbin: file row', { testIsolation: true }, () => { - let alice: User - let bob: User - let randomGroupName: string - let fileId: number - - before(() => { - randomGroupName = randomString(10) - cy.runOccCommand(`group:add ${randomGroupName}`) - - cy.createRandomUser().then((user) => { - alice = user - - cy.modifyUser(alice, 'display', 'Alice') - - cy.mkdir(alice, '/Shared') - }) - - cy.createRandomUser().then((user) => { - bob = user - - cy.modifyUser(bob, 'display', 'Bob') - - cy.runOccCommand(`group:adduser ${randomGroupName} ${bob.userId}`) - }) - }) - - it('shows data for file deleted by owner', () => { - cy.uploadContent(alice, new Blob(['']), 'text/plain', '/test-file.txt') - .then(({ headers }) => fileId = Number.parseInt(headers['oc-fileid'])) - .then(() => deleteFileWithRequest(alice, '/test-file.txt')) - - cy.login(alice) - cy.visit('/apps/files/trashbin') - - getRowForFileId(fileId).should('be.visible') - // The full name includes one span for the name and one span for the - // extension, so text() returns a space when composing them even if it - // will not be visible when rendered in the browser. - getRowForFileId(fileId).find('[data-cy-files-list-row-name]').should((element) => expect(element.text().trim()).to.equal('test-file .txt')) - getRowForFileId(fileId).find('[data-cy-files-list-row-column-custom="files_trashbin--original-location"]').should('have.text', 'All files') - getRowForFileId(fileId).find('[data-cy-files-list-row-column-custom="files_trashbin--deleted-by"]').should('have.text', 'You') - getRowForFileId(fileId).find('[data-cy-files-list-row-column-custom="files_trashbin--deleted"]').should('have.text', 'few seconds ago') - }) - - it('shows data for file deleted by sharee in a folder shared with a group', () => { - cy.createShare(alice, '/Shared', ShareType.Group, randomGroupName) - - cy.uploadContent(alice, new Blob(['']), 'text/plain', '/Shared/test-file.txt') - .then(({ headers }) => fileId = Number.parseInt(headers['oc-fileid'])) - .then(() => deleteFileWithRequest(bob, '/Shared/test-file.txt')) - - cy.login(alice) - cy.visit('/apps/files/trashbin') - - getRowForFileId(fileId).should('be.visible') - // The full name includes one span for the name and one span for the - // extension, so text() returns a space when composing them even if it - // will not be visible when rendered in the browser. - getRowForFileId(fileId).find('[data-cy-files-list-row-name]').should((element) => expect(element.text().trim()).to.equal('test-file .txt')) - getRowForFileId(fileId).find('[data-cy-files-list-row-column-custom="files_trashbin--original-location"]').should('have.text', 'Shared') - getRowForFileId(fileId).find('[data-cy-files-list-row-column-custom="files_trashbin--deleted-by"]').should('have.text', 'Bob') - getRowForFileId(fileId).find('[data-cy-files-list-row-column-custom="files_trashbin--deleted"]').should('have.text', 'few seconds ago') - }) -}) diff --git a/cypress/e2e/files_versions/filesVersionsUtils.ts b/cypress/e2e/files_versions/filesVersionsUtils.ts deleted file mode 100644 index ae23dca409789..0000000000000 --- a/cypress/e2e/files_versions/filesVersionsUtils.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { User } from '@nextcloud/e2e-test-server/cypress' -import type { ShareSetting } from '../files_sharing/FilesSharingUtils.ts' - -import { basename } from '@nextcloud/paths' -import { triggerActionForFile } from '../files/FilesUtils.ts' -import { createShare } from '../files_sharing/FilesSharingUtils.ts' - -export function uploadThreeVersions(user: User, fileName: string) { - // A new version will not be created if the changes occur - // within less than one second of each other. - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.uploadContent(user, new Blob(['v1'], { type: 'text/plain' }), 'text/plain', `/${fileName}`) - .wait(1100) - .uploadContent(user, new Blob(['v2'], { type: 'text/plain' }), 'text/plain', `/${fileName}`) - .wait(1100) - .uploadContent(user, new Blob(['v3'], { type: 'text/plain' }), 'text/plain', `/${fileName}`) - cy.login(user) -} - -export function openVersionsPanel(fileName: string) { - // Detect the versions list fetch - cy.intercept('PROPFIND', '**/dav/versions/*/versions/**').as('getVersions') - - triggerActionForFile(basename(fileName), 'details') - cy.get('[data-cy-sidebar]') - .as('sidebar') - .should('be.visible') - cy.get('@sidebar') - .find('[aria-controls="tab-files_versions"]') - .click() - - // Wait for the versions list to be fetched - cy.wait('@getVersions') - cy.get('#tab-files_versions').should('be.visible', { timeout: 10000 }) -} - -export function toggleVersionMenu(index: number) { - cy.get('#tab-files_versions [data-files-versions-version]') - .eq(index) - .find('button') - .click() -} - -export function triggerVersionAction(index: number, actionName: string) { - toggleVersionMenu(index) - cy.get(`[data-cy-files-versions-version-action="${actionName}"]`).filter(':visible').click() -} - -export function nameVersion(index: number, name: string) { - cy.intercept('PROPPATCH', '**/dav/versions/*/versions/**').as('labelVersion') - triggerVersionAction(index, 'label') - cy.get(':focused').type(`${name}{enter}`) - cy.wait('@labelVersion') -} - -export function restoreVersion(index: number) { - cy.intercept('MOVE', '**/dav/versions/*/versions/**').as('restoreVersion') - triggerVersionAction(index, 'restore') - cy.wait('@restoreVersion') -} - -export function deleteVersion(index: number) { - cy.intercept('DELETE', '**/dav/versions/*/versions/**').as('deleteVersion') - triggerVersionAction(index, 'delete') - cy.wait('@deleteVersion') -} - -export function doesNotHaveAction(index: number, actionName: string) { - toggleVersionMenu(index) - cy.get(`[data-cy-files-versions-version-action="${actionName}"]`).should('not.exist') - toggleVersionMenu(index) -} - -export function assertVersionContent(index: number, expectedContent: string) { - cy.intercept({ method: 'GET', times: 1, url: 'remote.php/**' }).as('downloadVersion') - triggerVersionAction(index, 'download') - cy.wait('@downloadVersion') - .then(({ response }) => expect(response?.body).to.equal(expectedContent)) -} - -export function setupTestSharedFileFromUser(owner: User, randomFileName: string, shareOptions: Partial) { - return cy.createRandomUser() - .then((recipient) => { - cy.login(owner) - cy.visit('/apps/files') - createShare(randomFileName, recipient.userId, shareOptions) - - cy.login(recipient) - cy.visit('/apps/files') - return cy.wrap(recipient) - }) -} diff --git a/cypress/e2e/files_versions/version_creation.cy.ts b/cypress/e2e/files_versions/version_creation.cy.ts deleted file mode 100644 index f1c578f1f0cd9..0000000000000 --- a/cypress/e2e/files_versions/version_creation.cy.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { randomString } from '../../support/utils/randomString.ts' -import { openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions creation', () => { - let randomFileName = '' - - before(() => { - randomFileName = randomString(10) + '.txt' - - cy.createRandomUser() - .then((user) => { - uploadThreeVersions(user, randomFileName) - cy.login(user) - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - }) - }) - - it('Opens the versions panel and sees the versions', () => { - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').should('have.length', 3) - cy.get('[data-files-versions-version]').eq(0).contains('Current version') - cy.get('[data-files-versions-version]').eq(2).contains('Initial version') - }) - }) - - it('See yourself as version author', () => { - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - - cy.findByRole('tabpanel', { name: 'Versions' }) - .findByRole('list', { name: 'File versions' }) - .findAllByRole('listitem') - .should('have.length', 3) - .first() - .find('[data-cy-files-version-author-name]') - .should('exist') - .and('contain.text', 'You') - }) -}) diff --git a/cypress/e2e/files_versions/version_cross_share_move_and_copy.cy.ts b/cypress/e2e/files_versions/version_cross_share_move_and_copy.cy.ts deleted file mode 100644 index d09b9c6aac933..0000000000000 --- a/cypress/e2e/files_versions/version_cross_share_move_and_copy.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 { randomString } from '../../support/utils/randomString.ts' -import { clickOnBreadcrumbs, closeSidebar, copyFile, moveFile, navigateToFolder } from '../files/FilesUtils.ts' -import { assertVersionContent, nameVersion, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions cross share move and copy', () => { - let randomSharedFolderName = '' - let randomFileName = '' - let randomFilePath = '' - let alice: User - let bob: User - - before(() => { - randomSharedFolderName = randomString(10) - - cy.createRandomUser() - .then((user) => { - alice = user - cy.mkdir(alice, `/${randomSharedFolderName}`) - setupTestSharedFileFromUser(alice, randomSharedFolderName, {}) - }) - .then((user) => { bob = user }) - }) - - beforeEach(() => { - randomFileName = randomString(10) + '.txt' - randomFilePath = `${randomSharedFolderName}/${randomFileName}` - uploadThreeVersions(alice, randomFilePath) - - cy.login(bob) - cy.visit('/apps/files') - navigateToFolder(randomSharedFolderName) - openVersionsPanel(randomFilePath) - nameVersion(2, 'v1') - closeSidebar() - }) - - it('Also moves versions when bob moves the file out of a received share', () => { - moveFile(randomFileName, '/') - assertVersionsContent(randomFileName) - // TODO: move that in assertVersionsContent when copying files keeps the versions' metadata - cy.get('[data-files-versions-version]').eq(2).contains('v1') - }) - - it('Also copies versions when bob copies the file out of a received share', () => { - copyFile(randomFileName, '/') - assertVersionsContent(randomFileName) - }) - - context('When a file is in a subfolder', () => { - let randomSubFolderName - let randomSubSubFolderName - - beforeEach(() => { - randomSubFolderName = randomString(10) - randomSubSubFolderName = randomString(10) - clickOnBreadcrumbs('All files') - cy.mkdir(bob, `/${randomSharedFolderName}/${randomSubFolderName}`) - cy.mkdir(bob, `/${randomSharedFolderName}/${randomSubFolderName}/${randomSubSubFolderName}`) - cy.login(bob) - navigateToFolder(randomSharedFolderName) - moveFile(randomFileName, `${randomSubFolderName}/${randomSubSubFolderName}`) - }) - - it('Also moves versions when bob moves the containing folder out of a received share', () => { - moveFile(randomSubFolderName, '/') - assertVersionsContent(`${randomSubFolderName}/${randomSubSubFolderName}/${randomFileName}`) - // TODO: move that in assertVersionsContent when copying files keeps the versions' metadata - cy.get('[data-files-versions-version]').eq(2).contains('v1') - }) - - it('Also copies versions when bob copies the containing folder out of a received share', () => { - copyFile(randomSubFolderName, '/') - assertVersionsContent(`${randomSubFolderName}/${randomSubSubFolderName}/${randomFileName}`) - }) - }) -}) - -/** - * @param filePath - */ -function assertVersionsContent(filePath: string) { - const path = filePath.split('/').slice(0, -1).join('/') - - clickOnBreadcrumbs('All files') - - if (path !== '') { - navigateToFolder(path) - } - - openVersionsPanel(filePath) - - cy.get('[data-files-versions-version]').should('have.length', 3) - assertVersionContent(0, 'v3') - assertVersionContent(1, 'v2') - assertVersionContent(2, 'v1') -} diff --git a/cypress/e2e/files_versions/version_deletion.cy.ts b/cypress/e2e/files_versions/version_deletion.cy.ts deleted file mode 100644 index c2779d7e8032c..0000000000000 --- a/cypress/e2e/files_versions/version_deletion.cy.ts +++ /dev/null @@ -1,61 +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 { navigateToFolder } from '../files/FilesUtils.ts' -import { deleteVersion, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions deletion', () => { - const folderName = 'shared_folder' - const randomFileName = randomString(10) + '.txt' - const randomFilePath = `/${folderName}/${randomFileName}` - let user: User - let versionCount = 0 - - beforeEach(() => { - cy.createRandomUser() - .then((_user) => { - user = _user - cy.mkdir(user, `/${folderName}`) - uploadThreeVersions(user, randomFilePath) - versionCount = 3 - cy.login(user) - cy.visit('/apps/files') - }) - }) - - it('Delete initial version', () => { - navigateToFolder(folderName) - openVersionsPanel(randomFilePath) - - cy.get('[data-files-versions-version]') - .should('have.length', versionCount) - deleteVersion(--versionCount) - cy.get('[data-files-versions-version]') - .should('have.length', versionCount) - }) - - it('Delete versions of shared file with delete permission', () => { - setupTestSharedFileFromUser(user, folderName, { delete: true }) - navigateToFolder(folderName) - openVersionsPanel(randomFilePath) - - cy.get('[data-files-versions-version]').should('have.length', versionCount) - deleteVersion(--versionCount) - cy.get('[data-files-versions-version]').should('have.length', versionCount) - }) - - it('Delete versions of shared file without delete permission', () => { - setupTestSharedFileFromUser(user, folderName, { delete: false }) - navigateToFolder(folderName) - openVersionsPanel(randomFilePath) - - doesNotHaveAction(0, 'delete') - doesNotHaveAction(1, 'delete') - doesNotHaveAction(2, 'delete') - }) -}) diff --git a/cypress/e2e/files_versions/version_download.cy.ts b/cypress/e2e/files_versions/version_download.cy.ts deleted file mode 100644 index 6aa51a89a1737..0000000000000 --- a/cypress/e2e/files_versions/version_download.cy.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 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 { assertVersionContent, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions download', () => { - let randomFileName = '' - let user: User - - before(() => cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')) - after(() => { - cy.runOccCommand('config:app:delete core shareapi_allow_view_without_download') - }) - - beforeEach(() => { - randomFileName = randomString(10) + '.txt' - - cy.createRandomUser() - .then((_user) => { - user = _user - uploadThreeVersions(user, randomFileName) - }) - }) - - it('Download versions and assert their content', () => { - cy.login(user) - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - - assertVersionContent(0, 'v3') - assertVersionContent(1, 'v2') - assertVersionContent(2, 'v1') - }) - - it('Download versions of shared file with download permission', () => { - setupTestSharedFileFromUser(user, randomFileName, { download: true }) - openVersionsPanel(randomFileName) - - assertVersionContent(0, 'v3') - assertVersionContent(1, 'v2') - assertVersionContent(2, 'v1') - }) - - it('Does not show action without download permission', () => { - setupTestSharedFileFromUser(user, randomFileName, { download: false }) - openVersionsPanel(randomFileName) - - cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist') - cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="download"]').should('not.exist') - - doesNotHaveAction(1, 'download') - doesNotHaveAction(2, 'download') - }) -}) diff --git a/cypress/e2e/files_versions/version_expiration.cy.ts b/cypress/e2e/files_versions/version_expiration.cy.ts deleted file mode 100644 index 2a974052fe340..0000000000000 --- a/cypress/e2e/files_versions/version_expiration.cy.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { randomString } from '../../support/utils/randomString.ts' -import { assertVersionContent, nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions expiration', () => { - let randomFileName = '' - - beforeEach(() => { - randomFileName = randomString(10) + '.txt' - - cy.createRandomUser() - .then((user) => { - uploadThreeVersions(user, randomFileName) - cy.login(user) - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - }) - }) - - it('Expire all versions', () => { - cy.runOccCommand('config:system:set versions_retention_obligation --value \'0, 0\'') - cy.runOccCommand('versions:expire') - cy.runOccCommand('config:system:set versions_retention_obligation --value auto') - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').should('have.length', 1) - cy.get('[data-files-versions-version]').eq(0).contains('Current version') - }) - - assertVersionContent(0, 'v3') - }) - - it('Expire versions v2', () => { - nameVersion(2, 'v1') - - cy.runOccCommand('config:system:set versions_retention_obligation --value \'0, 0\'') - cy.runOccCommand('versions:expire') - cy.runOccCommand('config:system:set versions_retention_obligation --value auto') - cy.visit('/apps/files') - openVersionsPanel(randomFileName) - - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').should('have.length', 2) - cy.get('[data-files-versions-version]').eq(0).contains('Current version') - cy.get('[data-files-versions-version]').eq(1).contains('v1') - }) - - assertVersionContent(0, 'v3') - assertVersionContent(1, 'v1') - }) -}) diff --git a/cypress/e2e/files_versions/version_naming.cy.ts b/cypress/e2e/files_versions/version_naming.cy.ts deleted file mode 100644 index 3d5d80bc3d19e..0000000000000 --- a/cypress/e2e/files_versions/version_naming.cy.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 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 { navigateToFolder } from '../files/FilesUtils.ts' -import { doesNotHaveAction, nameVersion, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions naming', () => { - let randomFileName = '' - let user: User - - beforeEach(() => { - randomFileName = randomString(10) + '.txt' - - cy.createRandomUser() - .then((_user) => { - user = _user - cy.mkdir(_user, '/share') - uploadThreeVersions(user, `share/${randomFileName}`) - }) - }) - - it('Names the versions', () => { - cy.login(user) - cy.visit('/apps/files') - navigateToFolder('share') - openVersionsPanel(randomFileName) - - nameVersion(2, 'v1') - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').eq(2).contains('v1') - cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist') - }) - - nameVersion(1, 'v2') - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').eq(1).contains('v2') - }) - - nameVersion(0, 'v3') - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').eq(0).contains('v3 (Current version)') - }) - }) - - it('Name versions of shared file with edit permission', () => { - setupTestSharedFileFromUser(user, 'share', { update: true }) - - navigateToFolder('share') - openVersionsPanel(randomFileName) - - nameVersion(2, 'v1 - shared') - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').eq(2).contains('v1 - shared') - cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist') - }) - - nameVersion(1, 'v2 - shared') - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').eq(1).contains('v2 - shared') - }) - - nameVersion(0, 'v3 - shared') - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').eq(0).contains('v3 - shared (Current version)') - }) - }) - - it('Name versions without edit permission fails', () => { - setupTestSharedFileFromUser(user, 'share', { update: false }) - - navigateToFolder('share') - openVersionsPanel(randomFileName) - - cy.get('[data-files-versions-version]') - .eq(0) - .as('firstVersion') - .find('.action-item__menutoggle') - .should('not.exist') - cy.get('@firstVersion') - .find('[data-cy-version-action="label"]') - .should('not.exist') - - doesNotHaveAction(1, 'label') - doesNotHaveAction(2, 'label') - }) -}) diff --git a/cypress/e2e/files_versions/version_restoration.cy.ts b/cypress/e2e/files_versions/version_restoration.cy.ts deleted file mode 100644 index 2d7e66068d90a..0000000000000 --- a/cypress/e2e/files_versions/version_restoration.cy.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 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 { navigateToFolder } from '../files/FilesUtils.ts' -import { assertVersionContent, doesNotHaveAction, openVersionsPanel, restoreVersion, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions restoration', () => { - let randomFileName = '' - let user: User - - beforeEach(() => { - randomFileName = randomString(10) + '.txt' - - cy.createRandomUser() - .then((_user) => { - user = _user - cy.mkdir(_user, '/share') - uploadThreeVersions(user, `share/${randomFileName}`) - cy.login(user) - cy.visit('/apps/files') - }) - }) - - it('Restores initial version', () => { - navigateToFolder('share') - openVersionsPanel(randomFileName) - // Current version does not have restore action - doesNotHaveAction(0, 'restore') - restoreVersion(2) - - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').should('have.length', 3) - cy.get('[data-files-versions-version]').eq(0).contains('Current version') - cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist') - }) - - // Downloads versions and assert there content - assertVersionContent(0, 'v1') - assertVersionContent(1, 'v3') - assertVersionContent(2, 'v2') - }) - - it('Restore versions of shared file with update permission', () => { - setupTestSharedFileFromUser(user, 'share', { update: true }) - navigateToFolder('share') - openVersionsPanel(randomFileName) - - restoreVersion(2) - cy.get('#tab-files_versions').within(() => { - cy.get('[data-files-versions-version]').should('have.length', 3) - cy.get('[data-files-versions-version]').eq(0).contains('Current version') - cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist') - }) - assertVersionContent(0, 'v1') - assertVersionContent(1, 'v3') - assertVersionContent(2, 'v2') - }) - - it('Does not show action without delete permission', () => { - setupTestSharedFileFromUser(user, 'share', { update: false }) - navigateToFolder('share') - openVersionsPanel(randomFileName) - - cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist') - cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="restore"]').should('not.exist') - - doesNotHaveAction(2, 'restore') - doesNotHaveAction(1, 'restore') - }) -}) diff --git a/cypress/e2e/files_versions/version_sharing.cy.ts b/cypress/e2e/files_versions/version_sharing.cy.ts deleted file mode 100644 index 4aa762cd28cf1..0000000000000 --- a/cypress/e2e/files_versions/version_sharing.cy.ts +++ /dev/null @@ -1,48 +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 { randomString } from '../../support/utils/randomString.ts' -import { navigateToFolder, triggerActionForFile } from '../files/FilesUtils.ts' -import { setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts' - -describe('Versions on shares', () => { - const randomSharedFolderName = randomString(10) - const randomFileName = randomString(10) + '.txt' - const randomFilePath = `${randomSharedFolderName}/${randomFileName}` - let alice: User - let bob: User - - before(() => { - cy.createRandomUser() - .then((user) => { - alice = user - }) - .then(() => { - cy.mkdir(alice, `/${randomSharedFolderName}`) - return setupTestSharedFileFromUser(alice, randomSharedFolderName, {}) - }) - .then((user) => { bob = user }) - .then(() => uploadThreeVersions(alice, randomFilePath)) - }) - - it('See sharees display name as author', () => { - cy.login(bob) - cy.visit('/apps/files') - - navigateToFolder(randomSharedFolderName) - - triggerActionForFile(randomFileName, 'details') - cy.findByRole('tab', { name: 'Versions' }).click() - - cy.findByRole('tabpanel', { name: 'Versions' }) - .findByRole('list', { name: 'File versions' }) - .findAllByRole('listitem') - .first() - .find('[data-cy-files-version-author-name]') - .should('be.visible') - .and('contain.text', alice.userId) - }) -}) diff --git a/cypress/e2e/login/login-redirect.cy.ts b/cypress/e2e/login/login-redirect.cy.ts deleted file mode 100644 index 0c7db13ad6b83..0000000000000 --- a/cypress/e2e/login/login-redirect.cy.ts +++ /dev/null @@ -1,60 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -/** - * Test that when a session expires / the user logged out in another tab, - * the user gets redirected to the login on the next request. - */ -describe('Logout redirect ', { testIsolation: true }, () => { - let user - - before(() => { - cy.createRandomUser() - .then(($user) => { - user = $user - }) - }) - - it('Redirects to login if session timed out', () => { - // Login and see settings - cy.login(user) - cy.visit('/settings/user#profile') - cy.findByRole('checkbox', { name: /Enable profile/i }) - .should('exist') - - // clear session - cy.clearAllCookies() - - // trigger an request - cy.findByRole('checkbox', { name: /Enable profile/i }) - .click({ force: true }) - - // See that we are redirected - cy.url() - .should('match', /\/login/i) - .and('include', `?redirect_url=${encodeURIComponent('/index.php/settings/user#profile')}`) - - cy.get('form[name="login"]').should('be.visible') - }) - - it('Redirect from login works', () => { - cy.logout() - // visit the login - cy.visit(`/login?redirect_url=${encodeURIComponent('/index.php/settings/user#profile')}`) - - // see login - cy.get('form[name="login"]').should('be.visible') - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(user.userId) - cy.get('input[name="password"]').type(user.password) - cy.contains('button[data-login-form-submit]', 'Log in').click() - }) - - // see that we are correctly redirected - cy.url().should('include', '/index.php/settings/user#profile') - cy.findByRole('checkbox', { name: /Enable profile/i }) - .should('exist') - }) -}) diff --git a/cypress/e2e/login/login.cy.ts b/cypress/e2e/login/login.cy.ts deleted file mode 100644 index 09b871792e465..0000000000000 --- a/cypress/e2e/login/login.cy.ts +++ /dev/null @@ -1,153 +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' - -import { getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts' - -describe('Login', () => { - let user: User - let disabledUser: User - - after(() => cy.deleteUser(user)) - before(() => { - // disable brute force protection - cy.runOccCommand('config:system:set auth.bruteforce.protection.enabled --value false --type bool') - cy.createRandomUser().then(($user) => { - user = $user - }) - cy.createRandomUser().then(($user) => { - disabledUser = $user - cy.runOccCommand(`user:disable '${disabledUser.userId}'`) - }) - }) - - beforeEach(() => { - cy.logout() - }) - - it('log in with valid account and password', () => { - // Given I visit the Home page - cy.visit('/') - // I see the login page - cy.get('form[name="login"]').should('be.visible') - // I log in with a valid account - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(user.userId) - cy.get('input[name="password"]').type(user.password) - cy.contains('button[data-login-form-submit]', 'Log in').click() - }) - - // see that the login is done - cy.get('[data-login-form-submit]').if().should('not.contain', 'Logging in') - - // Then I see that the current page is the Files app - cy.url().should('match', /apps\/dashboard(\/|$)/) - }) - - it('try to log in with valid account and invalid password', () => { - // Given I visit the Home page - cy.visit('/') - // I see the login page - cy.get('form[name="login"]').should('be.visible') - // I log in with a valid account but invalid password - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(user.userId) - cy.get('input[name="password"]').type(`${user.password}--wrong`) - cy.contains('button', 'Log in').click() - }) - - // see that the login is done - cy.get('[data-login-form-submit]').if().should('not.contain', 'Logging in') - - // Then I see that the current page is the Login page - cy.url().should('match', /\/login/) - // And I see that a wrong password message is shown - cy.get('form[name="login"]').then(($el) => expect($el.text()).to.match(/Wrong.+password/i)) - cy.get('input[name="password"]:invalid').should('exist') - }) - - it('try to log in with valid account and invalid password', () => { - // Given I visit the Home page - cy.visit('/') - // I see the login page - cy.get('form[name="login"]').should('be.visible') - // I log in with a valid account but invalid password - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(user.userId) - cy.get('input[name="password"]').type(`${user.password}--wrong`) - cy.contains('button', 'Log in').click() - }) - - // see that the login is done - cy.get('[data-login-form-submit]').if().should('not.contain', 'Logging in') - - // Then I see that the current page is the Login page - cy.url().should('match', /\/login/) - // And I see that a wrong password message is shown - cy.get('form[name="login"]').then(($el) => expect($el.text()).to.match(/Wrong.+password/i).and.to.match(/Wrong.+login/)) - cy.get('input[name="password"]:invalid').should('exist') - }) - - it('try to log in with invalid account', () => { - // Given I visit the Home page - cy.visit('/') - // I see the login page - cy.get('form[name="login"]').should('be.visible') - // I log in with an invalid user but valid password - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(`${user.userId}--wrong`) - cy.get('input[name="password"]').type(user.password) - cy.contains('button', 'Log in').click() - }) - - // see that the login is done - cy.get('[data-login-form-submit]').if().should('not.contain', 'Logging in') - - // Then I see that the current page is the Login page - cy.url().should('match', /\/login/) - // And I see that a wrong password message is shown - cy.get('form[name="login"]').then(($el) => expect($el.text()).to.match(/Wrong.+password/i).and.to.match(/Wrong.+login/)) - cy.get('input[name="password"]:invalid').should('exist') - }) - - it('try to log in as disabled account', () => { - // Given I visit the Home page - cy.visit('/') - // I see the login page - cy.get('form[name="login"]').should('be.visible') - // When I log in with user disabledUser and password - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(disabledUser.userId) - cy.get('input[name="password"]').type(disabledUser.password) - cy.contains('button', 'Log in').click() - }) - - // see that the login is done - cy.get('[data-login-form-submit]').if().should('not.contain', 'Logging in') - - // Then I see that the current page is the Login page - cy.url().should('match', /\/login/) - // And I see that the disabled account message is shown - cy.get('form[name="login"]').then(($el) => expect($el.text()).to.match(/Account.+disabled/i)) - cy.get('input[name="password"]:invalid').should('exist') - }) - - it('try to logout', () => { - cy.login(user) - - // Given I visit the Home page - cy.visit('/') - // I see the dashboard - cy.url().should('match', /apps\/dashboard(\/|$)/) - - // When click logout - getNextcloudUserMenuToggle().should('exist').click() - getNextcloudUserMenu().contains('a', 'Log out').click() - - // Then I see that the current page is the Login page - cy.url().should('match', /\/login/) - }) -}) diff --git a/cypress/e2e/login/webauth.cy.ts b/cypress/e2e/login/webauth.cy.ts deleted file mode 100644 index 4d8d3acc20a60..0000000000000 --- a/cypress/e2e/login/webauth.cy.ts +++ /dev/null @@ -1,152 +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' - -interface IChromeVirtualAuthenticator { - authenticatorId: string -} - -/** - * Create a virtual authenticator using chrome debug protocol - */ -async function createAuthenticator(): Promise { - await Cypress.automation('remote:debugger:protocol', { - command: 'WebAuthn.enable', - }) - const authenticator = await Cypress.automation('remote:debugger:protocol', { - command: 'WebAuthn.addVirtualAuthenticator', - params: { - options: { - protocol: 'ctap2', - ctap2Version: 'ctap2_1', - hasUserVerification: true, - transport: 'usb', - automaticPresenceSimulation: true, - isUserVerified: true, - }, - }, - }) - return authenticator -} - -/** - * Delete a virtual authenticator using chrome devbug protocol - * - * @param authenticator the authenticator object - */ -async function deleteAuthenticator(authenticator: IChromeVirtualAuthenticator) { - await Cypress.automation('remote:debugger:protocol', { - command: 'WebAuthn.removeVirtualAuthenticator', - params: { - ...authenticator, - }, - }) -} - -describe('Login using WebAuthn', () => { - let authenticator: IChromeVirtualAuthenticator - let user: User - - afterEach(() => { - cy.deleteUser(user) - .then(() => deleteAuthenticator(authenticator)) - }) - - beforeEach(() => { - cy.createRandomUser() - .then(($user) => { - user = $user - cy.login(user) - }) - .then(() => createAuthenticator()) - .then(($authenticator) => { - authenticator = $authenticator - cy.log('Created virtual authenticator') - }) - }) - - it('add and delete WebAuthn', () => { - cy.intercept('**/settings/api/personal/webauthn/registration').as('webauthn') - cy.visit('/settings/user/security') - - cy.contains('[role="note"]', /No devices configured/i).should('be.visible') - - cy.findByRole('button', { name: /Add WebAuthn device/i }) - .should('be.visible') - .click() - - cy.wait('@webauthn') - - cy.findByRole('textbox', { name: /Device name/i }) - .should('be.visible') - .type('test device{enter}') - - cy.wait('@webauthn') - - cy.contains('[role="note"]', /No devices configured/i).should('not.exist') - - cy.findByRole('list', { name: /following devices are configured for your account/i }) - .should('be.visible') - .contains('li', 'test device') - .should('be.visible') - .findByRole('button', { name: /Actions/i }) - .click() - - cy.findByRole('menuitem', { name: /Delete/i }) - .should('be.visible') - .click() - - cy.contains('[role="note"]', /No devices configured/i).should('be.visible') - cy.findByRole('list', { name: /following devices are configured for your account/i }) - .should('not.exist') - - cy.reload() - cy.contains('[role="note"]', /No devices configured/i).should('be.visible') - }) - - it('add WebAuthn and login', () => { - cy.intercept('GET', '**/settings/api/personal/webauthn/registration').as('webauthnSetupInit') - cy.intercept('POST', '**/settings/api/personal/webauthn/registration').as('webauthnSetupDone') - cy.intercept('POST', '**/login/webauthn/start').as('webauthnLogin') - - cy.visit('/settings/user/security') - - cy.findByRole('button', { name: /Add WebAuthn device/i }) - .should('be.visible') - .click() - cy.wait('@webauthnSetupInit') - - cy.findByRole('textbox', { name: /Device name/i }) - .should('be.visible') - .type('test device{enter}') - cy.wait('@webauthnSetupDone') - - cy.findByRole('list', { name: /following devices are configured for your account/i }) - .should('be.visible') - .findByText('test device') - .should('be.visible') - - cy.logout() - cy.visit('/login') - - cy.findByRole('button', { name: /Log in with a device/i }) - .should('be.visible') - .click() - - cy.findByRole('form', { name: /Log in with a device/i }) - .should('be.visible') - .findByRole('textbox', { name: /Login or email/i }) - .should('be.visible') - .type(`{selectAll}${user.userId}`) - - cy.findByRole('button', { name: /Log in/i }) - .click() - cy.wait('@webauthnLogin') - - // Then I see that the current page is the Files app - cy.url().should('match', /apps\/dashboard(\/|$)/) - }) -}) diff --git a/cypress/e2e/settings/access-levels.cy.ts b/cypress/e2e/settings/access-levels.cy.ts deleted file mode 100644 index 2a3699e985431..0000000000000 --- a/cypress/e2e/settings/access-levels.cy.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState, getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Settings: Ensure only administrator can see the administration settings section', { testIsolation: true }, () => { - beforeEach(() => { - clearState() - }) - - it('Regular users cannot see admin-level items on the Settings page', () => { - // Given I am logged in - cy.createRandomUser().then(($user) => { - cy.login($user) - cy.visit('/') - }) - - // I open the settings menu - getNextcloudUserMenuToggle().click() - // I navigate to the settings panel - getNextcloudUserMenu() - .findByRole('link', { name: /settings/i }) - .click() - cy.url().should('match', /\/settings\/user$/) - - cy.findAllByRole('navigation') - .filter('#app-navigation-vue') - .as('appNavigation') - .findByRole('list', { name: 'Personal' }) - .should('be.visible') - .findByRole('link', { name: /Personal info/i }) - .should('be.visible') - .and('have.attr', 'aria-current', 'page') - - cy.get('@appNavigation') - .findByRole('list', { name: 'Administration' }) - .should('not.exist') - }) - - it('Admin users can see admin-level items on the Settings page', () => { - // Given I am logged in - cy.login(admin) - cy.visit('/') - - // I open the settings menu - getNextcloudUserMenuToggle().click() - // I navigate to the settings panel - getNextcloudUserMenu() - .findByRole('link', { name: /Personal settings/i }) - .click() - cy.url().should('match', /\/settings\/user$/) - - cy.findAllByRole('navigation') - .filter('#app-navigation-vue') - .as('appNavigation') - .findByRole('list', { name: 'Personal' }) - .should('be.visible') - .findByRole('link', { name: /Personal info/i }) - .should('be.visible') - .and('have.attr', 'aria-current', 'page') - - cy.get('@appNavigation') - .findByRole('list', { name: 'Administration' }) - .should('be.visible') - .findByRole('link', { name: /Overview/i }) - .should('be.visible') - }) -}) diff --git a/cypress/e2e/settings/personal-info.cy.ts b/cypress/e2e/settings/personal-info.cy.ts deleted file mode 100644 index 068af94aec9cb..0000000000000 --- a/cypress/e2e/settings/personal-info.cy.ts +++ /dev/null @@ -1,449 +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 { handlePasswordConfirmation } from '../core-utils.ts' - -let user: User - -enum Visibility { - Private = 'Private', - Local = 'Local', - Federated = 'Federated', - Public = 'Published', -} - -const ALL_VISIBILITIES = [Visibility.Public, Visibility.Private, Visibility.Local, Visibility.Federated] - -/** - * Get the input connected to a specific label - * @param label The content of the label - */ -const inputForLabel = (label: string) => cy.contains('label', label).then((el) => cy.get(`#${el.attr('for')}`)) - -/** - * Get the property visibility button - * @param property The property to which to look for the button - */ -const getVisibilityButton = (property: string) => cy.get(`button[aria-label*="Change scope level of ${property.toLowerCase()}"`) - -/** - * Validate a specifiy visibility is set for a property - * @param property The property - * @param active The active visibility - */ -function validateActiveVisibility(property: string, active: Visibility) { - getVisibilityButton(property) - .should('have.attr', 'aria-label') - .and('match', new RegExp(`current scope is ${active}`, 'i')) - getVisibilityButton(property) - .click() - cy.get('ul[role="menu"]') - .contains('button', active) - .should('have.attr', 'aria-checked', 'true') - - // close menu - getVisibilityButton(property) - .click() -} - -/** - * Set a specific visibility for a property - * @param property The property - * @param active The visibility to set - */ -function setActiveVisibility(property: string, active: Visibility) { - getVisibilityButton(property) - .click() - cy.get('ul[role="menu"]') - .contains('button', active) - .click({ force: true }) - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') -} - -/** - * Helper to check that setting all visibilities on a property is possible - * @param property The property to test - * @param defaultVisibility The default visibility of that property - * @param allowedVisibility Visibility that is allowed and need to be checked - */ -function checkSettingsVisibility(property: string, defaultVisibility: Visibility = Visibility.Local, allowedVisibility: Visibility[] = ALL_VISIBILITIES) { - getVisibilityButton(property) - .scrollIntoView() - - validateActiveVisibility(property, defaultVisibility) - - allowedVisibility.forEach((active) => { - setActiveVisibility(property, active) - - cy.reload() - getVisibilityButton(property).scrollIntoView() - - validateActiveVisibility(property, active) - }) - - // TODO: Fix this in vue library then enable this test again - /* // Test that not allowed options are disabled - ALL_VISIBILITIES.filter((v) => !allowedVisibility.includes(v)).forEach((disabled) => { - getVisibilityButton(property) - .click() - cy.get('ul[role="dialog"') - .contains('button', disabled) - .should('exist') - .and('have.attr', 'disabled', 'true') - }) */ -} - -const genericProperties = [ - ['Location', 'Berlin'], - ['X (formerly Twitter)', 'nextclouders'], - ['Fediverse', 'nextcloud@mastodon.xyz'], -] -const nonfederatedProperties = ['Organisation', 'Role', 'Headline', 'About'] - -describe('Settings: Change personal information', { testIsolation: true }, () => { - let snapshot: string = '' - - before(() => { - // make sure the fediverse check does not do http requests - cy.runOccCommand('config:system:set has_internet_connection --type bool --value false') - // ensure we can set locale and language - cy.runOccCommand('config:system:delete force_language') - cy.runOccCommand('config:system:delete force_locale') - cy.createRandomUser().then(($user) => { - user = $user - cy.modifyUser(user, 'language', 'en') - cy.modifyUser(user, 'locale', 'en_US') - - // Make sure the user is logged in at least once - // before the snapshot is taken to speed up the tests - cy.login(user) - cy.visit('/settings/user') - - cy.saveState().then(($snapshot) => { - snapshot = $snapshot - }) - }) - }) - - after(() => { - cy.runOccCommand('config:system:delete has_internet_connection') - - cy.runOccCommand('config:system:set force_language --value en') - cy.runOccCommand('config:system:set force_locale --value en_US') - }) - - beforeEach(() => { - cy.login(user) - cy.visit('/settings/user') - cy.intercept('PUT', /ocs\/v2.php\/cloud\/users\//).as('submitSetting') - }) - - afterEach(() => { - cy.restoreState(snapshot) - }) - - it('Can dis- and enable the profile', () => { - cy.visit(`/u/${user.userId}`) - cy.contains('h2', user.userId).should('be.visible') - - cy.visit('/settings/user') - cy.contains('Enable profile').click() - handlePasswordConfirmation(user.password) - cy.wait('@submitSetting') - - cy.visit(`/u/${user.userId}`, { failOnStatusCode: false }) - cy.contains('h2', 'Profile not found').should('be.visible') - - cy.visit('/settings/user') - cy.contains('Enable profile').click() - handlePasswordConfirmation(user.password) - cy.wait('@submitSetting') - - cy.visit(`/u/${user.userId}`, { failOnStatusCode: false }) - cy.contains('h2', user.userId).should('be.visible') - }) - - it('Can change language', () => { - cy.intercept('GET', /settings\/user/).as('reload') - inputForLabel('Language').scrollIntoView() - inputForLabel('Language').type('Ned') - cy.contains('li[role="option"]', 'Nederlands') - .click() - cy.wait('@reload') - - // expect language changed - inputForLabel('Taal').scrollIntoView() - cy.contains('section', 'Help met vertalen') - }) - - it('Can change locale', () => { - cy.intercept('GET', /settings\/user/).as('reload') - cy.clock(new Date(2024, 0, 10)) - - // Default is US - cy.contains('section', '01/10/2024') - - inputForLabel('Locale').scrollIntoView() - inputForLabel('Locale').type('German') - cy.contains('li[role="option"]', 'German (Germany') - .click() - cy.wait('@reload') - - // expect locale changed - inputForLabel('Locale').scrollIntoView() - cy.contains('section', '10.01.2024') - }) - - it('Can set primary email and change its visibility', () => { - cy.contains('label', 'Email').scrollIntoView() - // Check invalid input - inputForLabel('Email').type('foo bar') - inputForLabel('Email').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false) - // handle valid input - inputForLabel('Email').type('{selectAll}hello@example.com') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Email').should('have.value', 'hello@example.com') - - checkSettingsVisibility( - 'Email', - Visibility.Federated, - // It is not possible to set it as private - ALL_VISIBILITIES.filter((v) => v !== Visibility.Private), - ) - - // check it is visible on the profile - cy.visit(`/u/${user.userId}`) - cy.contains('a', 'hello@example.com').should('be.visible').and('have.attr', 'href', 'mailto:hello@example.com') - }) - - it('Can delete primary email', () => { - cy.contains('label', 'Email').scrollIntoView() - inputForLabel('Email').type('{selectAll}hello@example.com') - handlePasswordConfirmation(user.password) - cy.wait('@submitSetting') - - // check after reload - cy.reload() - inputForLabel('Email').should('have.value', 'hello@example.com') - - // delete email - cy.get('button[aria-label="Remove primary email"]').click({ force: true }) - cy.wait('@submitSetting') - - // check after reload - cy.reload() - inputForLabel('Email').should('have.value', '') - }) - - it('Can set and delete additional emails', () => { - cy.get('button[aria-label="Add additional email"]').should('be.disabled') - // we need a primary email first - cy.contains('label', 'Email').scrollIntoView() - inputForLabel('Email').type('{selectAll}primary@example.com') - handlePasswordConfirmation(user.password) - cy.wait('@submitSetting') - - // add new email - cy.get('button[aria-label="Add additional email"]') - .click() - - // without any value we should not be able to add a second additional - cy.get('button[aria-label="Add additional email"]').should('be.disabled') - - // fill the first additional - inputForLabel('Additional email address 1') - .type('1@example.com') - handlePasswordConfirmation(user.password) - cy.wait('@submitSetting') - - // add second additional email - cy.get('button[aria-label="Add additional email"]') - .click() - - // fill the second additional - inputForLabel('Additional email address 2') - .type('2@example.com') - handlePasswordConfirmation(user.password) - cy.wait('@submitSetting') - - // check the content is saved - cy.reload() - inputForLabel('Additional email address 1') - .should('have.value', '1@example.com') - inputForLabel('Additional email address 2') - .should('have.value', '2@example.com') - - // delete the first - cy.get('button[aria-label="Options for additional email address 1"]') - .click({ force: true }) - cy.contains('button[role="menuitem"]', 'Delete email') - .click({ force: true }) - handlePasswordConfirmation(user.password) - - cy.reload() - inputForLabel('Additional email address 1') - .should('have.value', '2@example.com') - }) - - it('Can set Full name and change its visibility', () => { - cy.contains('label', 'Full name').scrollIntoView() - // handle valid input - inputForLabel('Full name').type('{selectAll}Jane Doe') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Full name').should('have.value', 'Jane Doe') - - checkSettingsVisibility( - 'Full name', - Visibility.Federated, - // It is not possible to set it as private - ALL_VISIBILITIES.filter((v) => v !== Visibility.Private), - ) - - // check it is visible on the profile - cy.visit(`/u/${user.userId}`) - cy.contains('h2', 'Jane Doe').should('be.visible') - }) - - it('Can set Phone number and its visibility', () => { - cy.contains('label', 'Phone number').scrollIntoView() - // Check invalid input - inputForLabel('Phone number').type('foo bar') - inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error') - // handle valid input - inputForLabel('Phone number').type('{selectAll}+49 89 721010 99701') - inputForLabel('Phone number').should('have.attr', 'class').and('not.contain', '--error') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Phone number').should('have.value', '+498972101099701') - - checkSettingsVisibility('Phone number') - - // check it is visible on the profile - cy.visit(`/u/${user.userId}`) - cy.get('a[href="tel:+498972101099701"]').should('be.visible') - }) - - it('Can set phone number with phone region', () => { - cy.contains('label', 'Phone number').scrollIntoView() - inputForLabel('Phone number').type('{selectAll}0 40 428990') - inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error') - - cy.runOccCommand('config:system:set default_phone_region --value DE') - cy.reload() - - cy.contains('label', 'Phone number').scrollIntoView() - inputForLabel('Phone number').type('{selectAll}0 40 428990') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Phone number').should('have.value', '+4940428990') - }) - - it('Can reset phone number', () => { - cy.contains('label', 'Phone number').scrollIntoView() - inputForLabel('Phone number').type('{selectAll}+49 40 428990') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Phone number').should('have.value', '+4940428990') - - inputForLabel('Phone number').clear() - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Phone number').should('have.value', '') - }) - - it('Can reset social media property', () => { - cy.contains('label', 'Fediverse').scrollIntoView() - inputForLabel('Fediverse').type('{selectAll}@nextcloud@mastodon.social') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Fediverse').should('have.value', 'nextcloud@mastodon.social') - - inputForLabel('Fediverse').clear() - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Fediverse').should('have.value', '') - }) - - it('Can set Website and change its visibility', () => { - cy.contains('label', 'Website').scrollIntoView() - // Check invalid input - inputForLabel('Website').type('foo bar') - inputForLabel('Website').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false) - // handle valid input - inputForLabel('Website').type('{selectAll}http://example.com') - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel('Website').should('have.value', 'http://example.com') - - checkSettingsVisibility('Website') - - // check it is visible on the profile - cy.visit(`/u/${user.userId}`) - cy.contains('http://example.com').should('be.visible') - }) - - // Check generic properties that allow any visibility and any value - genericProperties.forEach(([property, value]) => { - it(`Can set ${property} and change its visibility`, () => { - cy.contains('label', property).scrollIntoView() - inputForLabel(property).type(value) - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel(property).should('have.value', value) - - checkSettingsVisibility(property) - - // check it is visible on the profile - cy.visit(`/u/${user.userId}`) - cy.contains(value).should('be.visible') - }) - }) - - // Check non federated properties - those where we need special configuration and only support local visibility - nonfederatedProperties.forEach((property) => { - it(`Can set ${property} and change its visibility`, () => { - const uniqueValue = `${property.toUpperCase()} ${property.toLowerCase()}` - cy.contains('label', property).scrollIntoView() - inputForLabel(property).type(uniqueValue) - handlePasswordConfirmation(user.password) - - cy.wait('@submitSetting') - cy.reload() - inputForLabel(property).should('have.value', uniqueValue) - - checkSettingsVisibility(property, Visibility.Local, [Visibility.Private, Visibility.Local]) - - // check it is visible on the profile - cy.visit(`/u/${user.userId}`) - cy.contains(uniqueValue).should('be.visible') - }) - }) -}) diff --git a/cypress/e2e/settings/users-group-admin.cy.ts b/cypress/e2e/settings/users-group-admin.cy.ts deleted file mode 100644 index 68dddac2e053f..0000000000000 --- a/cypress/e2e/settings/users-group-admin.cy.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { randomString } from '../../support/utils/randomString.ts' -import { handlePasswordConfirmation } from '../core-utils.ts' -import { getUserListRow } from './usersUtils.ts' - -const admin = new User('admin', 'admin') -const john = new User('john', '123456') - -/** - * Make a user subadmin of a group. - * - * @param user - The user to make subadmin - * @param group - The group the user should be subadmin of - */ -function makeSubAdmin(user: User, group: string): void { - cy.request({ - url: `${Cypress.config('baseUrl')!.replace('/index.php', '')}/ocs/v2.php/cloud/users/${user.userId}/subadmins`, - method: 'POST', - auth: { - user: admin.userId, - password: admin.userId, - }, - headers: { - 'OCS-ApiRequest': 'true', - }, - body: { - groupid: group, - }, - }) -} - -describe('Settings: Create accounts as a group admin', function() { - let subadmin: User - let group: string - - beforeEach(() => { - group = randomString(7) - cy.deleteUser(john) - cy.createRandomUser().then((user) => { - subadmin = user - cy.runOccCommand(`group:add '${group}'`) - cy.runOccCommand(`group:adduser '${group}' '${subadmin.userId}'`) - makeSubAdmin(subadmin, group) - }) - }) - - it('Can create a user with prefilled single group', () => { - cy.login(subadmin) - // open the User settings - cy.visit('/settings/users') - - // open the New user modal - cy.get('button#new-user-button').click() - - cy.get('form[data-test="form"]').within(() => { - // see that the correct group is preselected - cy.contains('[data-test="groups"] .vs__selected', group).should('be.visible') - // see that the username is "" - cy.get('input[data-test="username"]').should('exist').and('have.value', '') - // set the username to john - cy.get('input[data-test="username"]').type(john.userId) - // see that the username is john - cy.get('input[data-test="username"]').should('have.value', john.userId) - // see that the password is "" - cy.get('input[type="password"]').should('exist').and('have.value', '') - // set the password to 123456 - cy.get('input[type="password"]').type(john.password) - // see that the password is 123456 - cy.get('input[type="password"]').should('have.value', john.password) - }) - - cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { - // submit the new user form - cy.get('button[type="submit"]').click({ force: true }) - }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - - // see that the created user is in the list - getUserListRow(john.userId) - // see that the list of users contains the user john - .contains(john.userId).should('exist') - }) - - // Skiping as this crash the webengine in the CI - it.skip('Can create a new user when member of multiple groups', () => { - const group2 = randomString(7) - cy.runOccCommand(`group:add '${group2}'`) - cy.runOccCommand(`group:adduser '${group2}' '${subadmin.userId}'`) - makeSubAdmin(subadmin, group2) - - cy.login(subadmin) - // open the User settings - cy.visit('/settings/users') - - // open the New user modal - cy.get('button#new-user-button').click() - - cy.get('form[data-test="form"]').within(() => { - // see that no group is pre-selected - cy.get('[data-test="groups"] .vs__selected').should('not.exist') - // see both groups are available - cy.findByRole('combobox', { name: /member of the following groups/i }) - .should('be.visible') - .click() - // can select both groups - cy.document().its('body') - .findByRole('listbox', { name: 'Options' }) - .should('be.visible') - .as('options') - .findAllByRole('option') - .should('have.length', 2) - .get('@options') - .findByRole('option', { name: group }) - .should('be.visible') - .get('@options') - .findByRole('option', { name: group2 }) - .should('be.visible') - .click() - // see group is selected - cy.contains('[data-test="groups"] .vs__selected', group2).should('be.visible') - - // see that the username is "" - cy.get('input[data-test="username"]').should('exist').and('have.value', '') - // set the username to john - cy.get('input[data-test="username"]').type(john.userId) - // see that the username is john - cy.get('input[data-test="username"]').should('have.value', john.userId) - // see that the password is "" - cy.get('input[type="password"]').should('exist').and('have.value', '') - // set the password to 123456 - cy.get('input[type="password"]').type(john.password) - // see that the password is 123456 - cy.get('input[type="password"]').should('have.value', john.password) - }) - - cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { - // submit the new user form - cy.get('button[type="submit"]').click({ force: true }) - }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - - // see that the created user is in the list - getUserListRow(john.userId) - // see that the list of users contains the user john - .contains(john.userId).should('exist') - }) - - it.skip('Only sees groups they are subadmin of', () => { - const group2 = randomString(7) - cy.runOccCommand(`group:add '${group2}'`) - cy.runOccCommand(`group:adduser '${group2}' '${subadmin.userId}'`) - // not a subadmin! - - cy.login(subadmin) - // open the User settings - cy.visit('/settings/users') - - // open the New user modal - cy.get('button#new-user-button').click() - - cy.get('form[data-test="form"]').within(() => { - // see that the subadmin group is pre-selected - cy.contains('[data-test="groups"] .vs__selected', group).should('be.visible') - // see only the subadmin group is available - cy.findByRole('combobox', { name: /member of the following groups/i }) - .should('be.visible') - .click() - // can select both groups - cy.document().its('body') - .findByRole('listbox', { name: 'Options' }) - .should('be.visible') - .as('options') - .findAllByRole('option') - .should('have.length', 1) - }) - }) -}) diff --git a/cypress/e2e/settings/users.cy.ts b/cypress/e2e/settings/users.cy.ts deleted file mode 100644 index 615455bc30282..0000000000000 --- a/cypress/e2e/settings/users.cy.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -/// - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { handlePasswordConfirmation } from '../core-utils.ts' -import { getUserListRow } from './usersUtils.ts' - -const admin = new User('admin', 'admin') -const john = new User('john', '123456') - -describe('Settings: Create and delete accounts', function() { - beforeEach(function() { - cy.listUsers().then((users) => { - if ((users as string[]).includes(john.userId)) { - // ensure created user is deleted - cy.deleteUser(john) - } - }) - cy.login(admin) - // open the User settings - cy.visit('/settings/users') - }) - - it('Can create a user', function() { - // open the New user modal - cy.get('button#new-user-button').click() - - cy.get('form[data-test="form"]').within(() => { - // see that the username is "" - cy.get('input[data-test="username"]').should('exist').and('have.value', '') - // set the username to john - cy.get('input[data-test="username"]').type(john.userId) - // see that the username is john - cy.get('input[data-test="username"]').should('have.value', john.userId) - // see that the password is "" - cy.get('input[type="password"]').should('exist').and('have.value', '') - // set the password to 123456 - cy.get('input[type="password"]').type(john.password) - // see that the password is 123456 - cy.get('input[type="password"]').should('have.value', john.password) - }) - - cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { - // submit the new user form - cy.get('button[type="submit"]').click({ force: true }) - }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - - // see that the created user is in the list - getUserListRow(john.userId) - // see that the list of users contains the user john - .contains(john.userId).should('exist') - }) - - it('Can create a user with additional field data', function() { - // open the New user modal - cy.get('button#new-user-button').click() - - cy.get('form[data-test="form"]').within(() => { - // set the username - cy.get('input[data-test="username"]').should('exist').and('have.value', '') - cy.get('input[data-test="username"]').type(john.userId) - cy.get('input[data-test="username"]').should('have.value', john.userId) - // set the display name - cy.get('input[data-test="displayName"]').should('exist').and('have.value', '') - cy.get('input[data-test="displayName"]').type('John Smith') - cy.get('input[data-test="displayName"]').should('have.value', 'John Smith') - // set the email - cy.get('input[data-test="email"]').should('exist').and('have.value', '') - cy.get('input[data-test="email"]').type('john@example.org') - cy.get('input[data-test="email"]').should('have.value', 'john@example.org') - // set the password - cy.get('input[type="password"]').should('exist').and('have.value', '') - cy.get('input[type="password"]').type(john.password) - cy.get('input[type="password"]').should('have.value', john.password) - }) - - cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { - // submit the new user form - cy.get('button[type="submit"]').click({ force: true }) - }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - - // see that the created user is in the list - getUserListRow(john.userId) - // see that the list of users contains the user john - .contains(john.userId) - .should('exist') - }) - - it('Can delete a user', function() { - let testUser - // create user - cy.createRandomUser() - .then(($user) => { - testUser = $user - }) - cy.login(admin) - // ensure created user is present - cy.reload().then(() => { - // see that the user is in the list - getUserListRow(testUser.userId).within(() => { - // see that the list of users contains the user testUser - cy.contains(testUser.userId).should('exist') - // open the actions menu for the user - cy.get('[data-cy-user-list-cell-actions]') - .find('button.action-item__menutoggle') - .click({ force: true }) - }) - - // The "Delete account" action in the actions menu is shown and clicked - cy.get('.action-item__popper .action').contains('Delete account').should('exist').click({ force: true }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - - // And confirmation dialog accepted - cy.get('.nc-generic-dialog button').contains(`Delete ${testUser.userId}`).click({ force: true }) - - // deleted clicked the user is not shown anymore - getUserListRow(testUser.userId).should('not.exist') - }) - }) -}) 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/settings/users_columns.cy.ts b/cypress/e2e/settings/users_columns.cy.ts deleted file mode 100644 index ad7db65c4f8c1..0000000000000 --- a/cypress/e2e/settings/users_columns.cy.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { assertNotExistOrNotVisible, getUserList } from './usersUtils.js' - -const admin = new User('admin', 'admin') - -describe('Settings: Show and hide columns', function() { - before(function() { - cy.login(admin) - // open the User settings - cy.visit('/settings/users') - }) - - beforeEach(function() { - // open the settings dialog - cy.contains('button', 'Account management settings').click() - // reset all visibility toggles - cy.get('.modal-container #settings-section_visibility-settings input[type="checkbox"]').uncheck({ force: true }) - - cy.contains('.modal-container', 'Account management settings').within(() => { - // enable the last login toggle - cy.get('[data-test="showLastLogin"] input[type="checkbox"]').check({ force: true }) - // close the settings dialog - cy.get('button.modal-container__close').click() - }) - cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el))) - }) - - it('Can show a column', function() { - // see that the language column is not in the header - cy.get('[data-cy-user-list-header-languages]').should('not.exist') - - // see that the language column is not in all user rows - cy.get('tbody.user-list__body tr').each(($row) => { - cy.wrap($row).get('[data-test="language"]').should('not.exist') - }) - - // open the settings dialog - cy.contains('button', 'Account management settings').click() - - cy.contains('.modal-container', 'Account management settings').within(() => { - // enable the language toggle - cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('not.be.checked') - cy.get('[data-test="showLanguages"] input[type="checkbox"]').check({ force: true }) - cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('be.checked') - // close the settings dialog - cy.get('button.modal-container__close').click() - }) - cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el))) - - // see that the language column is in the header - cy.get('[data-cy-user-list-header-languages]').should('exist') - - // see that the language column is in all user rows - getUserList().find('tbody tr').each(($row) => { - cy.wrap($row).get('[data-cy-user-list-cell-language]').should('exist') - }) - - // Clear local storage and reload to verify user settings DB persistence - cy.clearLocalStorage() - cy.reload() - cy.get('[data-cy-user-list-header-languages]').should('exist') - }) - - it('Can hide a column', function() { - // see that the last login column is in the header - cy.get('[data-cy-user-list-header-last-login]').should('exist') - - // see that the last login column is in all user rows - getUserList().find('tbody tr').each(($row) => { - cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('exist') - }) - - // open the settings dialog - cy.contains('button', 'Account management settings').click() - - cy.contains('.modal-container', 'Account management settings').within(() => { - // disable the last login toggle - cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('be.checked') - cy.get('[data-test="showLastLogin"] input[type="checkbox"]').uncheck({ force: true }) - cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('not.be.checked') - // close the settings dialog - cy.get('button.modal-container__close').click() - }) - cy.waitUntil(() => cy.contains('.modal-container', 'Account management settings').should((el) => assertNotExistOrNotVisible(el))) - - // see that the last login column is not in the header - cy.get('[data-cy-user-list-header-last-login]').should('not.exist') - - // see that the last login column is not in all user rows - getUserList().find('tbody tr').each(($row) => { - cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('not.exist') - }) - }) -}) diff --git a/cypress/e2e/settings/users_disable.cy.ts b/cypress/e2e/settings/users_disable.cy.ts deleted file mode 100644 index 23b0397aa9908..0000000000000 --- a/cypress/e2e/settings/users_disable.cy.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState } from '../../support/commonUtils.ts' -import { getUserListRow } from './usersUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Settings: Disable and enable users', function() { - let testUser: User - - beforeEach(function() { - clearState() - cy.createRandomUser().then(($user) => { - testUser = $user - }) - cy.login(admin) - // open the User settings - cy.visit('/settings/users') - }) - - // Not guranteed to run but would be nice to cleanup - after(() => { - cy.deleteUser(testUser) - }) - - it('Can disable the user', function() { - // ensure user is enabled - cy.enableUser(testUser) - - // see that the user is in the list of active users - getUserListRow(testUser.userId).within(() => { - // see that the list of users contains the user testUser - cy.contains(testUser.userId).should('exist') - // open the actions menu for the user - cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' }) - }) - - // The "Disable account" action in the actions menu is shown and clicked - cy.get('.action-item__popper .action').contains('Disable account').should('exist').click() - // When clicked the section is not shown anymore - getUserListRow(testUser.userId).should('not.exist') - // But the disabled user section now exists - cy.get('#disabled').should('exist') - // Open disabled users section - cy.get('#disabled a').click() - cy.url().should('match', /\/disabled/) - // The list of disabled users should now contain the user - getUserListRow(testUser.userId).should('exist') - }) - - it('Can enable the user', function() { - // ensure user is disabled - cy.enableUser(testUser, false).reload() - - // Open disabled users section - cy.get('#disabled a').click() - cy.url().should('match', /\/disabled/) - - // see that the user is in the list of active users - getUserListRow(testUser.userId).within(() => { - // see that the list of disabled users contains the user testUser - cy.contains(testUser.userId).should('exist') - // open the actions menu for the user - cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' }) - }) - - // The "Enable account" action in the actions menu is shown and clicked - cy.get('.action-item__popper .action').contains('Enable account').should('exist').click() - // When clicked the section is not shown anymore - cy.get('#disabled').should('not.exist') - // Make sure it is still gone after the reload reload - cy.reload().login(admin) - cy.get('#disabled').should('not.exist') - }) -}) diff --git a/cypress/e2e/settings/users_groups.cy.ts b/cypress/e2e/settings/users_groups.cy.ts deleted file mode 100644 index b8738115a0e07..0000000000000 --- a/cypress/e2e/settings/users_groups.cy.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState } from '../../support/commonUtils.ts' -import { randomString } from '../../support/utils/randomString.ts' -import { handlePasswordConfirmation } from '../core-utils.ts' -import { assertNotExistOrNotVisible, getUserListRow, openEditDialog, saveEditDialog } from './usersUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Settings: Create groups', () => { - let groupName: string - - after(() => { - cy.runOccCommand(`group:delete '${groupName!}'`) - }) - - before(() => { - cy.login(admin) - cy.visit('/settings/users') - }) - - it('Can create a group', () => { - cy.intercept('POST', '**/ocs/v2.php/cloud/groups').as('createGroups') - - groupName = randomString(7) - // open the Create group menu - cy.get('button[aria-label="Create group"]').click() - - cy.get('li[data-cy-users-settings-new-group-name]').within(() => { - // see that the group name is "" - cy.get('input').should('exist').and('have.value', '') - // set the group name to foo - cy.get('input').type(groupName) - // see that the group name is foo - cy.get('input').should('have.value', groupName) - // submit the group name - cy.get('input ~ button').click() - }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - cy.wait('@createGroups').its('response.statusCode').should('eq', 200) - - // see that the created group is in the list - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - // see that the list of groups contains the group foo - cy.contains(groupName).should('exist') - }) - }) -}) - -describe('Settings: Assign user to a group', { testIsolation: false }, () => { - const groupName = randomString(7) - let testUser: User - - after(() => { - cy.deleteUser(testUser) - cy.runOccCommand(`group:delete '${groupName}'`) - }) - - before(() => { - clearState() - - cy.createRandomUser().then((user) => { - testUser = user - }) - cy.runOccCommand(`group:add '${groupName}'`) - cy.login(admin) - cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups') - cy.visit('/settings/users') - cy.wait('@loadGroups') - }) - - it('see that the group is in the list', () => { - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .should('exist') - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .find('.counter-bubble__counter') - .should('not.exist') // is hidden when 0 - }) - - it('see that the user is in the list', () => { - getUserListRow(testUser.userId) - .contains(testUser.userId) - .should('exist') - .scrollIntoView() - }) - - it('assign the group via the edit dialog', () => { - openEditDialog(testUser) - - // Type part of the group name in the groups NcSelect - cy.get('.edit-dialog [data-test="form"]').within(() => { - cy.get('[data-test="groups"] input[type="search"]').click({ force: true }) - cy.get('[data-test="groups"] input[type="search"]').type(groupName.slice(0, 5)) - }) - - // Select the group from the floating dropdown - cy.get('.vs__dropdown-menu').should('be.visible') - .contains('li', groupName).click({ force: true }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - }) - - it('see the group was successfully assigned', () => { - // see a new member - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .find('.counter-bubble__counter') - .should('contain', '1') - }) - - it('validate the user was added on backend', () => { - cy.runOccCommand(`user:info --output=json '${testUser.userId}'`).then((output) => { - cy.wrap(output.exitCode).should('eq', 0) - cy.wrap(JSON.parse(output.stdout)?.groups).should('include', groupName) - }) - }) -}) - -describe('Settings: Delete an empty group', { testIsolation: false }, () => { - const groupName = randomString(7) - - after(() => { - cy.runOccCommand(`group:delete '${groupName}'`, { failOnNonZeroExit: false }) - }) - before(() => { - cy.runOccCommand(`group:add '${groupName}'`) - cy.login(admin) - cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups') - cy.visit('/settings/users') - cy.wait('@loadGroups') - }) - - it('see that the group is in the list', () => { - // see that the list of groups contains the group foo - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .should('exist') - .scrollIntoView() - // open the actions menu for the group - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .find('button.action-item__menutoggle') - .click({ force: true }) - }) - - it('can delete the group', () => { - // The "Delete group" action in the actions menu is shown and clicked - cy.get('.action-item__popper button').contains('Delete group').should('exist').click({ force: true }) - // And confirmation dialog accepted - cy.get('.modal-container button').contains('Confirm').click({ force: true }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - }) - - it('deleted group is not shown anymore', () => { - // see that the list of groups does not contain the group - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]') - .find('li') - .not('.app-navigation-caption') - .should('not.exist') - // and also not in database - cy.runOccCommand('group:list --output=json').then(($response) => { - const groups: string[] = Object.keys(JSON.parse($response.stdout)) - expect(groups).to.not.include(groupName) - }) - }) -}) - -describe('Settings: Delete a non empty group', () => { - let testUser: User - const groupName = randomString(7) - - after(() => { - cy.runOccCommand(`group:delete '${groupName}'`, { failOnNonZeroExit: false }) - }) - - before(() => { - cy.runOccCommand(`group:add '${groupName}'`) - cy.createRandomUser().then(($user) => { - testUser = $user - cy.runOccCommand(`group:addUser '${groupName}' '${$user.userId}'`) - }) - cy.login(admin) - cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups') - cy.visit('/settings/users') - cy.wait('@loadGroups') - }) - after(() => cy.deleteUser(testUser)) - - it('see that the group is in the list', () => { - // see that the list of groups contains the group - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .should('exist') - .scrollIntoView() - }) - - it('can delete the group', () => { - // open the menu - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) - .find('button.action-item__menutoggle') - .click({ force: true }) - - // The "Delete group" action in the actions menu is shown and clicked - cy.get('.action-item__popper button').contains('Delete group').should('exist').click({ force: true }) - // And confirmation dialog accepted - cy.get('.modal-container button').contains('Confirm').click({ force: true }) - - // Make sure no confirmation modal is shown - handlePasswordConfirmation(admin.password) - }) - - it('deleted group is not shown anymore', () => { - // see that the list of groups does not contain the group foo - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]') - .find('li') - .not('.app-navigation-caption') - .should('not.exist') - // and also not in database - cy.runOccCommand('group:list --output=json').then(($response) => { - const groups: string[] = Object.keys(JSON.parse($response.stdout)) - expect(groups).to.not.include(groupName) - }) - }) -}) - -describe('Settings: Sort groups in the UI', () => { - before(() => { - // Clear state - clearState() - - // Add two groups and add one user to group B - cy.runOccCommand('group:add A') - cy.runOccCommand('group:add B') - cy.createRandomUser().then((user) => { - cy.runOccCommand(`group:adduser B '${user.userId}'`) - }) - - // Visit the settings as admin - cy.login(admin) - cy.visit('/settings/users') - }) - - it('Can set sort by member count', () => { - // open the settings dialog - cy.contains('button', 'Account management settings').click() - - cy.contains('.modal-container', 'Account management settings').within(() => { - cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').scrollIntoView() - cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').check({ force: true }) - // close the settings dialog - cy.get('button.modal-container__close').click() - }) - cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el))) - }) - - it('See that the groups are sorted by the member count', () => { - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member - cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members - }) - }) - - it('See that the order is preserved after a reload', () => { - cy.reload() - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member - cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members - }) - }) - - it('Can set sort by group name', () => { - // open the settings dialog - cy.contains('button', 'Account management settings').click() - - cy.contains('.modal-container', 'Account management settings').within(() => { - cy.get('[data-test="sortGroupsByName"] input[type="radio"]').scrollIntoView() - cy.get('[data-test="sortGroupsByName"] input[type="radio"]').check({ force: true }) - // close the settings dialog - cy.get('button.modal-container__close').click() - }) - cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el))) - }) - - it('See that the groups are sorted by the user count', () => { - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A') - cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B') - }) - }) - - it('See that the order is preserved after a reload', () => { - cy.reload() - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A') - cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B') - }) - }) -}) diff --git a/cypress/e2e/settings/users_manager.cy.ts b/cypress/e2e/settings/users_manager.cy.ts deleted file mode 100644 index 68b453b1c9b9d..0000000000000 --- a/cypress/e2e/settings/users_manager.cy.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState } from '../../support/commonUtils.ts' -import { handlePasswordConfirmation } from '../core-utils.ts' -import { openEditDialog, saveEditDialog } from './usersUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Settings: User Manager Management', function() { - let user: User - let manager: User - - beforeEach(function() { - clearState() - cy.createRandomUser().then(($user) => { - manager = $user - return cy.createRandomUser() - }).then(($user) => { - user = $user - cy.login(admin) - }) - }) - - it('Can assign a manager through the edit dialog', function() { - cy.visit('/settings/users') - - openEditDialog(user) - - // Open the Manager NcSelect and type manager name - cy.get('.edit-dialog [data-test="form"]').within(() => { - cy.findByRole('combobox', { name: /Manager/i }).click({ force: true }) - cy.findByRole('combobox', { name: /Manager/i }).type(manager.userId) - }) - - // Select the manager from the floating dropdown - cy.get('.vs__dropdown-menu').should('be.visible') - .contains('li', manager.userId).click({ force: true }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.getUserData(user).then(($result) => { - expect($result.body).to.contain(`${manager.userId}`) - }) - }) - - it('Can remove a manager through the edit dialog', function() { - // Set manager via backend first. - // User::getManagerUids() decodes this with JSON_THROW_ON_ERROR, so we - // must store a JSON array, matching what setManagerUids() writes. - // Double-quotes are escaped because runOccCommand passes the command - // through `bash -c "..."`, which would otherwise eat them. - cy.runOccCommand(`user:setting '${user.userId}' settings manager '[\\"${manager.userId}\\"]'`) - - cy.visit('/settings/users') - - openEditDialog(user) - - // Clear the manager selection inside the dialog - cy.get('.edit-dialog [data-test="form"]').within(() => { - cy.get('.user-form__managers .vs__clear').click({ force: true }) - }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.getUserData(user).then(($result) => { - expect($result.body).to.not.contain(`${manager.userId}`) - expect($result.body).to.contain('') - }) - }) -}) diff --git a/cypress/e2e/settings/users_modify.cy.ts b/cypress/e2e/settings/users_modify.cy.ts deleted file mode 100644 index 7a3147124d5b0..0000000000000 --- a/cypress/e2e/settings/users_modify.cy.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState } from '../../support/commonUtils.ts' -import { handlePasswordConfirmation } from '../core-utils.ts' -import { openEditDialog, saveEditDialog } from './usersUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Settings: Change user properties', function() { - let user: User - - beforeEach(function() { - clearState() - cy.createRandomUser().then(($user) => { - user = $user - }) - cy.login(admin) - }) - - it('Can change the display name', function() { - cy.visit('/settings/users') - - openEditDialog(user) - - cy.get('.edit-dialog [data-test="form"]').within(() => { - cy.get('input[data-test="displayName"]').should('have.value', user.userId) - cy.get('input[data-test="displayName"]').clear() - cy.get('input[data-test="displayName"]').type('John Doe') - cy.get('input[data-test="displayName"]').should('have.value', 'John Doe') - }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => { - expect($result.exitCode).to.equal(0) - const info = JSON.parse($result.stdout) - expect(info?.display_name).to.equal('John Doe') - }) - }) - - it('Can change the password', function() { - cy.visit('/settings/users') - - openEditDialog(user) - - cy.get('.edit-dialog [data-test="form"]').within(() => { - cy.get('input[data-test="password"]').should('have.value', '') - cy.get('input[data-test="password"]').type('newpassword123') - }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify by logging in with the new password - cy.login(new User(user.userId, 'newpassword123')) - cy.visit('/apps/dashboard') - cy.url().should('include', '/apps/dashboard') - }) - - it('Can change the email address', function() { - cy.visit('/settings/users') - - openEditDialog(user) - - cy.get('.edit-dialog [data-test="form"]').within(() => { - cy.get('input[data-test="email"]').should('have.value', '') - cy.get('input[data-test="email"]').type('mymail@example.com') - cy.get('input[data-test="email"]').should('have.value', 'mymail@example.com') - }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => { - expect($result.exitCode).to.equal(0) - const info = JSON.parse($result.stdout) - expect(info?.email).to.equal('mymail@example.com') - }) - }) - - it('Can change the user quota to a predefined one', function() { - cy.visit('/settings/users') - - openEditDialog(user) - - cy.get('.edit-dialog [data-test="form"]').within(() => { - // Open the quota selector - cy.get('.vs__selected').contains('Unlimited').should('exist') - cy.findByRole('combobox', { name: /Quota/i }).click({ force: true }) - }) - - // Dropdown is floating outside the form — select 5 GB - cy.get('.vs__dropdown-menu').should('be.visible') - .contains('li', '5 GB').click({ force: true }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => { - expect($result.exitCode).to.equal(0) - const info = JSON.parse($result.stdout) - expect(info?.quota).to.equal('5 GB') - }) - }) - - it('Can change the user quota to a custom value', function() { - cy.visit('/settings/users') - - openEditDialog(user) - - cy.get('.edit-dialog [data-test="form"]').within(() => { - // Type a custom quota value - cy.findByRole('combobox', { name: /Quota/i }).type('4 MB{enter}') - }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => { - expect($result.exitCode).to.equal(0) - // Quota value is stored as bytes, verify it was set - const info = JSON.parse($result.stdout) - expect(info?.quota).to.not.equal('none') - }) - }) - - it('Can make user a subadmin of a group', function() { - const groupName = 'userstestgroup' - cy.runOccCommand(`group:add '${groupName}'`) - - cy.visit('/settings/users') - - openEditDialog(user) - - cy.get('.edit-dialog [data-test="form"]').within(() => { - // Find the subadmin NcSelect by its label and open the dropdown - cy.findByRole('combobox', { name: /Admin of the following groups/i }).click({ force: true }) - cy.findByRole('combobox', { name: /Admin of the following groups/i }).type('userstestgroup') - }) - - // Select the group from the floating dropdown - cy.get('.vs__dropdown-menu').should('be.visible') - .contains('li', groupName).click({ force: true }) - - handlePasswordConfirmation(admin.password) - saveEditDialog() - - cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist') - - // Verify backend - cy.getUserData(user).then(($response) => { - expect($response.status).to.equal(200) - const dom = (new DOMParser()).parseFromString($response.body, 'text/xml') - expect(dom.querySelector('subadmin element')?.textContent).to.contain(groupName) - }) - }) -}) diff --git a/cypress/e2e/settings/users_search.cy.ts b/cypress/e2e/settings/users_search.cy.ts deleted file mode 100644 index 3c2bbc5f57a92..0000000000000 --- a/cypress/e2e/settings/users_search.cy.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -/// - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { clearState } from '../../support/commonUtils.ts' -import { randomString } from '../../support/utils/randomString.ts' -import { getUserList, getUserListRow } from './usersUtils.ts' - -const admin = new User('admin', 'admin') - -/** Scope role queries to the account management sidebar so they don't match - * unrelated elements (e.g. the global unified search bar at the top of the page). - */ -function accountNav() { - return cy.findByRole('navigation', { name: /account management/i }) -} - -function waitForSearchRequest(alias: string, expectedSearch: string) { - return cy.wait(alias).then(({ request }) => { - expect(new URL(request.url).searchParams.get('search')).to.equal(expectedSearch) - }) -} - -describe('Settings: Unified search for accounts and groups', { testIsolation: false }, () => { - // Use a stable, searchable prefix in the group name so we can match - // it independently from the random user id below. - const matchingGroup = `zzz-match-${randomString(5)}` - const otherGroup = `aaa-other-${randomString(5)}` - let alice: User - let bob: User - - after(() => { - cy.deleteUser(alice) - cy.deleteUser(bob) - cy.runOccCommand(`group:delete '${matchingGroup}'`, { failOnNonZeroExit: false }) - cy.runOccCommand(`group:delete '${otherGroup}'`, { failOnNonZeroExit: false }) - }) - - before(() => { - clearState() - - cy.createRandomUser().then((user) => { - alice = user - }) - cy.createRandomUser().then((user) => { - bob = user - }) - - cy.runOccCommand(`group:add '${matchingGroup}'`) - cy.runOccCommand(`group:add '${otherGroup}'`) - - cy.login(admin) - cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=*').as('initialLoadGroups') - cy.intercept('GET', '**/ocs/v2.php/cloud/users/details?*').as('initialLoadUsers') - cy.visit('/settings/users') - cy.wait('@initialLoadGroups') - cy.wait('@initialLoadUsers') - }) - - beforeEach(() => { - // Intercept aliases reset between tests even with testIsolation: false, - // so re-register them here to capture requests triggered inside each test. - cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=*').as('loadGroups') - cy.intercept('GET', '**/ocs/v2.php/cloud/users/details?*').as('loadUsers') - }) - - it('shows the search input in the navigation sidebar', () => { - accountNav().findByRole('searchbox', { name: /search accounts and groups/i }) - .should('be.visible') - .and('have.value', '') - }) - - it('dispatches the query to both the users and groups API', () => { - accountNav().findByRole('searchbox', { name: /search accounts and groups/i }) - .type(alice.userId) - - // A single keystroke sequence debounces once (300ms), then fans out - // to both APIs — both requests must carry the same search term. - cy.wait('@loadUsers').its('request.url').should('include', `search=${alice.userId}`) - cy.wait('@loadGroups').its('request.url').should('include', `search=${alice.userId}`) - - // The user list reflects what the backend returned for this query. - getUserListRow(alice.userId).should('exist') - getUserList().should('not.contain', bob.userId) - }) - - it('filters the group list when the query matches a group name', () => { - accountNav().findByRole('searchbox', { name: /search accounts and groups/i }) - .clear() - .type(matchingGroup) - - cy.wait('@loadGroups').its('request.url').should('include', `search=${matchingGroup}`) - - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]') - .should('contain', matchingGroup) - .and('not.contain', otherGroup) - }) - - it('resets both lists when the clear button is clicked', () => { - accountNav().findByRole('button', { name: /clear search/i }).click() - - accountNav().findByRole('searchbox', { name: /search accounts and groups/i }) - .should('have.value', '') - - waitForSearchRequest('@loadUsers', '') - waitForSearchRequest('@loadGroups', '') - - getUserListRow(alice.userId).should('exist') - getUserListRow(bob.userId).should('exist') - cy.get('ul[data-cy-users-settings-navigation-groups="custom"]') - .should('contain', matchingGroup) - .and('contain', otherGroup) - }) -}) diff --git a/cypress/e2e/systemtags/admin-settings.cy.ts b/cypress/e2e/systemtags/admin-settings.cy.ts deleted file mode 100644 index b066bbbc409ee..0000000000000 --- a/cypress/e2e/systemtags/admin-settings.cy.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' - -const admin = new User('admin', 'admin') - -const tagName = 'foo' -const updatedTagName = 'bar' - -function openTagSelector() { - cy.findByRole('combobox', { name: 'Search for a tag to edit' }).click() -} - -describe('Create system tags', () => { - before(() => { - // delete any existing tags - cy.runOccCommand('tag:list --output=json').then((output) => { - Object.keys(JSON.parse(output.stdout)).forEach((id) => { - cy.runOccCommand(`tag:delete ${id}`) - }) - }) - - // login as admin and go to admin settings - cy.login(admin) - cy.visit('/settings/admin') - }) - - it('Can create a tag', () => { - cy.intercept('POST', '/remote.php/dav/systemtags').as('createTag') - cy.get('input#system-tag-name').should('exist').and('have.value', '') - cy.get('input#system-tag-name').type(tagName) - cy.get('input#system-tag-name').should('have.value', tagName) - // submit the form - cy.get('input#system-tag-name').type('{enter}') - - // wait for the tag to be created - cy.wait('@createTag').its('response.statusCode').should('eq', 201) - - // see that the created tag is in the list - openTagSelector() - cy.findByRole('option', { name: tagName }).should('be.visible') - }) -}) - -describe('Update system tags', { testIsolation: false }, () => { - before(() => { - cy.login(admin) - cy.visit('/settings/admin') - }) - - it('select the tag', () => { - openTagSelector() - cy.findByRole('option', { name: tagName }).click() - // see that the tag name matches the selected tag - cy.get('input#system-tag-name').should('exist').and('have.value', tagName) - // see that the tag level matches the selected tag - cy.get('.system-tag-form__group:has(#system-tag-level) .vs__selected') - .should('contain.text', 'Public') - }) - - it('update the tag name and level', () => { - cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*').as('updateTag') - cy.get('input#system-tag-name').clear() - cy.get('input#system-tag-name').type(updatedTagName) - cy.get('input#system-tag-name').should('have.value', updatedTagName) - // select the new tag level - cy.get('#system-tag-level').click() - cy.findByRole('option', { name: 'Invisible' }).click() - // submit the form - cy.get('input#system-tag-name').type('{enter}') - // wait for the tag to be updated - cy.wait('@updateTag').its('response.statusCode').should('eq', 207) - }) - - it('see the tag was successfully updated', () => { - openTagSelector() - // NcEllipsisedOption splits long names across spans, breaking the accessible name, - // so match on the rendered text content instead of the option's accessible name. - cy.contains('[role="option"]', `${updatedTagName} (invisible)`).should('be.visible') - }) -}) - -describe('Delete system tags', { testIsolation: false }, () => { - before(() => { - cy.login(admin) - cy.visit('/settings/admin') - }) - - it('select the tag', () => { - // select the tag to edit - openTagSelector() - cy.contains('[role="option"]', `${updatedTagName} (invisible)`).click() - // see that the tag name matches the selected tag - cy.get('input#system-tag-name').should('exist').and('have.value', updatedTagName) - // see that the tag level matches the selected tag - cy.get('.system-tag-form__group:has(#system-tag-level) .vs__selected') - .should('contain.text', 'Invisible') - }) - - it('can delete the tag', () => { - cy.intercept('DELETE', '/remote.php/dav/systemtags/*').as('deleteTag') - cy.get('.system-tag-form__row').within(() => { - cy.contains('button', 'Delete').should('be.enabled').click() - }) - // wait for the tag to be deleted - cy.wait('@deleteTag').its('response.statusCode').should('eq', 204) - }) - - it('see that the deleted tag is not present', () => { - openTagSelector() - cy.findByRole('option', { name: updatedTagName }).should('not.exist') - }) -}) diff --git a/cypress/e2e/systemtags/files-bulk-action.cy.ts b/cypress/e2e/systemtags/files-bulk-action.cy.ts deleted file mode 100644 index 491b551a09bc4..0000000000000 --- a/cypress/e2e/systemtags/files-bulk-action.cy.ts +++ /dev/null @@ -1,468 +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 { randomBytes } from 'crypto' -import { getRowForFile, selectAllFiles, selectRowForFile, triggerSelectionAction } from '../files/FilesUtils.ts' -import { createShare } from '../files_sharing/FilesSharingUtils.ts' - -let tags = {} as Record -const files = [ - 'file1.txt', - 'file2.txt', - 'file3.txt', - 'file4.txt', - 'file5.txt', -] - -describe('Systemtags: Files bulk action', { testIsolation: false }, () => { - let user1: User - let user2: User - - before(() => { - cy.createRandomUser().then((_user1) => { - user1 = _user1 - cy.createRandomUser().then((_user2) => { - user2 = _user2 - }) - - files.forEach((file) => { - cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file) - }) - }) - - resetTags() - }) - - after(() => { - resetTags() - cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 0') - }) - - it('Can assign tag to selection', () => { - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectRowForFile('file2.txt') - selectRowForFile('file4.txt') - - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - cy.get('[data-cy-systemtags-picker-tag-color]').should('have.length', 5) - - cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData') - cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData') - - const tag = Object.keys(tags)[3]! - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData') - cy.wait('@assignTagData') - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file2.txt', [tag]) - expectInlineTagForFile('file4.txt', [tag]) - }) - - it('Can assign multiple tags to selection', () => { - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectAllFiles() - - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - cy.get('[data-cy-systemtags-picker-tag-color]').should('have.length', 5) - - cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData') - cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData') - - const prevTag = Object.keys(tags)[3]! - const tag1 = Object.keys(tags)[1]! - const tag2 = Object.keys(tags)[2]! - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData') - cy.wait('@assignTagData') - cy.get('@getTagData.all').should('have.length', 2) - cy.get('@assignTagData.all').should('have.length', 2) - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file1.txt', [tag1, tag2]) - expectInlineTagForFile('file2.txt', [prevTag, tag1, tag2]) - expectInlineTagForFile('file3.txt', [tag1, tag2]) - expectInlineTagForFile('file4.txt', [prevTag, tag1, tag2]) - expectInlineTagForFile('file5.txt', [tag1, tag2]) - }) - - it('Can remove tag from selection', () => { - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectRowForFile('file1.txt') - selectRowForFile('file3.txt') - selectRowForFile('file4.txt') - - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - - cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData') - cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData') - - const firstTag = Object.keys(tags)[3]! - const tag1 = Object.keys(tags)[1]! - const tag2 = Object.keys(tags)[2]! - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData') - cy.wait('@assignTagData') - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file1.txt', [tag1]) - expectInlineTagForFile('file2.txt', [firstTag, tag1, tag2]) - expectInlineTagForFile('file3.txt', [tag1]) - expectInlineTagForFile('file4.txt', [firstTag, tag1]) - expectInlineTagForFile('file5.txt', [tag1, tag2]) - }) - - it('Can remove multiple tags from selection', () => { - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectAllFiles() - - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - - 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-tag] input:indeterminate').should('exist') - .click({ force: true, multiple: true }) - // indeterminate became checked - cy.get('[data-cy-systemtags-picker-tag] input:checked').should('exist') - .click({ force: true, multiple: true }) - // now all are unchecked - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData') - cy.wait('@assignTagData') - cy.get('@getTagData.all').should('have.length', 3) - cy.get('@assignTagData.all').should('have.length', 3) - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file1.txt', []) - expectInlineTagForFile('file2.txt', []) - expectInlineTagForFile('file3.txt', []) - expectInlineTagForFile('file4.txt', []) - expectInlineTagForFile('file5.txt', []) - }) - - it('Can assign and remove multiple tags as a secondary user', () => { - // Create new users - cy.createRandomUser().then((_user1) => { - user1 = _user1 - cy.createRandomUser().then((_user2) => { - user2 = _user2 - }) - - files.forEach((file) => { - cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file) - }) - }) - - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectAllFiles() - - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - - cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData1') - cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData1') - - const tag1 = Object.keys(tags)[0]! - const tag2 = Object.keys(tags)[3]! - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData1') - cy.wait('@assignTagData1') - cy.get('@getTagData1.all').should('have.length', 2) - cy.get('@assignTagData1.all').should('have.length', 2) - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file1.txt', [tag1, tag2]) - expectInlineTagForFile('file2.txt', [tag1, tag2]) - expectInlineTagForFile('file3.txt', [tag1, tag2]) - expectInlineTagForFile('file4.txt', [tag1, tag2]) - expectInlineTagForFile('file5.txt', [tag1, tag2]) - - createShare('file1.txt', user2.userId) - createShare('file3.txt', user2.userId) - - cy.login(user2) - cy.visit('/apps/files') - - getRowForFile('file1.txt').should('be.visible') - getRowForFile('file3.txt').should('be.visible') - - expectInlineTagForFile('file1.txt', [tag1, tag2]) - expectInlineTagForFile('file3.txt', [tag1, tag2]) - - selectRowForFile('file1.txt') - selectRowForFile('file3.txt') - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - - cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData2') - cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData2') - - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData2') - cy.wait('@assignTagData2') - cy.get('@getTagData2.all').should('have.length', 2) - cy.get('@assignTagData2.all').should('have.length', 2) - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file1.txt', []) - expectInlineTagForFile('file3.txt', []) - - cy.login(user1) - cy.visit('/apps/files') - - expectInlineTagForFile('file1.txt', []) - expectInlineTagForFile('file3.txt', []) - }) - - it('Can create tag and assign files to it', () => { - cy.createRandomUser().then((user1) => { - files.forEach((file) => { - cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file) - }) - - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectAllFiles() - - triggerTagManagementDialogAction() - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5) - - 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') - - const newTag = randomBytes(8).toString('base64').slice(0, 6) - 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') - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 6) - // 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('@getTagData') - cy.wait('@assignTagData') - cy.get('@getTagData.all').should('have.length', 1) - cy.get('@assignTagData.all').should('have.length', 1) - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - expectInlineTagForFile('file1.txt', [newTag]) - expectInlineTagForFile('file2.txt', [newTag]) - expectInlineTagForFile('file3.txt', [newTag]) - expectInlineTagForFile('file4.txt', [newTag]) - expectInlineTagForFile('file5.txt', [newTag]) - }) - }) - - it('Cannot create tag if restriction is in place', () => { - let tagId: string - - cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 1') - cy.runOccCommand('tag:add testTag public --output json').then(({ stdout }) => { - const tag = JSON.parse(stdout) - tagId = tag.id - }) - - cy.createRandomUser().then((user1) => { - files.forEach((file) => { - cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file) - }) - - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectAllFiles() - - triggerTagManagementDialogAction() - - cy.findByRole('textbox', { name: 'Search or create tag' }).should('not.exist') - cy.findByRole('textbox', { name: 'Search tag' }).should('be.visible') - - cy.get('[data-cy-systemtags-picker-input]').type('testTag') - - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 1) - cy.get('[data-cy-systemtags-picker-button-create]').should('not.exist') - cy.get('[data-cy-systemtags-picker-tag-color]').should('not.exist') - - // Assign the tag - 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-tag="${tagId}"]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData') - cy.wait('@assignTagData') - - cy.get('[data-cy-systemtags-picker]').should('not.exist') - - // Finally, reset the restriction - cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 0') - }) - }) - - it('Can search for tags with insensitive case', () => { - let tagId: string - resetTags() - - cy.runOccCommand('tag:add TESTTAG public --output json').then(({ stdout }) => { - const tag = JSON.parse(stdout) - tagId = tag.id - }) - - cy.createRandomUser().then((user1) => { - files.forEach((file) => { - cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file) - }) - - cy.login(user1) - cy.visit('/apps/files') - - files.forEach((file) => { - getRowForFile(file).should('be.visible') - }) - selectAllFiles() - - triggerTagManagementDialogAction() - - cy.findByRole('textbox', { name: 'Search or create tag' }).should('be.visible') - cy.findByRole('textbox', { name: 'Search tag' }).should('not.exist') - - cy.get('[data-cy-systemtags-picker-input]').type('testtag') - - cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 1) - cy.get(`[data-cy-systemtags-picker-tag="${tagId}"]`).should('be.visible') - .findByRole('checkbox').should('not.be.checked') - - // Assign the tag - 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-tag="${tagId}"]`).should('be.visible') - .findByRole('checkbox').click({ force: true }) - cy.get('[data-cy-systemtags-picker-button-submit]').click() - - cy.wait('@getTagData') - cy.wait('@assignTagData') - - expectInlineTagForFile('file1.txt', ['TESTTAG']) - expectInlineTagForFile('file2.txt', ['TESTTAG']) - expectInlineTagForFile('file3.txt', ['TESTTAG']) - expectInlineTagForFile('file4.txt', ['TESTTAG']) - expectInlineTagForFile('file5.txt', ['TESTTAG']) - - cy.get('[data-cy-systemtags-picker]').should('not.exist') - }) - }) -}) - -function resetTags() { - tags = {} - for (let i = 0; i < 5; i++) { - tags[randomBytes(8).toString('base64').slice(0, 6)] = 0 - } - - // delete any existing tags - cy.runOccCommand('tag:list --output=json').then((output) => { - Object.keys(JSON.parse(output.stdout)).forEach((id) => { - cy.runOccCommand(`tag:delete ${id}`) - }) - }) - - // create tags - Object.keys(tags).forEach((tag) => { - cy.runOccCommand(`tag:add ${tag} public --output=json`).then((output) => { - tags[tag] = JSON.parse(output.stdout).id as number - }) - }) - cy.log('Using tags', tags) -} - -function expectInlineTagForFile(file: string, tags: string[]) { - getRowForFile(file) - .find('[data-systemtags-fileid]') - .findAllByRole('listitem') - .should('have.length', tags.length) - .each((tag) => { - expect(tag.text()).to.be.oneOf(tags) - }) -} - -function triggerTagManagementDialogAction() { - cy.intercept('PROPFIND', '/remote.php/dav/systemtags/').as('getTagsList') - triggerSelectionAction('systemtags:bulk') - cy.wait('@getTagsList') - cy.get('[data-cy-systemtags-picker]').should('be.visible') -} diff --git a/cypress/e2e/systemtags/files-inline-action.cy.ts b/cypress/e2e/systemtags/files-inline-action.cy.ts deleted file mode 100644 index a3225671a23b8..0000000000000 --- a/cypress/e2e/systemtags/files-inline-action.cy.ts +++ /dev/null @@ -1,78 +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 { randomBytes } from 'crypto' -import { getRowForFile } from '../files/FilesUtils.ts' -import { addTagToFile } from './utils.ts' - -describe('Systemtags: Files integration', { testIsolation: true }, () => { - let user: User - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.mkdir(user, '/folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.login(user) - cy.visit('/apps/files') - })) - - it('See first assigned tag in the file list', () => { - const tag = randomBytes(8).toString('base64') - addTagToFile('file.txt', tag) - cy.reload() - - getRowForFile('file.txt') - .findByRole('list', { name: /collaborative tags/i }) - .findByRole('listitem') - .should('be.visible') - .and('contain.text', tag) - }) - - it('See two assigned tags are also shown in the file list', () => { - const tag1 = randomBytes(5).toString('base64') - const tag2 = randomBytes(5).toString('base64') - addTagToFile('file.txt', tag1) - addTagToFile('file.txt', tag2) - cy.reload() - - getRowForFile('file.txt') - .findByRole('list', { name: /collaborative tags/i }) - .children() - .should('have.length', 2) - .should('contain.text', tag1) - .should('contain.text', tag2) - }) - - it('See three assigned tags result in overflow entry', () => { - const tag1 = randomBytes(4).toString('base64') - const tag2 = randomBytes(4).toString('base64') - const tag3 = randomBytes(4).toString('base64') - addTagToFile('file.txt', tag1) - addTagToFile('file.txt', tag2) - addTagToFile('file.txt', tag3) - cy.reload() - - getRowForFile('file.txt') - .findByRole('list', { name: /collaborative tags/i }) - .children() - .then(($children) => { - expect($children.length).to.eq(4) - expect($children.get(0)).be.visible - expect($children.get(1)).be.visible - // not visible - just for accessibility - expect($children.get(2)).not.be.visible - expect($children.get(3)).not.be.visible - // Text content - expect($children.get(1)).contain.text('+2') - // Remove the '+x' element - const elements = [$children.get(0), ...$children.get().slice(2)] - .map((el) => el.innerText.trim()) - expect(elements).to.have.members([tag1, tag2, tag3]) - }) - }) -}) diff --git a/cypress/e2e/systemtags/files-sidebar.cy.ts b/cypress/e2e/systemtags/files-sidebar.cy.ts deleted file mode 100644 index fd9fd797e17ac..0000000000000 --- a/cypress/e2e/systemtags/files-sidebar.cy.ts +++ /dev/null @@ -1,41 +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 { randomBytes } from 'crypto' -import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts' -import { createNewTagInDialog } from './utils.ts' - -describe('Systemtags: Files sidebar integration', { testIsolation: true }, () => { - let user: User - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.mkdir(user, '/folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.login(user) - })) - - it('Can assign tags using the sidebar', () => { - const tag = randomBytes(8).toString('base64') - cy.visit('/apps/files') - - getRowForFile('file.txt').should('be.visible') - triggerActionForFile('file.txt', 'details') - - cy.get('[data-cy-sidebar]') - .should('be.visible') - .findByRole('button', { name: 'Actions' }) - .should('be.visible') - .click() - - cy.findByRole('menuitem', { name: 'Add tags' }) - .click() - - createNewTagInDialog(tag) - }) -}) diff --git a/cypress/e2e/systemtags/files-view.cy.ts b/cypress/e2e/systemtags/files-view.cy.ts deleted file mode 100644 index 46b724fcde540..0000000000000 --- a/cypress/e2e/systemtags/files-view.cy.ts +++ /dev/null @@ -1,42 +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 { randomBytes } from 'crypto' -import { getRowForFile } from '../files/FilesUtils.ts' -import { addTagToFile } from './utils.ts' - -describe('Systemtags: Files view', { testIsolation: true }, () => { - let user: User - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.mkdir(user, '/folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.login(user) - cy.visit('/apps/files') - })) - - it('See first assigned tag in the file list', () => { - const tag = randomBytes(8).toString('base64') - addTagToFile('folder', tag) - - // open the tags view - cy.visit('/apps/files/tags').then(() => { - // see the tag - getRowForFile('folder').should('not.exist') - getRowForFile('file.txt').should('not.exist') - cy.findByRole('cell', { name: tag }) - .should('be.visible') - .click() - - // see that the tag has its content - getRowForFile('folder').should('be.visible') - getRowForFile('file.txt').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/a11y-color-contrast.cy.ts b/cypress/e2e/theming/a11y-color-contrast.cy.ts deleted file mode 100644 index 53796707b1cad..0000000000000 --- a/cypress/e2e/theming/a11y-color-contrast.cy.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -const themesToTest = ['light', 'dark', 'light-highcontrast', 'dark-highcontrast'] - -const testCases = { - 'Main text': { - foregroundColors: [ - 'color-main-text', - // 'color-text-light', deprecated - // 'color-text-lighter', deprecated - 'color-text-maxcontrast', - ], - backgroundColors: [ - 'color-main-background', - 'color-background-hover', - 'color-background-dark', - // 'color-background-darker', this should only be used for elements not for text - ], - }, - 'blurred background': { - foregroundColors: [ - 'color-main-text', - 'color-text-maxcontrast-blur', - ], - backgroundColors: [ - 'color-main-background-blur', - ], - }, - Primary: { - foregroundColors: [ - 'color-primary-text', - ], - backgroundColors: [ - // 'color-primary-default', this should only be used for elements not for text! - // 'color-primary-hover', this should only be used for elements and not for text! - 'color-primary', - ], - }, - 'Primary light': { - foregroundColors: [ - 'color-primary-light-text', - ], - backgroundColors: [ - 'color-primary-light', - 'color-primary-light-hover', - ], - }, - 'Primary element': { - foregroundColors: [ - 'color-primary-element-text', - 'color-primary-element-text-dark', - ], - backgroundColors: [ - 'color-primary-element', - 'color-primary-element-hover', - ], - }, - 'Primary element light': { - foregroundColors: [ - 'color-primary-element-light-text', - ], - backgroundColors: [ - 'color-primary-element-light', - 'color-primary-element-light-hover', - ], - }, - 'Severity information texts': { - foregroundColors: [ - 'color-error-text', - 'color-warning-text', - 'color-success-text', - 'color-info-text', - ], - backgroundColors: [ - 'color-main-background', - 'color-background-hover', - ], - }, - // only most important severity colors are supported on the blur - 'Severity information on blur': { - foregroundColors: [ - 'color-error-text', - 'color-success-text', - ], - backgroundColors: [ - 'color-main-background-blur', - ], - }, -} - -/** - * Create a wrapper element with color and background set - * - * @param foreground The foreground color (css variable without leading --) - * @param background The background color - */ -function createTestCase(foreground: string, background: string) { - const wrapper = document.createElement('div') - wrapper.style.padding = '14px' - wrapper.style.color = `var(--${foreground})` - wrapper.style.backgroundColor = `var(--${background})` - if (background.includes('blur')) { - wrapper.style.backdropFilter = 'var(--filter-background-blur)' - } - - const testCase = document.createElement('div') - testCase.innerText = `${foreground} ${background}` - testCase.setAttribute('data-cy-testcase', '') - - wrapper.appendChild(testCase) - return wrapper -} - -describe('Accessibility of Nextcloud theming colors', () => { - for (const theme of themesToTest) { - context(`Theme: ${theme}`, () => { - before(() => { - cy.createRandomUser().then(($user) => { - // set user theme - cy.runOccCommand(`user:setting -- '${$user.userId}' theming enabled-themes '[\\"${theme}\\"]'`) - cy.login($user) - cy.visit('/') - cy.injectAxe({ axeCorePath: 'node_modules/axe-core/axe.min.js' }) - }) - }) - - beforeEach(() => { - cy.document().then((doc) => { - // Unset background image and thus use background-color for testing blur background (images do not work with axe-core) - doc.body.style.backgroundImage = 'unset' - - const root = doc.querySelector('#content') - if (root === null) { - throw new Error('No test root found') - } - root.innerHTML = '' - }) - }) - - for (const [name, { backgroundColors, foregroundColors }] of Object.entries(testCases)) { - context(`Accessibility of CSS color variables for ${name}`, () => { - for (const foreground of foregroundColors) { - for (const background of backgroundColors) { - it(`color contrast of ${foreground} on ${background}`, () => { - cy.document().then((doc) => { - const element = createTestCase(foreground, background) - const root = doc.querySelector('#content') - - expect(root).not.to.be.undefined - - root!.appendChild(element) - - cy.checkA11y('[data-cy-testcase]', { - runOnly: ['color-contrast'], - }) - }) - }) - } - } - }) - } - }) - } -}) diff --git a/cypress/e2e/theming/admin-settings_background.cy.ts b/cypress/e2e/theming/admin-settings_background.cy.ts deleted file mode 100644 index 52c27aaed8a76..0000000000000 --- a/cypress/e2e/theming/admin-settings_background.cy.ts +++ /dev/null @@ -1,385 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { NavigationHeader } from '../../pages/NavigationHeader.ts' -import { - defaultBackground, - defaultPrimary, - pickColor, - validateBodyThemingCss, - validateUserThemingDefaultCss, -} from './themingUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Remove the default background and restore it', { testIsolation: false }, function() { - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Remove the default background', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') - cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded') - - cy.findByRole('checkbox', { name: /remove background image/i }) - .should('exist') - .should('not.be.checked') - .check({ force: true }) - - cy.wait('@removeBackground') - cy.wait('@cssLoaded') - - cy.window() - .should(() => validateBodyThemingCss(defaultPrimary, null)) - cy.waitUntil(() => cy.window().then((win) => { - const backgroundPlain = getComputedStyle(win.document.body).getPropertyValue('--image-background') - return backgroundPlain !== '' - })) - }) - - it('Screenshot the login page and validate login page', function() { - cy.logout() - cy.visit('/') - - cy.window() - .should(() => validateBodyThemingCss(defaultPrimary, null)) - cy.screenshot() - }) - - it('Undo theming settings and validate login page again', function() { - cy.resetAdminTheming() - cy.visit('/') - - cy.window() - .should(() => validateBodyThemingCss()) - cy.screenshot() - }) -}) - -describe('Remove the default background with a custom background color', function() { - let selectedColor = '' - - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Change the background color', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') - cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded') - - pickColor(cy.findByRole('button', { name: /Background color/ })) - .then((color) => { - selectedColor = color - }) - - cy.wait('@setColor') - cy.wait('@cssLoaded') - - cy.window() - .should(() => validateBodyThemingCss( - defaultPrimary, - defaultBackground, - selectedColor, - )) - }) - - it('Remove the default background', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') - - cy.findByRole('checkbox', { name: /remove background image/i }) - .should('exist') - .should('not.be.checked') - .check({ force: true }) - cy.wait('@removeBackground') - }) - - it('Screenshot the login page and validate login page', function() { - cy.logout() - cy.visit('/') - - cy.window() - .should(() => validateBodyThemingCss(defaultPrimary, null, selectedColor)) - cy.screenshot() - }) - - it('Undo theming settings and validate login page again', function() { - cy.resetAdminTheming() - cy.visit('/') - - cy.window() - .should(() => validateBodyThemingCss()) - cy.screenshot() - }) -}) - -describe('Remove the default background with a bright color', function() { - const navigationHeader = new NavigationHeader() - let selectedColor = '' - - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.resetUserTheming(admin) - cy.login(admin) - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Remove the default background', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') - cy.findByRole('checkbox', { name: /remove background image/i }) - .check({ force: true }) - cy.wait('@removeBackground') - }) - - it('Change the background color', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') - cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded') - - pickColor(cy.findByRole('button', { name: /Background color/ }), 4) - .then((color) => { - selectedColor = color - }) - - cy.wait('@setColor') - cy.wait('@cssLoaded') - - cy.window() - .should(() => validateBodyThemingCss(defaultPrimary, null, selectedColor)) - }) - - it('See the header being inverted', function() { - // Probe the Nextcloud logo: it carries the same - // `var(--background-image-invert-if-bright)` filter and is always - // present in the header. The waffle launcher's current-app icon only - // renders when an app is active, which isn't the case on settings, - // and the in-popover tiles use a fixed brightness/invert filter - // regardless of theme so they're not a valid inversion probe. - cy.waitUntil(() => navigationHeader - .logo() - .find('.logo') - .then((el) => { - let ret = true - el.each(function() { - ret = ret && window.getComputedStyle(this).filter === 'invert(1)' - }) - return ret - })) - }) -}) - -describe('Disable user theming and enable it back', function() { - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Disable user background theming', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('disableUserTheming') - - cy.findByRole('checkbox', { name: /Disable user theming/ }) - .should('exist') - .and('not.be.checked') - .check({ force: true }) - - cy.wait('@disableUserTheming') - }) - - it('Login as user', function() { - cy.logout() - cy.createRandomUser().then((user) => { - cy.login(user) - }) - }) - - it('User cannot not change background settings', function() { - cy.visit('/settings/user/theming') - cy.contains('Customization has been disabled by your administrator').should('exist') - }) -}) - -describe('The user default background settings reflect the admin theming settings', function() { - let selectedColor = '' - - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - after(function() { - cy.resetAdminTheming() - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Change the default background', function() { - cy.intercept('*/apps/theming/ajax/uploadImage').as('setBackground') - cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded') - - cy.fixture('image.jpg', null).as('background') - cy.get('input[type="file"][name="background"]') - .should('exist') - .selectFile('@background', { force: true }) - - cy.wait('@setBackground') - cy.wait('@cssLoaded') - - cy.window() - .should(() => validateBodyThemingCss( - defaultPrimary, - '/apps/theming/image/background?v=', - null, - )) - }) - - it('Change the background color', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') - cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded') - - pickColor(cy.findByRole('button', { name: /Background color/ })) - .then((color) => { - selectedColor = color - }) - - cy.wait('@setColor') - cy.wait('@cssLoaded') - - cy.window() - .should(() => validateBodyThemingCss( - defaultPrimary, - '/apps/theming/image/background?v=', - selectedColor, - )) - }) - - it('Login page should match admin theming settings', function() { - cy.logout() - cy.visit('/') - - cy.window() - .should(() => validateBodyThemingCss( - defaultPrimary, - '/apps/theming/image/background?v=', - selectedColor, - )) - }) - - it('Login as user', function() { - cy.createRandomUser().then((user) => { - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .scrollIntoView() - }) - - it('Default user background settings should match admin theming settings', function() { - cy.findByRole('button', { name: 'Default background' }) - .should('exist') - .and('have.attr', 'aria-pressed', 'true') - - cy.window() - .should(() => validateUserThemingDefaultCss( - selectedColor, - '/apps/theming/image/background?v=', - )) - }) -}) - -describe('The user default background settings reflect the admin theming settings with background removed', function() { - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - after(function() { - cy.resetAdminTheming() - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Remove the default background', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') - cy.findByRole('checkbox', { name: /remove background image/i }) - .check({ force: true }) - cy.wait('@removeBackground') - }) - - it('Login page should match admin theming settings', function() { - cy.logout() - cy.visit('/') - - cy.window() - .should(() => validateBodyThemingCss(defaultPrimary, null)) - }) - - it('Login as user', function() { - cy.createRandomUser().then((user) => { - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .scrollIntoView() - }) - - it('Default user background settings should match admin theming settings', function() { - cy.findByRole('button', { name: 'Default background' }) - .should('exist') - .and('have.attr', 'aria-pressed', 'true') - - cy.window() - .should(() => validateUserThemingDefaultCss(defaultPrimary, null)) - }) -}) diff --git a/cypress/e2e/theming/admin-settings_branding.cy.ts b/cypress/e2e/theming/admin-settings_branding.cy.ts deleted file mode 100644 index 314a7e2224b6f..0000000000000 --- a/cypress/e2e/theming/admin-settings_branding.cy.ts +++ /dev/null @@ -1,219 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' - -const admin = new User('admin', 'admin') - -describe('Admin theming: Setting custom project URLs', function() { - this.beforeEach(() => { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - cy.visit('/settings/admin/theming') - cy.intercept('POST', '**/apps/theming/ajax/updateStylesheet').as('updateTheming') - }) - - it('Setting the web link', () => { - cy.findByRole('textbox', { name: /web link/i }) - .and('have.attr', 'type', 'url') - .as('input') - .scrollIntoView() - cy.get('@input') - .should('be.visible') - .type('{selectAll}http://example.com/path?query#fragment{enter}') - - cy.wait('@updateTheming') - - cy.logout() - - cy.visit('/') - cy.contains('a', 'Nextcloud') - .should('be.visible') - .and('have.attr', 'href', 'http://example.com/path?query#fragment') - }) - - it('Setting the legal notice link', () => { - cy.findByRole('textbox', { name: /legal notice link/i }) - .should('exist') - .and('have.attr', 'type', 'url') - .as('input') - .scrollIntoView() - cy.get('@input') - .type('http://example.com/path?query#fragment{enter}') - - cy.wait('@updateTheming') - - cy.logout() - - cy.visit('/') - cy.contains('a', /legal notice/i) - .should('be.visible') - .and('have.attr', 'href', 'http://example.com/path?query#fragment') - }) - - it('Setting the privacy policy link', () => { - cy.findByRole('textbox', { name: /privacy policy link/i }) - .should('exist') - .as('input') - .scrollIntoView() - cy.get('@input') - .should('have.attr', 'type', 'url') - .type('http://privacy.local/path?query#fragment{enter}') - - cy.wait('@updateTheming') - - cy.logout() - - cy.visit('/') - cy.contains('a', /privacy policy/i) - .should('be.visible') - .and('have.attr', 'href', 'http://privacy.local/path?query#fragment') - }) -}) - -describe('Admin theming: Web link corner cases', function() { - this.beforeEach(() => { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - cy.visit('/settings/admin/theming') - cy.intercept('POST', '**/apps/theming/ajax/updateStylesheet').as('updateTheming') - }) - - it('Already URL encoded', () => { - cy.findByRole('textbox', { name: /web link/i }) - .and('have.attr', 'type', 'url') - .as('input') - .scrollIntoView() - cy.get('@input') - .should('be.visible') - .type('{selectAll}http://example.com/%22path%20with%20space%22{enter}') - - cy.wait('@updateTheming') - - cy.logout() - - cy.visit('/') - cy.contains('a', 'Nextcloud') - .should('be.visible') - .and('have.attr', 'href', 'http://example.com/%22path%20with%20space%22') - }) - - it('URL with double quotes', () => { - cy.findByRole('textbox', { name: /web link/i }) - .and('have.attr', 'type', 'url') - .as('input') - .scrollIntoView() - cy.get('@input') - .should('be.visible') - .type('{selectAll}http://example.com/"path"{enter}') - - cy.wait('@updateTheming') - - cy.logout() - - cy.visit('/') - cy.contains('a', 'Nextcloud') - .should('be.visible') - .and('have.attr', 'href', 'http://example.com/%22path%22') - }) - - it('URL with double quotes and already encoded', () => { - cy.findByRole('textbox', { name: /web link/i }) - .and('have.attr', 'type', 'url') - .as('input') - .scrollIntoView() - cy.get('@input') - .should('be.visible') - .type('{selectAll}http://example.com/"the%20path"{enter}') - - cy.wait('@updateTheming') - - cy.logout() - - cy.visit('/') - cy.contains('a', 'Nextcloud') - .should('be.visible') - .and('have.attr', 'href', 'http://example.com/%22the%20path%22') - }) -}) - -describe('Admin theming: Change the login fields then reset them', function() { - const name = 'ABCdef123' - const url = 'https://example.com' - const slogan = 'Testing is fun' - - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: /^Theming/ }) - .should('exist') - .scrollIntoView() - }) - - it('Change the name field', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('updateFields') - - // Name - cy.findByRole('textbox', { name: 'Name' }) - .should('be.visible') - .type(`{selectall}${name}{enter}`) - cy.wait('@updateFields') - - // Url - cy.findByRole('textbox', { name: 'Web link' }) - .should('be.visible') - .type(`{selectall}${url}{enter}`) - cy.wait('@updateFields') - - // Slogan - cy.findByRole('textbox', { name: 'Slogan' }) - .should('be.visible') - .type(`{selectall}${slogan}{enter}`) - cy.wait('@updateFields') - }) - - it('Ensure undo button presence', function() { - cy.findAllByRole('button', { name: /undo changes/i }) - .should('have.length', 3) - }) - - it('Validate login screen changes', function() { - cy.logout() - cy.visit('/') - - cy.get('[data-login-form-headline]').should('contain.text', name) - cy.get('footer p a').should('have.text', name) - cy.get('footer p a').should('have.attr', 'href', url) - cy.get('footer p').should('contain.text', `– ${slogan}`) - }) - - it('Undo theming settings', function() { - cy.login(admin) - cy.visit('/settings/admin/theming') - cy.findAllByRole('button', { name: /undo changes/i }) - .each((button) => { - cy.intercept('*/apps/theming/ajax/undoChanges').as('undoField') - cy.wrap(button).click() - cy.wait('@undoField') - }) - cy.logout() - }) - - it('Validate login screen changes again', function() { - cy.visit('/') - - cy.get('[data-login-form-headline]').should('not.contain.text', name) - cy.get('footer p a').should('not.have.text', name) - cy.get('footer p a').should('not.have.attr', 'href', url) - cy.get('footer p').should('not.contain.text', `– ${slogan}`) - }) -}) diff --git a/cypress/e2e/theming/admin-settings_colors.cy.ts b/cypress/e2e/theming/admin-settings_colors.cy.ts deleted file mode 100644 index 6651c3a4714ca..0000000000000 --- a/cypress/e2e/theming/admin-settings_colors.cy.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { - defaultBackground, - defaultPrimary, - pickColor, - validateBodyThemingCss, -} from './themingUtils.ts' - -const admin = new User('admin', 'admin') - -describe('Change the primary color and reset it', function() { - let selectedColor = '' - - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - it('See the admin theming section', function() { - cy.visit('/settings/admin/theming') - cy.findByRole('heading', { name: 'Background and color' }) - .should('exist') - .scrollIntoView() - }) - - it('Change the primary color', function() { - cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') - - pickColor(cy.findByRole('button', { name: /Primary color/ })) - .then((color) => { - selectedColor = color - }) - - cy.wait('@setColor') - cy.waitUntil(() => validateBodyThemingCss( - selectedColor, - defaultBackground, - defaultPrimary, - )) - }) - - it('Screenshot the login page and validate login page', function() { - cy.logout() - cy.visit('/') - - cy.waitUntil(() => validateBodyThemingCss( - selectedColor, - defaultBackground, - defaultPrimary, - )) - cy.screenshot() - }) - - it('Undo theming settings and validate login page again', function() { - cy.resetAdminTheming() - cy.visit('/') - - cy.waitUntil(validateBodyThemingCss) - cy.screenshot() - }) -}) diff --git a/cypress/e2e/theming/admin-settings_default-app.cy.ts b/cypress/e2e/theming/admin-settings_default-app.cy.ts deleted file mode 100644 index 9d497ba2d2c7d..0000000000000 --- a/cypress/e2e/theming/admin-settings_default-app.cy.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { NavigationHeader } from '../../pages/NavigationHeader.ts' - -const admin = new User('admin', 'admin') - -after(() => cy.resetAdminTheming()) - -describe('Admin theming set default apps', () => { - const navigationHeader = new NavigationHeader() - - before(function() { - // Just in case previous test failed - cy.resetAdminTheming() - cy.login(admin) - }) - - it('See the current default app is the dashboard', () => { - // check default route - cy.visit('/') - cy.url().should('match', /apps\/dashboard/) - - // Also check the top logo link - navigationHeader.logo().click() - cy.url().should('match', /apps\/dashboard/) - }) - - it('See the default app settings', () => { - cy.visit('/settings/admin/theming') - - cy.get('.settings-section').contains('Navigation bar settings').should('exist') - getDefaultAppSwitch().should('exist') - getDefaultAppSwitch().scrollIntoView() - }) - - it('Toggle the "use custom default app" switch', () => { - getDefaultAppSwitch().should('not.be.checked') - cy.findByRole('region', { name: 'Global default app' }) - .should('not.exist') - - getDefaultAppSwitch().check({ force: true }) - getDefaultAppSwitch().should('be.checked') - cy.findByRole('region', { name: 'Global default app' }) - .should('exist') - }) - - it('See the default app combobox', () => { - cy.findByRole('region', { name: 'Global default app' }) - .should('exist') - .findByRole('combobox') - .scrollIntoView() - - cy.findByRole('region', { name: 'Global default app' }) - .findByRole('button', { name: 'Deselect Dashboard' }) - .should('be.visible') - cy.findByRole('region', { name: 'Global default app' }) - .findByRole('button', { name: 'Deselect Files' }) - .should('be.visible') - }) - - it('See the default app order selector', () => { - cy.findByRole('region', { name: 'Global default app' }) - .should('exist') - cy.findByRole('list', { name: 'Navigation bar app order' }) - .should('exist') - .findAllByRole('listitem') - .should('have.length', 2) - .then((elements) => { - const appIDs = elements.map((idx, el) => el.innerText.trim()).get() - expect(appIDs).to.deep.eq(['Dashboard', 'Files']) - }) - }) - - it('Change the default app', () => { - cy.findByRole('list', { name: 'Navigation bar app order' }) - .should('exist') - .as('appOrderSelector') - .scrollIntoView() - - cy.get('@appOrderSelector') - .findAllByRole('listitem') - .filter((_, e) => !!e.innerText.match(/Files/i)) - .findByRole('button', { name: 'Move up' }) - .as('moveFilesUpButton') - - cy.get('@moveFilesUpButton').should('be.visible') - cy.get('@moveFilesUpButton').click() - cy.get('@moveFilesUpButton').should('not.exist') - }) - - it('See the default app is changed', () => { - cy.findByRole('list', { name: 'Navigation bar app order' }) - .findAllByRole('listitem') - .then((elements) => { - const appIDs = elements.map((idx, el) => el.innerText.trim()).get() - expect(appIDs).to.deep.eq(['Files', 'Dashboard']) - }) - - // Check the redirect to the default app works - cy.request({ url: '/', followRedirect: false }).then((response) => { - expect(response.status).to.eq(302) - expect(response).to.have.property('headers') - expect(response.headers.location).to.contain('/apps/files') - }) - }) - - it('Toggle the "use custom default app" switch back to reset the default apps', () => { - cy.visit('/settings/admin/theming') - getDefaultAppSwitch().scrollIntoView() - - getDefaultAppSwitch().should('be.checked') - getDefaultAppSwitch().uncheck({ force: true }) - getDefaultAppSwitch().should('be.not.checked') - - // Check the redirect to the default app works - cy.request({ url: '/', followRedirect: false }).then((response) => { - expect(response.status).to.eq(302) - expect(response).to.have.property('headers') - expect(response.headers.location).to.contain('/apps/dashboard') - }) - }) -}) - -function getDefaultAppSwitch() { - return cy.findByRole('checkbox', { name: 'Use custom default app' }) -} 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/e2e/theming/user-settings_app-order.cy.ts b/cypress/e2e/theming/user-settings_app-order.cy.ts deleted file mode 100644 index 5098b9af4314e..0000000000000 --- a/cypress/e2e/theming/user-settings_app-order.cy.ts +++ /dev/null @@ -1,302 +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' - -import { NavigationHeader } from '../../pages/NavigationHeader.ts' -import { SettingsAppOrderList } from '../../pages/SettingsAppOrderList.ts' -import { installTestApp, uninstallTestApp } from '../../support/commonUtils.ts' - -before(() => uninstallTestApp()) - -describe('User theming set app order', () => { - const navigationHeader = new NavigationHeader() - const appOrderList = new SettingsAppOrderList() - let user: User - - before(() => { - cy.resetAdminTheming() - // Create random user for this test - cy.createRandomUser().then(($user) => { - user = $user - cy.login($user) - }) - }) - - after(() => cy.deleteUser(user)) - - it('See the app order settings', () => { - visitAppOrderSettings() - }) - - it('See that the dashboard app is the first one', () => { - const appOrder = ['Dashboard', 'Files'] - appOrderList.assertAppOrder(appOrder) - - // Check the top app menu order. The launcher grid appends a synthetic - // "More apps" / "App store" tile to the user's apps, so iterate - // positionally only over the real-app prefix. - navigationHeader.getNavigationEntries().then(($entries) => { - appOrder.forEach((name, index) => { - expect($entries.eq(index)).to.contain.text(name) - }) - }) - }) - - it('Change the app order', () => { - appOrderList.interceptAppOrder() - appOrderList.getAppOrderList() - .scrollIntoView() - appOrderList.getUpButtonForApp('Files') - .should('be.visible') - .click() - appOrderList.waitForAppOrderUpdate() - - appOrderList.assertAppOrder(['Files', 'Dashboard']) - }) - - it('See the app menu order is changed', () => { - cy.reload() - const appOrder = ['Files', 'Dashboard'] - appOrderList.getAppOrderList() - .scrollIntoView() - appOrderList.assertAppOrder(appOrder) - - // Check the top app menu order. Idempotent open in the page object - // re-opens the popover after the reload above. The synthetic trailing - // tile is ignored by iterating only over the expected app names. - navigationHeader.getNavigationEntries().then(($entries) => { - appOrder.forEach((name, index) => { - expect($entries.eq(index)).to.contain.text(name) - }) - }) - }) -}) - -describe('User theming set app order with default app', () => { - const appOrderList = new SettingsAppOrderList() - const navigationHeader = new NavigationHeader() - let user: User - - before(() => { - cy.resetAdminTheming() - // install a third app - installTestApp() - // set files as default app - cy.runOccCommand('config:system:set --value \'files\' defaultapp') - - // Create random user for this test - cy.createRandomUser().then(($user) => { - user = $user - cy.login($user) - }) - }) - - after(() => { - cy.deleteUser(user) - uninstallTestApp() - }) - - it('See files is the default app', () => { - // Check the redirect to the default app works - cy.request({ url: '/', followRedirect: false }).then((response) => { - expect(response.status).to.eq(302) - expect(response).to.have.property('headers') - expect(response.headers.location).to.contain('/apps/files') - }) - }) - - it('See the app order settings: files is the first one', () => { - visitAppOrderSettings() - - const appOrder = ['Files', 'Dashboard', 'Test App 2', 'Test App'] - appOrderList.getAppOrderList() - .scrollIntoView() - appOrderList.assertAppOrder(appOrder) - }) - - it('Can not change the default app', () => { - appOrderList.getUpButtonForApp('Files').should('not.exist') - appOrderList.getDownButtonForApp('Files').should('not.exist') - appOrderList.getUpButtonForApp('Dashboard').should('not.exist') - // but can move down - appOrderList.getDownButtonForApp('Dashboard').should('be.visible') - }) - - it('Can see the correct buttons for other apps', () => { - appOrderList.getUpButtonForApp('Test App 2').should('be.visible') - appOrderList.getDownButtonForApp('Test App 2').should('be.visible') - appOrderList.getUpButtonForApp('Test App').should('be.visible') - appOrderList.getDownButtonForApp('Test App').should('not.exist') - }) - - it('Change the order of the other apps', () => { - appOrderList.interceptAppOrder() - appOrderList.getUpButtonForApp('Test App').click() - appOrderList.waitForAppOrderUpdate() - appOrderList.getUpButtonForApp('Test App').click() - appOrderList.waitForAppOrderUpdate() - - // Can't get up anymore, files is enforced as default app - appOrderList.getUpButtonForApp('Test App').should('not.exist') - - // Check the app order settings UI - appOrderList.assertAppOrder(['Files', 'Test App', 'Dashboard', 'Test App 2']) - }) - - it('See the app menu order is changed', () => { - cy.reload() - - const appOrder = ['Files', 'Test App', 'Dashboard', 'Test App 2'] - // Check the top app menu order. See note above: the launcher appends - // a synthetic tile that we skip by iterating positionally. - navigationHeader.getNavigationEntries().then(($entries) => { - appOrder.forEach((name, index) => { - expect($entries.eq(index)).to.contain.text(name) - }) - }) - }) -}) - -describe('User theming app order list accessibility', () => { - const appOrderList = new SettingsAppOrderList() - let user: User - - before(() => { - cy.resetAdminTheming() - installTestApp() - // Create random user for this test - cy.createRandomUser().then(($user) => { - user = $user - cy.login($user) - }) - }) - - after(() => { - uninstallTestApp() - cy.deleteUser(user) - }) - - it('click the first button', () => { - visitAppOrderSettings() - appOrderList.interceptAppOrder() - appOrderList.getDownButtonForApp('Dashboard') - .should('be.visible') - .scrollIntoView() - appOrderList.getDownButtonForApp('Dashboard') - .focus() - appOrderList.getDownButtonForApp('Dashboard') - .click() - appOrderList.waitForAppOrderUpdate() - }) - - it('see the same app kept the focus', () => { - appOrderList.getDownButtonForApp('Dashboard').should('have.focus') - }) - - it('click the last button', () => { - appOrderList.interceptAppOrder() - appOrderList.getUpButtonForApp('Dashboard') - .should('be.visible') - .focus() - appOrderList.getUpButtonForApp('Dashboard').click() - appOrderList.waitForAppOrderUpdate() - }) - - it('see the same app kept the focus', () => { - appOrderList.getUpButtonForApp('Dashboard').should('not.exist') - appOrderList.getDownButtonForApp('Dashboard').should('have.focus') - }) -}) - -describe('User theming reset app order', () => { - const appOrderList = new SettingsAppOrderList() - const navigationHeader = new NavigationHeader() - let user: User - - before(() => { - cy.resetAdminTheming() - // Create random user for this test - cy.createRandomUser().then(($user) => { - user = $user - cy.login($user) - }) - }) - - after(() => cy.deleteUser(user)) - - it('See that the dashboard app is the first one', () => { - visitAppOrderSettings() - - const appOrder = ['Dashboard', 'Files'] - appOrderList.assertAppOrder(appOrder) - - // Check the top app menu order. See note above on the synthetic tile. - navigationHeader.getNavigationEntries().then(($entries) => { - appOrder.forEach((name, index) => { - expect($entries.eq(index)).to.contain.text(name) - }) - }) - }) - - it('See the reset button is disabled', () => { - appOrderList.getResetButton() - .scrollIntoView() - appOrderList.getResetButton() - .should('be.disabled') - }) - - it('Change the app order', () => { - appOrderList.interceptAppOrder() - appOrderList.getUpButtonForApp('Files') - .should('be.visible') - .click() - appOrderList.waitForAppOrderUpdate() - - appOrderList.assertAppOrder(['Files', 'Dashboard']) - }) - - it('See the reset button is no longer disabled', () => { - appOrderList.getResetButton() - .scrollIntoView() - appOrderList.getResetButton() - .should('be.visible') - .and('be.enabled') - }) - - it('Reset the app order', () => { - cy.intercept('GET', '/ocs/v2.php/core/navigation/apps').as('loadApps') - appOrderList.interceptAppOrder() - appOrderList.getResetButton().click({ force: true }) - - cy.wait('@updateAppOrder') - .its('request.body') - .should('have.property', 'configValue', '[]') - cy.wait('@loadApps') - }) - - it('See the app order is restored', () => { - const appOrder = ['Dashboard', 'Files'] - appOrderList.assertAppOrder(appOrder) - // Check the top app menu order. See note above on the synthetic tile. - navigationHeader.getNavigationEntries().then(($entries) => { - appOrder.forEach((name, index) => { - expect($entries.eq(index)).to.contain.text(name) - }) - }) - }) - - it('See the reset button is disabled again', () => { - appOrderList.getResetButton() - .should('be.disabled') - }) -}) - -function visitAppOrderSettings() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Navigation bar settings/ }) - .should('exist') - .scrollIntoView() -} diff --git a/cypress/e2e/theming/user-settings_background.cy.ts b/cypress/e2e/theming/user-settings_background.cy.ts deleted file mode 100644 index 2ad0023047253..0000000000000 --- a/cypress/e2e/theming/user-settings_background.cy.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { User } from '@nextcloud/e2e-test-server/cypress' -import { NavigationHeader } from '../../pages/NavigationHeader.ts' -import { defaultPrimary, pickColor, validateBodyThemingCss } from './themingUtils.ts' - -const admin = new User('admin', 'admin') - -describe('User default background settings', function() { - before(function() { - cy.resetAdminTheming() - cy.resetUserTheming(admin) - cy.createRandomUser().then((user: User) => { - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Appearance and accessibility settings/ }) - .should('be.visible') - }) - - it('Default is selected on new users', function() { - cy.findByRole('button', { name: 'Default background', pressed: true }) - .should('exist') - .scrollIntoView() - }) -}) - -describe('User select shipped backgrounds and remove background', function() { - before(function() { - cy.createRandomUser().then((user: User) => { - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Background and color/ }) - .should('exist') - .scrollIntoView() - }) - - it('Select a shipped background', function() { - const background = 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' - const backgroundName = 'Background picture of a red-ish butterfly wing under microscope' - cy.intercept('*/apps/theming/background/shipped').as('setBackground') - - // Select background - cy.findByRole('button', { name: backgroundName, pressed: false }) - .click() - cy.findByRole('button', { name: backgroundName, pressed: true }) - .should('be.visible') - - // Validate changed background and primary - cy.wait('@setBackground') - cy.waitUntil(() => validateBodyThemingCss('#a53c17', background, '#652e11')) - }) - - it('Select a bright shipped background', function() { - const background = 'bernie-cetonia-aurata-take-off-composition.jpg' - const backgroundName = 'Montage of a cetonia aurata bug that takes off with white background' - cy.intercept('*/apps/theming/background/shipped').as('setBackground') - - cy.findByRole('button', { name: backgroundName, pressed: false }) - .click() - cy.findByRole('button', { name: backgroundName, pressed: true }) - .should('be.visible') - - // Validate changed background and primary - cy.wait('@setBackground') - cy.waitUntil(() => validateBodyThemingCss('#56633d', background, '#dee0d3')) - }) -}) - -describe('User select a custom color', function() { - before(function() { - cy.createRandomUser().then((user: User) => { - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Background and color/ }) - .should('exist') - .scrollIntoView() - }) - - it('Select a custom color', function() { - cy.intercept('*/apps/theming/background/color').as('clearBackground') - - // Clear background - pickColor(cy.findByRole('button', { name: 'Plain background' }), 7) - - // Validate clear background - cy.wait('@clearBackground') - cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#3794ac')) - }) -}) - -describe('User select a bright custom color and remove background', function() { - const navigationHeader = new NavigationHeader() - - before(function() { - cy.createRandomUser().then((user: User) => { - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Background and color/ }) - .should('exist') - .scrollIntoView() - }) - - it('Remove background', function() { - cy.intercept('*/apps/theming/background/color').as('clearBackground') - - // Clear background - pickColor(cy.findByRole('button', { name: 'Plain background' }), 4) - - // Validate clear background - cy.wait('@clearBackground') - cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#ddcb55')) - }) - - it('See the header being inverted', function() { - // Probe the Nextcloud logo: it carries the same - // `var(--background-image-invert-if-bright)` filter and is always - // present in the header. The waffle launcher's current-app icon only - // renders when an app is active, which isn't the case on settings, - // and the in-popover tiles use a fixed brightness/invert filter - // regardless of theme so they're not a valid inversion probe. - cy.waitUntil(() => navigationHeader.logo().find('.logo').then((el) => { - let ret = true - el.each(function() { - ret = ret && window.getComputedStyle(this).filter === 'invert(1)' - }) - return ret - })) - }) - - it('Select another but non-bright shipped background', function() { - const background = 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' - const backgroundName = 'Background picture of a red-ish butterfly wing under microscope' - cy.intercept('*/apps/theming/background/shipped').as('setBackground') - - // Select background - cy.findByRole('button', { name: backgroundName, pressed: false }) - .click() - cy.findByRole('button', { name: backgroundName, pressed: true }) - .should('be.visible') - - // Validate changed background and primary - cy.wait('@setBackground') - cy.waitUntil(() => validateBodyThemingCss('#a53c17', background, '#652e11')) - }) - - it('See the header NOT being inverted this time', function() { - // Probe the Nextcloud logo: see the inverted-header test above for - // why we don't probe the menu icons. - cy.waitUntil(() => navigationHeader.logo().find('.logo').then((el) => { - let ret = true - el.each(function() { - ret = ret && window.getComputedStyle(this).filter === 'none' - }) - return ret - })) - }) -}) - -describe('User select a custom background', function() { - const image = 'image.jpg' - before(function() { - cy.createRandomUser().then((user: User) => { - cy.uploadFile(user, image, 'image/jpeg') - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Background and color/ }) - .should('exist') - .scrollIntoView() - }) - - it('Select a custom background', function() { - cy.intercept('*/apps/theming/background/custom').as('setBackground') - - // Pick background - cy.findByRole('button', { name: 'Custom background' }).click() - cy.findByRole('dialog') - .should('be.visible') - .findAllByRole('row') - .contains(image) - .click() - cy.findByRole('button', { name: 'Select background' }).click() - - // Wait for background to be set - cy.wait('@setBackground') - cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, 'apps/theming/background?v=', '#2f2221')) - }) -}) - -describe('User changes settings and reload the page', function() { - const image = 'image.jpg' - - before(function() { - cy.createRandomUser().then((user: User) => { - cy.uploadFile(user, image, 'image/jpeg') - cy.login(user) - }) - }) - - it('See the user background settings', function() { - cy.visit('/settings/user/theming') - cy.findByRole('heading', { name: /Background and color/ }) - .should('exist') - .scrollIntoView() - }) - - it('Select a custom background', function() { - cy.intercept('*/apps/theming/background/custom').as('setBackground') - - // Pick background - cy.findByRole('button', { name: 'Custom background' }).click() - cy.findByRole('dialog') - .should('be.visible') - .findAllByRole('row') - .contains(image) - .click() - cy.findByRole('button', { name: 'Select background' }).click() - - // Wait for background to be set - cy.wait('@setBackground') - cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, 'apps/theming/background?v=', '#2f2221')) - }) - - it('Select a custom background color', function() { - cy.intercept('*/apps/theming/background/color').as('clearBackground') - - // Clear background - pickColor(cy.findByRole('button', { name: 'Plain background' }), 5) - - // Validate clear background - cy.wait('@clearBackground') - cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#a5b872')) - }) - - it('Select a custom primary color', function() { - cy.intercept('/ocs/v2.php/apps/provisioning_api/api/v1/config/users/theming/primary_color').as('setPrimaryColor') - - pickColor(cy.findByRole('button', { name: 'Primary color' }), 2) - - cy.wait('@setPrimaryColor') - cy.waitUntil(() => validateBodyThemingCss('#c98879', null, '#a5b872')) - }) - - it('Reload the page and validate persistent changes', function() { - cy.reload() - cy.waitUntil(() => validateBodyThemingCss('#c98879', null, '#a5b872')) - }) -}) 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 8ca8f3ef52733..0000000000000 --- a/cypress/fixtures/testapp/lib/AppInfo/Application.php +++ /dev/null @@ -1,18 +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