diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-07-01 09:19:52 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-07-01 19:47:25 +0200 |
commit | c2d5b4eaf20db61757148483d08a41b85b44cb40 (patch) | |
tree | aa8f41a19496b06456937b8d981220d7bbac2631 | |
parent | 7cac057747f7db1a8c3454f3be504d45d1d65388 (diff) | |
download | nextcloud-server-c2d5b4eaf20db61757148483d08a41b85b44cb40.tar.gz nextcloud-server-c2d5b4eaf20db61757148483d08a41b85b44cb40.zip |
test: add e2e tests for files search
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | apps/files/src/views/Navigation.cy.ts | 25 | ||||
-rw-r--r-- | cypress/e2e/files/search.cy.ts | 198 | ||||
-rw-r--r-- | cypress/pages/FilesNavigation.ts | 13 |
3 files changed, 232 insertions, 4 deletions
diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts index a88878e2d3a..6b03efa4f5f 100644 --- a/apps/files/src/views/Navigation.cy.ts +++ b/apps/files/src/views/Navigation.cy.ts @@ -10,7 +10,8 @@ import NavigationView from './Navigation.vue' import { useViewConfigStore } from '../store/viewConfig' import { Folder, View, getNavigation } from '@nextcloud/files' -import router from '../router/router' +import router from '../router/router.ts' +import RouterService from '../services/RouterService' const resetNavigation = () => { const nav = getNavigation() @@ -27,9 +28,18 @@ const createView = (id: string, name: string, parent?: string) => new View({ parent, }) +function mockWindow() { + window.OCP ??= {} + window.OCP.Files ??= {} + window.OCP.Files.Router = new RouterService(router) +} + describe('Navigation renders', () => { - before(() => { + before(async () => { delete window._nc_navigation + mockWindow() + getNavigation().register(createView('files', 'Files')) + await router.replace({ name: 'filelist', params: { view: 'files' } }) cy.mockInitialState('files', 'storageStats', { used: 1000 * 1000 * 1000, @@ -41,6 +51,7 @@ describe('Navigation renders', () => { it('renders', () => { cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -60,6 +71,7 @@ describe('Navigation API', () => { before(async () => { delete window._nc_navigation Navigation = getNavigation() + mockWindow() await router.replace({ name: 'filelist', params: { view: 'files' } }) }) @@ -152,14 +164,18 @@ describe('Navigation API', () => { }) describe('Quota rendering', () => { - before(() => { + before(async () => { delete window._nc_navigation + mockWindow() + getNavigation().register(createView('files', 'Files')) + await router.replace({ name: 'filelist', params: { view: 'files' } }) }) afterEach(() => cy.unmockInitialState()) it('Unknown quota', () => { cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -177,6 +193,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -197,6 +214,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -219,6 +237,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, diff --git a/cypress/e2e/files/search.cy.ts b/cypress/e2e/files/search.cy.ts new file mode 100644 index 00000000000..eeb08da5a22 --- /dev/null +++ b/cypress/e2e/files/search.cy.ts @@ -0,0 +1,198 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/cypress' +import { FilesNavigationPage } from '../../pages/FilesNavigation' +import { getRowForFile, navigateToFolder } from './FilesUtils' + +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, '/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', '/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 globally/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 globally/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('can search locally', () => { + navigateToFolder('some folder') + getRowForFile('a file.txt').should('be.visible') + + navigation.searchScopeTrigger().click() + navigation.searchScopeMenu() + .should('be.visible') + .findByRole('menuitem', { name: /search from this location/i }) + .should('be.visible') + .click() + navigation.searchInput().type('file') + + getRowForFile('a file.txt').should('be.visible') + getRowForFile('a second file.txt').should('be.visible') + cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2) + }) + + 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 from this location/i }) + .should('be.visible') + .click() + navigation.searchInput().type('folder') + + // see the empty content message + cy.contains('[role="note"]', /No search results for .folder./) + .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', 'folder') + // and we can switch from local to global search + cy.findByRole('button', { name: 'Search globally' }) + .should('be.visible') + }) + }) + + it('can turn local search into global search', () => { + navigateToFolder('some folder') + getRowForFile('a file.txt').should('be.visible') + + navigation.searchScopeTrigger().click() + navigation.searchScopeMenu() + .should('be.visible') + .findByRole('menuitem', { name: /search from this location/i }) + .should('be.visible') + .click() + navigation.searchInput().type('folder') + + // see the empty content message and turn into global search + cy.contains('[role="note"]', /No search results for .folder./) + .should('be.visible') + .findByRole('button', { name: 'Search globally' }) + .should('be.visible') + .click() + + getRowForFile('some folder').should('be.visible') + getRowForFile('other folder').should('be.visible') + cy.get('[data-cy-files-list-row-fileid]').should('have.length', 2) + }) + + it('can alter search', () => { + navigation.searchScopeTrigger().click() + navigation.searchScopeMenu() + .should('be.visible') + .findByRole('menuitem', { name: /search globally/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 globally/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 globally/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 + cy.findByRole('button', { description: /reload current directory/i }).click() + // 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/pages/FilesNavigation.ts b/cypress/pages/FilesNavigation.ts index 768ca99320c..1be11231bad 100644 --- a/cypress/pages/FilesNavigation.ts +++ b/cypress/pages/FilesNavigation.ts @@ -13,7 +13,18 @@ export class FilesNavigationPage { } searchInput() { - return this.navigation().findByRole('searchbox', { name: /filter file names/i }) + 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() { |