Skip to content

Commit 8fc2e1c

Browse files
authored
Merge pull request #61153 from nextcloud/test/migrate-files-regression-playwright
test(files): migrate recent-view and regression specs from Cypress to…
2 parents 954fc50 + b256bbe commit 8fc2e1c

7 files changed

Lines changed: 161 additions & 131 deletions

File tree

cypress/e2e/files/duplicated-node-regression.cy.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

cypress/e2e/files/files-xml-regression.cy.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

cypress/e2e/files/recent-view.cy.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test, expect } from '../../support/fixtures/files-page.ts'
7+
import { mkdir } from '../../support/utils/dav.ts'
8+
9+
test.describe('Files: Duplicated node regression', () => {
10+
test.beforeEach(async ({ page, user, filesListPage }) => {
11+
await mkdir(page.request, user, '/only once')
12+
await filesListPage.open()
13+
})
14+
15+
/**
16+
* Regression: https://github.com/nextcloud/server/issues/47904
17+
* Deleting a node and recreating it with the same name left two rows in the list.
18+
*/
19+
test('does not duplicate a node after delete and recreate', async ({ page, filesListPage }) => {
20+
await expect(filesListPage.getRowForFile('only once')).toBeVisible()
21+
22+
const deleted = page.waitForResponse(
23+
(r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'),
24+
)
25+
await filesListPage.triggerActionForFile('only once', 'delete')
26+
await deleted
27+
await expect(filesListPage.getRowForFile('only once')).toHaveCount(0)
28+
29+
await filesListPage.createFolder('only once')
30+
31+
await expect(filesListPage.getRowForFile('only once')).toHaveCount(1)
32+
})
33+
})
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test, expect } from '../../support/fixtures/files-page.ts'
7+
import { uploadContent } from '../../support/utils/dav.ts'
8+
9+
/**
10+
* Regression: https://github.com/nextcloud/server/issues/43331
11+
* Files whose names contain XML entities (e.g. "&.txt") were wrongly
12+
* displayed and could no longer be renamed or deleted.
13+
*/
14+
test.describe('Files: XML entities in file names', () => {
15+
test('renames a file to a name with XML entities and keeps it after reload', async ({ page, user, filesListPage }) => {
16+
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/and.txt')
17+
await filesListPage.open()
18+
19+
await filesListPage.triggerActionForFile('and.txt', 'rename')
20+
const input = filesListPage.getRenameInputForFile('and.txt')
21+
await expect(input).toBeVisible()
22+
23+
const renamed = page.waitForResponse(
24+
(r) => r.request().method() === 'MOVE' && r.url().includes('/remote.php/dav/files/'),
25+
)
26+
await input.fill('&.txt')
27+
await input.press('Enter')
28+
await renamed
29+
30+
// The literal name is kept, not decoded to "&.txt"
31+
await expect(filesListPage.getRowForFile('&.txt')).toBeVisible()
32+
await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0)
33+
34+
await page.reload()
35+
await expect(filesListPage.getRowForFile('&.txt')).toBeVisible()
36+
await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0)
37+
})
38+
39+
test('can delete a file whose name contains XML entities', async ({ page, user, filesListPage }) => {
40+
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/&.txt')
41+
await filesListPage.open()
42+
43+
await expect(filesListPage.getRowForFile('&.txt')).toBeVisible()
44+
45+
const deleted = page.waitForResponse(
46+
(r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'),
47+
)
48+
await filesListPage.triggerActionForFile('&.txt', 'delete')
49+
await deleted
50+
51+
await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0)
52+
53+
await page.reload()
54+
await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0)
55+
await expect(filesListPage.getRowForFile('&.txt')).toHaveCount(0)
56+
})
57+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test, expect } from '../../support/fixtures/files-page.ts'
7+
import { uploadContent } from '../../support/utils/dav.ts'
8+
9+
test.describe('Files: Recent view', () => {
10+
test.beforeEach(async ({ page, user }) => {
11+
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt')
12+
})
13+
14+
test('shows a recently created file in the recent view', async ({ filesListPage }) => {
15+
await filesListPage.open('recent')
16+
await expect(filesListPage.getRowForFile('file.txt')).toBeVisible()
17+
})
18+
19+
/**
20+
* Regression: the recent view loaded files with an invalid source, so the
21+
* delete action failed. Deleting from the recent view must work and remove
22+
* the file everywhere.
23+
*/
24+
test('can delete a file from the recent view', async ({ page, filesListPage }) => {
25+
await filesListPage.open('recent')
26+
await expect(filesListPage.getRowForFile('file.txt')).toBeVisible()
27+
28+
const deleted = page.waitForResponse(
29+
(r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'),
30+
)
31+
await filesListPage.triggerActionForFile('file.txt', 'delete')
32+
await deleted
33+
34+
await expect(filesListPage.getRowForFile('file.txt')).toHaveCount(0)
35+
36+
// Gone from the default view too
37+
await filesListPage.open()
38+
await expect(filesListPage.getRowForFile('file.txt')).toHaveCount(0)
39+
})
40+
})

tests/playwright/support/sections/FilesListPage.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import type { Locator, Page } from '@playwright/test'
88
export class FilesListPage {
99
constructor(private readonly page: Page) {}
1010

11-
async open(): Promise<void> {
12-
await this.page.goto('apps/files')
11+
/**
12+
* Open the files app. Pass a view id (e.g. 'recent') to open that view
13+
* instead of the default "All files" list.
14+
*/
15+
async open(viewId?: string): Promise<void> {
16+
await this.page.goto(viewId ? `apps/files/${viewId}` : 'apps/files')
1317
await this.page.locator('[data-cy-files-list]').waitFor({ state: 'visible' })
1418
}
1519

@@ -127,4 +131,29 @@ export class FilesListPage {
127131
.click()
128132
}
129133
}
134+
135+
/**
136+
* Create a folder through the upload picker's "New" menu and wait for the
137+
* MKCOL to land. The upload-picker and new-node-dialog hooks are product-owned
138+
* data-cy attributes (no stable accessible name to target by role).
139+
*/
140+
async createFolder(folderName: string): Promise<void> {
141+
const created = this.page.waitForResponse(
142+
(r) => r.request().method() === 'MKCOL' && r.url().includes('/remote.php/dav/files/'),
143+
)
144+
145+
await this.page.locator('[data-cy-upload-picker]')
146+
.getByRole('button', { name: 'New' })
147+
.click()
148+
await this.page.locator('[data-cy-upload-picker-menu-entry="newFolder"]')
149+
.getByRole('menuitem')
150+
.click()
151+
152+
const dialog = this.page.locator('[data-cy-files-new-node-dialog]')
153+
await dialog.getByRole('textbox').fill(folderName)
154+
await dialog.locator('[data-cy-files-new-node-dialog-submit]').click()
155+
156+
await created
157+
await this.getRowForFile(folderName).waitFor({ state: 'visible' })
158+
}
130159
}

0 commit comments

Comments
 (0)