diff options
-rw-r--r-- | apps/files/src/components/FileListFilters.vue | 5 | ||||
-rw-r--r-- | apps/files/src/components/NavigationQuota.vue | 3 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 67 | ||||
-rw-r--r-- | cypress/e2e/files/files-filtering.cy.ts | 231 | ||||
-rw-r--r-- | cypress/e2e/files/files-searching.cy.ts | 90 | ||||
-rw-r--r-- | cypress/pages/FilesFilters.ts | 34 | ||||
-rw-r--r-- | cypress/pages/FilesNavigation.ts | 35 |
7 files changed, 340 insertions, 125 deletions
diff --git a/apps/files/src/components/FileListFilters.vue b/apps/files/src/components/FileListFilters.vue index 20c9179bd15..5cdc4e877fd 100644 --- a/apps/files/src/components/FileListFilters.vue +++ b/apps/files/src/components/FileListFilters.vue @@ -4,14 +4,15 @@ --> <template> <div class="file-list-filters"> - <div class="file-list-filters__filter"> + <div class="file-list-filters__filter" data-cy-files-filters> <span v-for="filter of visualFilters" :key="filter.id" ref="filterElements" /> </div> <ul v-if="activeChips.length > 0" class="file-list-filters__active" :aria-label="t('files', 'Active filters')"> <li v-for="(chip, index) of activeChips" :key="index"> - <NcChip :icon-svg="chip.icon" + <NcChip :aria-label-close="t('files', 'Remove filter')" + :icon-svg="chip.icon" :text="chip.text" @close="chip.onclick" /> </li> diff --git a/apps/files/src/components/NavigationQuota.vue b/apps/files/src/components/NavigationQuota.vue index 557fb240797..0619f6bc3fd 100644 --- a/apps/files/src/components/NavigationQuota.vue +++ b/apps/files/src/components/NavigationQuota.vue @@ -4,7 +4,7 @@ --> <template> <NcAppNavigationItem v-if="storageStats" - :aria-label="t('files', 'Storage informations')" + :aria-description="t('files', 'Storage information')" :class="{ 'app-navigation-entry__settings-quota--not-unlimited': storageStats.quota >= 0}" :loading="loadingStorageStats" :name="storageStatsTitle" @@ -17,6 +17,7 @@ <!-- Progress bar --> <NcProgressBar v-if="storageStats.quota >= 0" slot="extra" + :aria-label="t('files', 'Storage quota')" :error="storageStats.relative > 80" :value="Math.min(storageStats.relative, 100)" /> </NcAppNavigationItem> diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 51430dd54b2..cfd170bd073 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -8,33 +8,40 @@ <template #search> <NcAppNavigationSearch v-model="searchQuery" :label="t('files', 'Filter filenames…')" /> </template> - <template #list> - <NcAppNavigationItem v-for="view in parentViews" - :key="view.id" - :allow-collapse="true" - :data-cy-files-navigation-item="view.id" - :exact="useExactRouteMatching(view)" - :icon="view.iconClass" - :name="view.name" - :open="isExpanded(view)" - :pinned="view.sticky" - :to="generateToNavigation(view)" - @update:open="onToggleExpand(view)"> - <!-- Sanitized icon as svg if provided --> - <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" /> - - <!-- Child views if any --> - <NcAppNavigationItem v-for="child in childViews[view.id]" - :key="child.id" - :data-cy-files-navigation-item="child.id" - :exact-path="true" - :icon="child.iconClass" - :name="child.name" - :to="generateToNavigation(child)"> + <template #default> + <NcAppNavigationList :aria-label="t('files', 'Views')"> + <NcAppNavigationItem v-for="view in parentViews" + :key="view.id" + :allow-collapse="true" + :data-cy-files-navigation-item="view.id" + :exact="useExactRouteMatching(view)" + :icon="view.iconClass" + :name="view.name" + :open="isExpanded(view)" + :pinned="view.sticky" + :to="generateToNavigation(view)" + @update:open="onToggleExpand(view)"> <!-- Sanitized icon as svg if provided --> - <NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" /> + <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" /> + + <!-- Child views if any --> + <NcAppNavigationItem v-for="child in childViews[view.id]" + :key="child.id" + :data-cy-files-navigation-item="child.id" + :exact-path="true" + :icon="child.iconClass" + :name="child.name" + :to="generateToNavigation(child)"> + <!-- Sanitized icon as svg if provided --> + <NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" /> + </NcAppNavigationItem> </NcAppNavigationItem> - </NcAppNavigationItem> + </NcAppNavigationList> + + <!-- Settings modal--> + <SettingsModal :open="settingsOpened" + data-cy-files-navigation-settings + @close="onSettingsClose" /> </template> <!-- Non-scrollable navigation bottom elements --> @@ -44,19 +51,13 @@ <NavigationQuota /> <!-- Files settings modal toggle--> - <NcAppNavigationItem :aria-label="t('files', 'Open the files app settings')" - :name="t('files', 'Files settings')" + <NcAppNavigationItem :name="t('files', 'Files settings')" data-cy-files-navigation-settings-button @click.prevent.stop="openSettings"> <IconCog slot="icon" :size="20" /> </NcAppNavigationItem> </ul> </template> - - <!-- Settings modal--> - <SettingsModal :open="settingsOpened" - data-cy-files-navigation-settings - @close="onSettingsClose" /> </NcAppNavigation> </template> @@ -70,6 +71,7 @@ import { defineComponent } from 'vue' import IconCog from 'vue-material-design-icons/Cog.vue' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' +import NcAppNavigationList from '@nextcloud/vue/dist/Components/NcAppNavigationList.js' import NcAppNavigationSearch from '@nextcloud/vue/dist/Components/NcAppNavigationSearch.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' import NavigationQuota from '../components/NavigationQuota.vue' @@ -90,6 +92,7 @@ export default defineComponent({ NavigationQuota, NcAppNavigation, NcAppNavigationItem, + NcAppNavigationList, NcAppNavigationSearch, NcIconSvgWrapper, SettingsModal, diff --git a/cypress/e2e/files/files-filtering.cy.ts b/cypress/e2e/files/files-filtering.cy.ts new file mode 100644 index 00000000000..c7b147d4a9c --- /dev/null +++ b/cypress/e2e/files/files-filtering.cy.ts @@ -0,0 +1,231 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/cypress' +import { getRowForFile, navigateToFolder } from './FilesUtils' +import { FilesNavigationPage } from '../../pages/FilesNavigation' +import { FilesFilterPage } from '../../pages/FilesFilters' + +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.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + cy.findByRole('menuitemcheckbox', { name: 'Spreadsheets' }) + .should('be.visible') + .click() + filesFilters.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + + // 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.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + cy.findByRole('menuitemcheckbox', { name: 'Spreadsheets' }) + .should('be.visible') + .click() + filesFilters.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + + // See folder is not visible + getRowForFile('folder').should('not.exist') + + // clear filter + filesFilters.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + cy.findByRole('menuitem', { name: /clear filter/i }) + .should('be.visible') + .click() + filesFilters.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + + // 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.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + cy.findByRole('menuitemcheckbox', { name: 'Spreadsheets' }) + .should('be.visible') + .click() + filesFilters.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + + // 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 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 that the folder is also filtered + getRowForFile('text.txt').should('not.exist') + }) + + 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.filterContainter() + .findByRole('button', { name: 'Type' }) + .should('be.visible') + .click() + cy.findByRole('menuitemcheckbox', { name: 'Folders' }) + .should('be.visible') + .click() + filesFilters.filterContainter() + .findByRole('button', { name: 'Type' }) + .click() + + // 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') + }) + + 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-searching.cy.ts b/cypress/e2e/files/files-searching.cy.ts deleted file mode 100644 index 10ca1b44e2f..00000000000 --- a/cypress/e2e/files/files-searching.cy.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { User } from '@nextcloud/cypress' -import { getRowForFile, navigateToFolder } from './FilesUtils' -import { UnifiedSearchPage } from '../../pages/UnifiedSearch.ts' - -describe('files: Search and filter in files list', { testIsolation: true }, () => { - const unifiedSearch = new UnifiedSearchPage() - let user: User - - beforeEach(() => cy.createRandomUser().then(($user) => { - user = $user - - cy.mkdir(user, '/a folder') - cy.uploadContent(user, new Blob([]), 'text/plain', '/b file') - cy.uploadContent(user, new Blob([]), 'text/plain', '/a folder/c file') - cy.login(user) - cy.visit('/apps/files') - })) - - it('files app supports local search', () => { - unifiedSearch.openLocalSearch() - unifiedSearch.localSearchInput() - .should('not.have.css', 'display', 'none') - .and('not.be.disabled') - }) - - it('filters current view', () => { - // All are visible by default - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('be.visible') - - // Set up a search query - unifiedSearch.openLocalSearch() - unifiedSearch.typeLocalSearch('a folder') - - // See that only the folder is visible - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('not.exist') - }) - - it('resets filter when changeing the directory', () => { - // All are visible by default - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('be.visible') - - // Set up a search query - unifiedSearch.openLocalSearch() - unifiedSearch.typeLocalSearch('a folder') - - // See that only the folder is visible - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('not.exist') - - // go to that folder - navigateToFolder('a folder') - - // see that the folder is not filtered - getRowForFile('c file').should('be.visible') - }) - - it('resets filter when changeing the view', () => { - // All are visible by default - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('be.visible') - - // Set up a search query - unifiedSearch.openLocalSearch() - unifiedSearch.typeLocalSearch('a folder') - - // See that only the folder is visible - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('not.exist') - - // go to other view - cy.get('[data-cy-files-navigation-item="personal"] a').click({ force: true }) - // wait for view changed - cy.url().should('match', /apps\/files\/personal/) - - // see that the folder is not filtered - getRowForFile('a folder').should('be.visible') - getRowForFile('b file').should('be.visible') - - // see the filter bar is gone - unifiedSearch.localSearchInput().should('not.exist') - }) -}) diff --git a/cypress/pages/FilesFilters.ts b/cypress/pages/FilesFilters.ts new file mode 100644 index 00000000000..036530f1e8a --- /dev/null +++ b/cypress/pages/FilesFilters.ts @@ -0,0 +1,34 @@ +/*! + * 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 { + + filterContainter() { + return cy.get('[data-cy-files-filters]') + } + + 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 new file mode 100644 index 00000000000..cb3897afb90 --- /dev/null +++ b/cypress/pages/FilesNavigation.ts @@ -0,0 +1,35 @@ +/*! + * 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', { name: /filter filenames/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]') + } + +} |