Merge pull request #61153 from nextcloud/test/migrate-files-regression-playwright

test(files): migrate recent-view and regression specs from Cypress to…
This commit is contained in:
Louis 2026-06-10 17:56:13 +02:00 committed by GitHub
commit 8fc2e1c2b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 161 additions and 131 deletions

View file

@ -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)
})

View file

@ -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')
})
})

View file

@ -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')
})
})

View file

@ -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)
})
})

View file

@ -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)
})
})

View file

@ -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)
})
})

View file

@ -8,8 +8,12 @@ import type { Locator, Page } from '@playwright/test'
export class FilesListPage {
constructor(private readonly page: Page) {}
async open(): Promise<void> {
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<void> {
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<void> {
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' })
}
}