Skip to content

Commit 3c7487a

Browse files
committed
test(files): migrate files selection e2e from Cypress to Playwright
1 parent 692f5f3 commit 3c7487a

3 files changed

Lines changed: 110 additions & 84 deletions

File tree

cypress/e2e/files/files-selection.cy.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 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 { uploadContent } from '../../support/utils/dav.ts'
8+
9+
// Names sort ascending, so the on-screen order is:
10+
// archive.zip, audio.mp3, document.pdf, image.jpg, readme.md, video.mp4, welcome.txt
11+
const files: Record<string, string> = {
12+
'image.jpg': 'image/jpeg',
13+
'document.pdf': 'application/pdf',
14+
'archive.zip': 'application/zip',
15+
'audio.mp3': 'audio/mpeg',
16+
'video.mp4': 'video/mp4',
17+
'readme.md': 'text/markdown',
18+
'welcome.txt': 'text/plain',
19+
}
20+
const filesCount = Object.keys(files).length
21+
22+
test.describe('Files: Select files', () => {
23+
test.beforeEach(async ({ page, user, filesListPage }) => {
24+
// Uploading welcome.txt overwrites the auto-created one, so the list holds exactly these files
25+
for (const [name, mime] of Object.entries(files)) {
26+
await uploadContent(page.request, user, Buffer.alloc(0), mime, `/${name}`)
27+
}
28+
await filesListPage.open()
29+
})
30+
31+
test('selects and deselects all files', async ({ page, filesListPage }) => {
32+
await expect(filesListPage.getRows()).toHaveCount(filesCount)
33+
await expect(filesListPage.getRowCheckboxes()).toHaveCount(filesCount)
34+
35+
await filesListPage.selectAll()
36+
await expect(page.getByText(`${filesCount} selected`)).toBeVisible()
37+
await expect(filesListPage.getSelectedRowCheckboxes()).toHaveCount(filesCount)
38+
39+
await filesListPage.deselectAll()
40+
await expect(page.getByText(/\d+ selected/)).toHaveCount(0)
41+
await expect(filesListPage.getSelectedRowCheckboxes()).toHaveCount(0)
42+
})
43+
44+
test('selects an arbitrary subset of files', async ({ page, filesListPage }) => {
45+
const subset = ['image.jpg', 'document.pdf', 'audio.mp3', 'readme.md']
46+
47+
for (const name of subset) {
48+
await filesListPage.selectRowForFile(name)
49+
}
50+
51+
await expect(page.getByText(`${subset.length} selected`)).toBeVisible()
52+
await expect(filesListPage.getSelectedRowCheckboxes()).toHaveCount(subset.length)
53+
})
54+
55+
test('selects a range of files with the shift key', async ({ page, filesListPage }) => {
56+
// audio.mp3 -> readme.md spans audio.mp3, document.pdf, image.jpg, readme.md
57+
await filesListPage.selectRowForFile('audio.mp3')
58+
await filesListPage.selectRowForFile('readme.md', { shift: true })
59+
60+
await expect(page.getByText('4 selected')).toBeVisible()
61+
await expect(filesListPage.getSelectedRowCheckboxes()).toHaveCount(4)
62+
})
63+
})

tests/playwright/support/sections/FilesListPage.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ export class FilesListPage {
2525
return this.page.locator(`[data-cy-files-list-row-fileid="${fileid}"]`)
2626
}
2727

28+
/** All file rows currently rendered in the list (e.g. for count assertions). */
29+
getRows(): Locator {
30+
return this.page.locator('[data-cy-files-list-row-fileid]')
31+
}
32+
33+
/** The per-row selection checkboxes. */
34+
getRowCheckboxes(): Locator {
35+
return this.page.locator('[data-cy-files-list-row-checkbox]')
36+
}
37+
38+
/** The per-row selection checkboxes that are currently checked (i.e. selected rows). */
39+
getSelectedRowCheckboxes(): Locator {
40+
return this.getRowCheckboxes().locator('input:checked')
41+
}
42+
2843
private getActionsButtonForFile(filename: string): Locator {
2944
return this.getRowForFile(filename)
3045
.getByRole('button', { name: 'Actions' })
@@ -66,17 +81,42 @@ export class FilesListPage {
6681
return this.getRowForFile(filename).getByRole('img', { name: 'Favorite' })
6782
}
6883

69-
async selectAll(): Promise<void> {
70-
await this.page.locator('[data-cy-files-list-selection-checkbox]')
84+
private getSelectAllCheckbox(): Locator {
85+
return this.page.locator('[data-cy-files-list-selection-checkbox]')
7186
.getByRole('checkbox')
72-
.click({ force: true })
7387
}
7488

75-
async selectRowForFile(filename: string): Promise<void> {
76-
// The checkbox is visually hidden inside NcCheckboxRadioSwitch, so force the check
77-
await this.getRowForFile(filename)
89+
async selectAll(): Promise<void> {
90+
await this.getSelectAllCheckbox().click({ force: true })
91+
}
92+
93+
/**
94+
* Clear the current selection via the master checkbox. It is a toggle, so it
95+
* clicks the same control as {@link selectAll}; call it while rows are
96+
* selected to deselect them all.
97+
*/
98+
async deselectAll(): Promise<void> {
99+
await this.getSelectAllCheckbox().click({ force: true })
100+
}
101+
102+
/**
103+
* Select a single row's checkbox. Pass `{ shift: true }` to extend the
104+
* selection as a range from the previously selected row. Range selection
105+
* reads the global keyboard store, so Shift is held with real keyboard
106+
* events rather than a click modifier.
107+
*/
108+
async selectRowForFile(filename: string, { shift = false }: { shift?: boolean } = {}): Promise<void> {
109+
// The checkbox is visually hidden inside NcCheckboxRadioSwitch, so force the interaction
110+
const checkbox = this.getRowForFile(filename)
78111
.getByRole('checkbox', { name: /Toggle selection/ })
79-
.check({ force: true })
112+
113+
if (shift) {
114+
await this.page.keyboard.down('Shift')
115+
await checkbox.click({ force: true })
116+
await this.page.keyboard.up('Shift')
117+
} else {
118+
await checkbox.check({ force: true })
119+
}
80120
}
81121

82122
/**

0 commit comments

Comments
 (0)