Skip to content

Commit 4afadf9

Browse files
authored
Merge pull request #61219 from nextcloud/test/migrate-files-trashbin-playwright
test(files): migrate files/trashbin e2e from Cypress to Playwright
2 parents f8ad326 + a430a14 commit 4afadf9

9 files changed

Lines changed: 369 additions & 220 deletions

File tree

cypress/e2e/files_trashbin/files-trash-action.cy.ts

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

cypress/e2e/files_trashbin/files.cy.ts

Lines changed: 0 additions & 137 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from '../../support/fixtures/files-page.ts'
7+
import { rm, uploadContent } from '../../support/utils/dav.ts'
8+
9+
const FILE_COUNT = 5
10+
11+
test.describe('files_trashbin: empty trashbin action', () => {
12+
test.beforeEach(async ({ page, user }) => {
13+
// Create FILE_COUNT files and move them all to the trash
14+
for (let index = 0; index < FILE_COUNT; index++) {
15+
await uploadContent(page.request, user, '<content>', 'text/plain', `/file${index}.txt`)
16+
await rm(page.request, user, `/file${index}.txt`)
17+
}
18+
})
19+
20+
test('can empty trashbin', async ({ page, filesListPage }) => {
21+
await filesListPage.open()
22+
// Home holds only the default welcome file and offers no empty-trash action
23+
await expect(filesListPage.getRows()).toHaveCount(1)
24+
await expect(filesListPage.getListActionButton('empty-trash')).toHaveCount(0)
25+
26+
await filesListPage.open('trashbin')
27+
await expect(filesListPage.getRows()).toHaveCount(FILE_COUNT)
28+
29+
const emptied = page.waitForResponse((r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/trashbin/'))
30+
await filesListPage.triggerListAction('empty-trash')
31+
32+
// Confirm in the dialog
33+
await page.getByRole('dialog')
34+
.getByRole('button', { name: 'Empty deleted files' })
35+
.click()
36+
37+
expect((await emptied).status()).toBe(204)
38+
await expect(filesListPage.getRows()).toHaveCount(0)
39+
})
40+
41+
test('cancelling the empty trashbin action does not delete anything', async ({ page, filesListPage }) => {
42+
await filesListPage.open('trashbin')
43+
await expect(filesListPage.getRows()).toHaveCount(FILE_COUNT)
44+
45+
await filesListPage.triggerListAction('empty-trash')
46+
47+
// Cancel the dialog: no request is sent and the files remain
48+
await page.getByRole('dialog')
49+
.getByRole('button', { name: 'Cancel' })
50+
.click()
51+
52+
await expect(filesListPage.getRows()).toHaveCount(FILE_COUNT)
53+
})
54+
})
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Page } from '@playwright/test'
7+
import type { TrashbinListPage } from '../../support/sections/TrashbinListPage.ts'
8+
9+
import { readFile } from 'node:fs/promises'
10+
import { expect, test } from '../../support/fixtures/files-trashbin-page.ts'
11+
import { mkdir, rm, uploadContent } from '../../support/utils/dav.ts'
12+
import { ALL_PERMISSIONS, createShare, ShareType } from '../../support/utils/sharing.ts'
13+
import { setUserDisplayName } from '../../support/utils/users.ts'
14+
15+
test.describe('files_trashbin: download files', () => {
16+
let fileIds: [number, number]
17+
18+
test.beforeEach(async ({ page, user, filesListPage }) => {
19+
const first = await uploadContent(page.request, user, '<content>', 'text/plain', '/file.txt')
20+
await rm(page.request, user, '/file.txt')
21+
const second = await uploadContent(page.request, user, '<content>', 'text/plain', '/other-file.txt')
22+
await rm(page.request, user, '/other-file.txt')
23+
fileIds = [Number(first), Number(second)]
24+
25+
await filesListPage.open('trashbin')
26+
})
27+
28+
test('can download a file', async ({ page, filesListPage }) => {
29+
await expect(filesListPage.getRowForFileId(fileIds[0])).toBeVisible()
30+
await expect(filesListPage.getRowForFileId(fileIds[1])).toBeVisible()
31+
32+
await expectFileDownload(page, () => filesListPage.triggerActionForFileId(fileIds[0], 'download'))
33+
})
34+
35+
test('can download a file using the default action', async ({ page, filesListPage }) => {
36+
await expectFileDownload(page, () => {
37+
// The inline "Download" button is the row's default action; force past the sticky header
38+
return filesListPage.getRowForFileId(fileIds[0])
39+
.getByRole('button', { name: 'Download' })
40+
.click({ force: true })
41+
})
42+
})
43+
44+
// Trashbin has no bulk download: the webdav zip-folder plugin does not work for
45+
// the trashbin (and never did with the legacy ajax download either).
46+
test('does not offer bulk download', async ({ page, filesListPage }) => {
47+
await expect(filesListPage.getRowCheckboxes()).toHaveCount(2)
48+
await filesListPage.selectAll()
49+
await expect(page.getByText('2 selected')).toBeVisible()
50+
51+
await expect(filesListPage.getSelectionActionEntry('restore')).toBeVisible()
52+
await expect(filesListPage.getSelectionActionEntry('download')).toHaveCount(0)
53+
})
54+
})
55+
56+
test.describe('files_trashbin: file row', () => {
57+
test('shows data for a file deleted by the owner', async ({ user, aliceRequest, filesListPage }) => {
58+
const fileId = Number(await uploadContent(aliceRequest, user, '<content>', 'text/plain', '/test-file.txt'))
59+
await rm(aliceRequest, user, '/test-file.txt')
60+
61+
await filesListPage.open('trashbin')
62+
63+
// The owner's own deletions render as "You" regardless of display name
64+
await expectTrashbinRow(filesListPage, fileId, 'test-file .txt', 'All files', 'You')
65+
})
66+
67+
test('shows data for a file deleted by a sharee in a group share', async ({ user, aliceRequest, bob, bobRequest, group, filesListPage }) => {
68+
await setUserDisplayName(bobRequest, bob.userId, 'Bob')
69+
await mkdir(aliceRequest, user, '/Shared')
70+
await createShare(aliceRequest, '/Shared', group, ALL_PERMISSIONS, ShareType.GROUP)
71+
72+
const fileId = Number(await uploadContent(aliceRequest, user, '<content>', 'text/plain', '/Shared/test-file.txt'))
73+
// Bob (the sharee) deletes the file from his view of the shared folder
74+
await rm(bobRequest, bob, '/Shared/test-file.txt')
75+
76+
await filesListPage.open('trashbin')
77+
78+
await expectTrashbinRow(filesListPage, fileId, 'test-file .txt', 'Shared', 'Bob')
79+
})
80+
})
81+
82+
/** Run `trigger`, then assert it downloaded `file.txt` with the expected content. */
83+
async function expectFileDownload(page: Page, trigger: () => Promise<void>) {
84+
const downloadPromise = page.waitForEvent('download')
85+
await trigger()
86+
const download = await downloadPromise
87+
expect(download.suggestedFilename()).toBe('file.txt')
88+
expect(await readFile(await download.path(), 'utf-8')).toBe('<content>')
89+
}
90+
91+
/** Assert a trashbin row's name and custom columns (the deleted time is always recent). */
92+
async function expectTrashbinRow(filesListPage: TrashbinListPage, rowId: number, name: string, location: string, deletedBy: string) {
93+
const row = filesListPage.getRowForFileId(rowId)
94+
await expect(row).toBeVisible()
95+
// Name and extension render as separate spans, so the composed text has a space
96+
await expect(filesListPage.fileNameCell(row)).toHaveText(name)
97+
await expect(filesListPage.originalLocationCell(row)).toHaveText(location)
98+
await expect(filesListPage.deletedByCell(row)).toHaveText(deletedBy)
99+
// Match any relative-time string ("a few seconds ago", "a minute ago", …)
100+
// rather than a fixed string that breaks when setup is slow.
101+
await expect(filesListPage.deletedAtCell(row)).toHaveText(/ago/)
102+
}

0 commit comments

Comments
 (0)