/* * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { Locator, Page } from '@playwright/test' import { escapeAttributeValue } from '../utils/css.ts' export class FilesListPage { constructor(protected readonly page: Page) {} /** * 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 { await this.page.goto(viewId ? `apps/files/${viewId}` : 'apps/files') await this.page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) } getRowForFile(filename: string): Locator { return this.page.locator(`[data-cy-files-list-row-name="${escapeAttributeValue(filename)}"]`) } getRowForFileId(fileid: number): Locator { return this.page.locator(`[data-cy-files-list-row-fileid="${fileid}"]`) } /** All file rows currently rendered in the list (e.g. for count assertions). */ getRows(): Locator { return this.page.locator('[data-cy-files-list-row-fileid]') } /** The per-row selection checkboxes. */ getRowCheckboxes(): Locator { return this.page.locator('[data-cy-files-list-row-checkbox]') } /** The per-row selection checkboxes that are currently checked (i.e. selected rows). */ getSelectedRowCheckboxes(): Locator { return this.getRowCheckboxes().getByRole('checkbox', { checked: true }) } private getActionsButtonForFile(filename: string): Locator { return this.getRowForFile(filename) .getByRole('button', { name: 'Actions' }) } /** * Open a row's actions menu and return the menu popover locator. Keyed on a * row Locator so it serves both name- and fileid-addressed rows. */ private async openActionsMenuForRow(row: Locator): Promise { await row.hover() const actionsButton = row.getByRole('button', { name: 'Actions' }) await actionsButton.scrollIntoViewIfNeeded() // force: true to avoid issues with the sticky file list header await actionsButton.click({ force: true }) const menuId = await actionsButton.getAttribute('aria-controls') const menu = this.page.locator(`#${menuId}`) await menu.waitFor({ state: 'visible' }) return menu } private async triggerActionForRow(row: Locator, actionId: string): Promise { const menu = await this.openActionsMenuForRow(row) const actionEntry = this.getActionButtonInMenu(menu, actionId) await actionEntry.waitFor({ state: 'visible' }) await actionEntry.click() } /** * Open the row actions menu for a file and return the menu popover locator. * Use this when a test needs to inspect a menu entry (e.g. its label) before * clicking; for a plain "open and click" use {@link triggerActionForFile}. */ async openActionsMenuForFile(filename: string): Promise { return this.openActionsMenuForRow(this.getRowForFile(filename)) } getActionButtonInMenu(menu: Locator, actionId: string): Locator { // The action button has role="menuitem", so use tag selector not getByRole return menu.locator(`[data-cy-files-list-row-action="${actionId}"] button`) } async triggerActionForFile(filename: string, actionId: string): Promise { await this.triggerActionForRow(this.getRowForFile(filename), actionId) } /** * Like {@link triggerActionForFile} but addresses the row by file id. Trashbin * rows are keyed by id because a deleted file's name is no longer unique (the * same name can be trashed several times). */ async triggerActionForFileId(fileid: number, actionId: string): Promise { await this.triggerActionForRow(this.getRowForFileId(fileid), actionId) } /** * A file-list-level action button rendered in the list header (e.g. * "empty-trash"), as opposed to a per-row or selection action. */ getListActionButton(actionId: string): Locator { return this.page.locator(`[data-cy-files-list-action="${actionId}"]`) } async triggerListAction(actionId: string): Promise { // .last(): the action can render both inline and inside the overflow menu; // the last match is the actionable one await this.getListActionButton(actionId).last().click({ force: true }) } getFavoriteIconForFile(filename: string): Locator { return this.getRowForFile(filename).getByRole('img', { name: 'Favorite' }) } /** * The inline "Download" button rendered on a row for the default download action. */ getDownloadButtonForFile(filename: string): Locator { return this.getRowForFile(filename).getByRole('button', { name: 'Download' }) } private getSelectAllCheckbox(): Locator { return this.page.locator('[data-cy-files-list-selection-checkbox]') .getByRole('checkbox') } async selectAll(): Promise { await this.getSelectAllCheckbox().click({ force: true }) } /** * Clear the current selection via the master checkbox. It is a toggle, so it * clicks the same control as {@link selectAll}; call it while rows are * selected to deselect them all. */ async deselectAll(): Promise { await this.getSelectAllCheckbox().click({ force: true }) } /** * Select a single row's checkbox. Pass `{ shift: true }` to extend the * selection as a range from the previously selected row. Range selection * reads the global keyboard store, so Shift is held with real keyboard * events rather than a click modifier. */ async selectRowForFile(filename: string, { shift = false }: { shift?: boolean } = {}): Promise { // The checkbox is visually hidden inside NcCheckboxRadioSwitch, so force the interaction const checkbox = this.getRowForFile(filename) .getByRole('checkbox', { name: /Toggle selection/ }) if (shift) { await this.page.keyboard.down('Shift') await checkbox.click({ force: true }) await this.page.keyboard.up('Shift') } else { await checkbox.check({ force: true }) } } /** * The toolbar that replaces the list header once one or more rows are selected. */ getSelectionActionsToolbar(): Locator { return this.page.locator('[data-cy-files-list-selection-actions]') } private getSelectionActionsButton(): Locator { return this.getSelectionActionsToolbar().getByRole('button', { name: 'Actions' }) } /** * Open the bulk-selection actions menu. Pair with {@link getSelectionActionEntry} * to inspect an entry (e.g. assert it is visible) before acting; for a plain * "open and click" use {@link triggerSelectionAction}. */ async openSelectionActionsMenu(): Promise { await this.getSelectionActionsButton().click({ force: true }) } /** * A selection action entry. Matched at page level on the product-owned * attribute because selection actions can render inline or inside the menu popover. */ getSelectionActionEntry(actionId: string): Locator { return this.page.locator(`[data-cy-files-list-selection-action="${actionId}"]`) } async triggerSelectionAction(actionId: string): Promise { await this.openSelectionActionsMenu() // NcActionButton renders as