mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
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:
commit
8fc2e1c2b3
7 changed files with 161 additions and 131 deletions
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
57
tests/playwright/e2e/files/files-xml-regression.spec.ts
Normal file
57
tests/playwright/e2e/files/files-xml-regression.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
40
tests/playwright/e2e/files/recent-view.spec.ts
Normal file
40
tests/playwright/e2e/files/recent-view.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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' })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue