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 14355a62b9d..00000000000 --- 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/files-xml-regression.cy.ts b/cypress/e2e/files/files-xml-regression.cy.ts deleted file mode 100644 index a961b78e2f4..00000000000 --- 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/recent-view.cy.ts b/cypress/e2e/files/recent-view.cy.ts deleted file mode 100644 index 3dd6fb2a8a6..00000000000 --- 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/tests/playwright/e2e/files/duplicated-node-regression.spec.ts b/tests/playwright/e2e/files/duplicated-node-regression.spec.ts new file mode 100644 index 00000000000..29cc27dbfa2 --- /dev/null +++ b/tests/playwright/e2e/files/duplicated-node-regression.spec.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { test, expect } from '../../support/fixtures/files-page.ts' +import { mkdir } from '../../support/utils/dav.ts' + +test.describe('Files: Duplicated node regression', () => { + test.beforeEach(async ({ page, user, filesListPage }) => { + await mkdir(page.request, user, '/only once') + await filesListPage.open() + }) + + /** + * Regression: https://github.com/nextcloud/server/issues/47904 + * Deleting a node and recreating it with the same name left two rows in the list. + */ + test('does not duplicate a node after delete and recreate', async ({ page, filesListPage }) => { + await expect(filesListPage.getRowForFile('only once')).toBeVisible() + + const deleted = page.waitForResponse( + (r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'), + ) + await filesListPage.triggerActionForFile('only once', 'delete') + await deleted + await expect(filesListPage.getRowForFile('only once')).toHaveCount(0) + + await filesListPage.createFolder('only once') + + await expect(filesListPage.getRowForFile('only once')).toHaveCount(1) + }) +}) diff --git a/tests/playwright/e2e/files/files-xml-regression.spec.ts b/tests/playwright/e2e/files/files-xml-regression.spec.ts new file mode 100644 index 00000000000..4682dee1d16 --- /dev/null +++ b/tests/playwright/e2e/files/files-xml-regression.spec.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { test, expect } from '../../support/fixtures/files-page.ts' +import { uploadContent } from '../../support/utils/dav.ts' + +/** + * Regression: https://github.com/nextcloud/server/issues/43331 + * Files whose names contain XML entities (e.g. "&.txt") were wrongly + * displayed and could no longer be renamed or deleted. + */ +test.describe('Files: XML entities in file names', () => { + test('renames a file to a name with XML entities and keeps it after reload', async ({ page, user, filesListPage }) => { + await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/and.txt') + await filesListPage.open() + + await filesListPage.triggerActionForFile('and.txt', 'rename') + const input = filesListPage.getRenameInputForFile('and.txt') + await expect(input).toBeVisible() + + const renamed = page.waitForResponse( + (r) => r.request().method() === 'MOVE' && r.url().includes('/remote.php/dav/files/'), + ) + await input.fill('&.txt') + await input.press('Enter') + await renamed + + // The literal name is kept, not decoded to "&.txt" + await expect(filesListPage.getRowForFile('&.txt')).toBeVisible() + await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0) + + await page.reload() + await expect(filesListPage.getRowForFile('&.txt')).toBeVisible() + await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0) + }) + + test('can delete a file whose name contains XML entities', async ({ page, user, filesListPage }) => { + await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/&.txt') + await filesListPage.open() + + await expect(filesListPage.getRowForFile('&.txt')).toBeVisible() + + const deleted = page.waitForResponse( + (r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'), + ) + await filesListPage.triggerActionForFile('&.txt', 'delete') + await deleted + + await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0) + + await page.reload() + await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0) + await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0) + }) +}) diff --git a/tests/playwright/e2e/files/recent-view.spec.ts b/tests/playwright/e2e/files/recent-view.spec.ts new file mode 100644 index 00000000000..18466aa0797 --- /dev/null +++ b/tests/playwright/e2e/files/recent-view.spec.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { test, expect } from '../../support/fixtures/files-page.ts' +import { uploadContent } from '../../support/utils/dav.ts' + +test.describe('Files: Recent view', () => { + test.beforeEach(async ({ page, user }) => { + await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt') + }) + + test('shows a recently created file in the recent view', async ({ filesListPage }) => { + await filesListPage.open('recent') + await expect(filesListPage.getRowForFile('file.txt')).toBeVisible() + }) + + /** + * Regression: the recent view loaded files with an invalid source, so the + * delete action failed. Deleting from the recent view must work and remove + * the file everywhere. + */ + test('can delete a file from the recent view', async ({ page, filesListPage }) => { + await filesListPage.open('recent') + await expect(filesListPage.getRowForFile('file.txt')).toBeVisible() + + const deleted = page.waitForResponse( + (r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'), + ) + await filesListPage.triggerActionForFile('file.txt', 'delete') + await deleted + + await expect(filesListPage.getRowForFile('file.txt')).toHaveCount(0) + + // Gone from the default view too + await filesListPage.open() + await expect(filesListPage.getRowForFile('file.txt')).toHaveCount(0) + }) +}) diff --git a/tests/playwright/support/sections/FilesListPage.ts b/tests/playwright/support/sections/FilesListPage.ts index 18b9fc25b33..ea194deb7bc 100644 --- a/tests/playwright/support/sections/FilesListPage.ts +++ b/tests/playwright/support/sections/FilesListPage.ts @@ -8,8 +8,12 @@ import type { Locator, Page } from '@playwright/test' export class FilesListPage { constructor(private readonly page: Page) {} - async open(): Promise { - await this.page.goto('apps/files') + /** + * Open the files app. Pass a view id (e.g. 'recent') to open that view + * instead of the default "All files" list. + */ + async open(viewId?: string): Promise { + await this.page.goto(viewId ? `apps/files/${viewId}` : 'apps/files') await this.page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) } @@ -127,4 +131,29 @@ export class FilesListPage { .click() } } + + /** + * Create a folder through the upload picker's "New" menu and wait for the + * MKCOL to land. The upload-picker and new-node-dialog hooks are product-owned + * data-cy attributes (no stable accessible name to target by role). + */ + async createFolder(folderName: string): Promise { + const created = this.page.waitForResponse( + (r) => r.request().method() === 'MKCOL' && r.url().includes('/remote.php/dav/files/'), + ) + + await this.page.locator('[data-cy-upload-picker]') + .getByRole('button', { name: 'New' }) + .click() + await this.page.locator('[data-cy-upload-picker-menu-entry="newFolder"]') + .getByRole('menuitem') + .click() + + const dialog = this.page.locator('[data-cy-files-new-node-dialog]') + await dialog.getByRole('textbox').fill(folderName) + await dialog.locator('[data-cy-files-new-node-dialog-submit]').click() + + await created + await this.getRowForFile(folderName).waitFor({ state: 'visible' }) + } }