aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-07-01 09:19:52 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2025-07-01 19:47:25 +0200
commitc2d5b4eaf20db61757148483d08a41b85b44cb40 (patch)
treeaa8f41a19496b06456937b8d981220d7bbac2631
parent7cac057747f7db1a8c3454f3be504d45d1d65388 (diff)
downloadnextcloud-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.ts25
-rw-r--r--cypress/e2e/files/search.cy.ts198
-rw-r--r--cypress/pages/FilesNavigation.ts13
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() {