diff --git a/cypress/e2e/files/favorites.cy.ts b/cypress/e2e/files/favorites.cy.ts deleted file mode 100644 index e992b7e5783..00000000000 --- a/cypress/e2e/files/favorites.cy.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { User } from '@nextcloud/e2e-test-server/cypress' - -import { closeSidebar, getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts' - -describe('files: Favorites', { testIsolation: true }, () => { - let user: User - - beforeEach(() => { - cy.createRandomUser().then(($user) => { - user = $user - cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt') - cy.mkdir(user, '/new folder') - cy.login(user) - cy.visit('/apps/files') - }) - }) - - it('Mark file as favorite', () => { - // See file exists - getRowForFile('file.txt') - .should('exist') - - cy.intercept('POST', '**/apps/files/api/v1/files/file.txt').as('addToFavorites') - // Click actions - getActionButtonForFile('file.txt').click({ force: true }) - // See action is called 'Add to favorites' - cy.get('[data-cy-files-list-row-action="favorite"] > button').last() - .should('exist') - .and('contain.text', 'Add to favorites') - .click({ force: true }) - cy.wait('@addToFavorites') - // See favorites star - getRowForFile('file.txt') - .findByRole('img', { name: 'Favorite' }) - .should('exist') - }) - - it('Un-mark file as favorite', () => { - // See file exists - getRowForFile('file.txt') - .should('exist') - - cy.intercept('POST', '**/apps/files/api/v1/files/file.txt').as('addToFavorites') - // toggle favorite - triggerActionForFile('file.txt', 'favorite') - cy.wait('@addToFavorites') - - // See favorites star - getRowForFile('file.txt') - .findByRole('img', { name: 'Favorite' }) - .should('be.visible') - - // Remove favorite - // click action button - getActionButtonForFile('file.txt').click({ force: true }) - // See action is called 'Remove from favorites' - cy.get('[data-cy-files-list-row-action="favorite"] > button').last() - .should('exist') - .and('have.text', 'Remove from favorites') - .click({ force: true }) - cy.wait('@addToFavorites') - // See no favorites star anymore - getRowForFile('file.txt') - .findByRole('img', { name: 'Favorite' }) - .should('not.exist') - }) - - it('See favorite folders in navigation', () => { - cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites') - - // see navigation has no entry - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('not.exist') - - // toggle favorite - triggerActionForFile('new folder', 'favorite') - cy.wait('@addToFavorites') - - // See in navigation - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('exist') - - // toggle favorite - triggerActionForFile('new folder', 'favorite') - cy.wait('@addToFavorites') - - // See no longer in navigation - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('not.exist') - }) - - it('Mark file as favorite using the sidebar', () => { - // See file exists - getRowForFile('new folder') - .should('exist') - // see navigation has no entry - cy.get('[data-cy-files-navigation-item="favorites"]') - .should('be.visible') - .contains('new folder') - .should('not.exist') - - cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites') - // open sidebar - triggerActionForFile('new folder', 'details') - cy.get('[data-cy-sidebar]') - .should('be.visible') - - // open sidebar actions - cy.get('[data-cy-sidebar]') - .findByRole('button', { name: 'Actions' }) - .click() - // trigger menu button - cy.findAllByRole('menu') - .findByRole('menuitem', { name: 'Favorite' }) - .should('be.visible') - .click() - cy.wait('@addToFavorites') - - // close sidebar - closeSidebar() - - // See favorites star - getRowForFile('new folder') - .findByRole('img', { name: 'Favorite' }) - .should('be.visible') - - cy.reload() - getRowForFile('new folder') - .should('be.visible') - - // can unfavorite - triggerActionForFile('new folder', 'details') - cy.get('[data-cy-sidebar]') - .should('be.visible') - - cy.get('[data-cy-sidebar]') - .findByRole('button', { name: 'Actions' }) - .click() - // trigger menu button - cy.findAllByRole('menu') - .findByRole('menuitem', { name: 'Unfavorite' }) - .should('be.visible') - .click() - - cy.wait('@addToFavorites') - closeSidebar() - - getRowForFile('new folder') - .findByRole('img', { name: 'Favorite' }) - .should('not.exist') - }) -}) diff --git a/tests/playwright/e2e/files/files-favorites.spec.ts b/tests/playwright/e2e/files/files-favorites.spec.ts new file mode 100644 index 00000000000..da123222d1f --- /dev/null +++ b/tests/playwright/e2e/files/files-favorites.spec.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { Page } from '@playwright/test' +import { test, expect } from '../../support/fixtures/files-page.ts' +import { mkdir, rm, uploadContent } from '../../support/utils/dav.ts' + +/** + * Run an action that toggles a favorite and wait for the server to store it. + * Toggling hits POST /apps/files/api/v1/files/; the listener is registered + * before the action and awaited after, so later assertions see the stored state. + */ +async function toggleFavorite(page: Page, path: string, action: () => Promise): Promise { + const encoded = path.split('/').map(encodeURIComponent).join('/') + const response = page.waitForResponse( + (r) => r.url().includes(`/apps/files/api/v1/files/${encoded}`) + && r.request().method() === 'POST', + ) + await action() + await response +} + +test.describe('Files: Favorites', () => { + test.beforeEach(async ({ page, user, filesListPage }) => { + // New users get welcome.txt — remove it so the list contains only our test files + await rm(page.request, user, '/welcome.txt') + await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt') + await mkdir(page.request, user, '/new folder') + await filesListPage.open() + }) + + test('marks a file as favorite from the row actions', async ({ page, filesListPage }) => { + await expect(filesListPage.getRowForFile('file.txt')).toBeVisible() + + const menu = await filesListPage.openActionsMenuForFile('file.txt') + const favoriteAction = filesListPage.getActionButtonInMenu(menu, 'favorite') + await expect(favoriteAction).toContainText('Add to favorites') + + await toggleFavorite(page, 'file.txt', () => favoriteAction.click()) + + await expect(filesListPage.getFavoriteIconForFile('file.txt')).toBeVisible() + }) + + test('un-marks a file as favorite from the row actions', async ({ page, filesListPage }) => { + await expect(filesListPage.getRowForFile('file.txt')).toBeVisible() + + // Favorite it first + await toggleFavorite(page, 'file.txt', () => filesListPage.triggerActionForFile('file.txt', 'favorite')) + await expect(filesListPage.getFavoriteIconForFile('file.txt')).toBeVisible() + + // Re-open the menu — the action now offers to remove the favorite + const menu = await filesListPage.openActionsMenuForFile('file.txt') + const favoriteAction = filesListPage.getActionButtonInMenu(menu, 'favorite') + await expect(favoriteAction).toContainText('Remove from favorites') + + await toggleFavorite(page, 'file.txt', () => favoriteAction.click()) + + await expect(filesListPage.getFavoriteIconForFile('file.txt')).toHaveCount(0) + }) + + test('shows favorite folders in the navigation', async ({ page, filesListPage, filesNavigation }) => { + const favoritesNav = filesNavigation.getNavigationItem('favorites') + const favoriteEntry = favoritesNav.getByRole('link', { name: 'new folder' }) + + await expect(favoritesNav).toBeVisible() + await expect(favoriteEntry).toHaveCount(0) + + // Favorite the folder — it appears as a (collapsed) child of the favorites view + await toggleFavorite(page, 'new folder', () => filesListPage.triggerActionForFile('new folder', 'favorite')) + await filesNavigation.expandNavigationItem('favorites') + await expect(favoriteEntry).toBeVisible() + + // Un-favorite — it disappears again + await toggleFavorite(page, 'new folder', () => filesListPage.triggerActionForFile('new folder', 'favorite')) + await expect(favoriteEntry).toHaveCount(0) + }) + + test('marks a folder as favorite from the sidebar', async ({ page, filesListPage, filesNavigation, filesSidebar }) => { + await expect(filesListPage.getRowForFile('new folder')).toBeVisible() + + const favoriteEntry = filesNavigation.getNavigationItem('favorites').getByRole('link', { name: 'new folder' }) + await expect(favoriteEntry).toHaveCount(0) + + // Open the sidebar for the folder + await filesListPage.triggerActionForFile('new folder', 'details') + await expect(filesSidebar.sidebar()).toBeVisible() + + await toggleFavorite(page, 'new folder', () => filesSidebar.triggerAction('Favorite')) + + await filesSidebar.close() + await expect(filesSidebar.sidebar()).not.toBeVisible() + await expect(filesListPage.getFavoriteIconForFile('new folder')).toBeVisible() + + // Favorite survives a reload + await page.reload() + await expect(filesListPage.getRowForFile('new folder')).toBeVisible() + await expect(filesListPage.getFavoriteIconForFile('new folder')).toBeVisible() + + // Un-favorite again from the sidebar + await filesListPage.triggerActionForFile('new folder', 'details') + await expect(filesSidebar.sidebar()).toBeVisible() + + await toggleFavorite(page, 'new folder', () => filesSidebar.triggerAction('Unfavorite')) + + await filesSidebar.close() + await expect(filesSidebar.sidebar()).not.toBeVisible() + await expect(filesListPage.getFavoriteIconForFile('new folder')).toHaveCount(0) + }) +}) diff --git a/tests/playwright/support/fixtures/files-page.ts b/tests/playwright/support/fixtures/files-page.ts index c1eba8c96a5..35dace2b842 100644 --- a/tests/playwright/support/fixtures/files-page.ts +++ b/tests/playwright/support/fixtures/files-page.ts @@ -8,11 +8,13 @@ import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright' import { test as baseTest } from '@playwright/test' import type { User } from '@nextcloud/e2e-test-server' import { FilesListPage } from '../sections/FilesListPage.ts' +import { FilesNavigationPage } from '../sections/FilesNavigationPage.ts' import { FilesSidebarPage } from '../sections/FilesSidebarPage.ts' type FilesFixtures = { user: User filesListPage: FilesListPage + filesNavigation: FilesNavigationPage filesSidebar: FilesSidebarPage } @@ -34,6 +36,10 @@ export const test = baseTest.extend({ await use(new FilesListPage(page)) }, + filesNavigation: async ({ page }, use) => { + await use(new FilesNavigationPage(page)) + }, + filesSidebar: async ({ page }, use) => { await use(new FilesSidebarPage(page)) }, diff --git a/tests/playwright/support/sections/FilesListPage.ts b/tests/playwright/support/sections/FilesListPage.ts index 3f8918e81a8..282583b4a39 100644 --- a/tests/playwright/support/sections/FilesListPage.ts +++ b/tests/playwright/support/sections/FilesListPage.ts @@ -26,7 +26,12 @@ export class FilesListPage { .getByRole('button', { name: 'Actions' }) } - async triggerActionForFile(filename: string, actionId: string): Promise { + /** + * 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 { const row = this.getRowForFile(filename) await row.hover() @@ -36,13 +41,27 @@ export class FilesListPage { 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 + } + + getActionButtonInMenu(menu: Locator, actionId: string): Locator { // The action button has role="menuitem", so use tag selector not getByRole - const actionEntry = this.page - .locator(`#${menuId} [data-cy-files-list-row-action="${actionId}"] button`) + return menu.locator(`[data-cy-files-list-row-action="${actionId}"] button`) + } + + async triggerActionForFile(filename: string, actionId: string): Promise { + const menu = await this.openActionsMenuForFile(filename) + const actionEntry = this.getActionButtonInMenu(menu, actionId) await actionEntry.waitFor({ state: 'visible' }) await actionEntry.click() } + getFavoriteIconForFile(filename: string): Locator { + return this.getRowForFile(filename).getByRole('img', { name: 'Favorite' }) + } + async selectAll(): Promise { await this.page.locator('[data-cy-files-list-selection-checkbox]') .getByRole('checkbox') diff --git a/tests/playwright/support/sections/FilesNavigationPage.ts b/tests/playwright/support/sections/FilesNavigationPage.ts new file mode 100644 index 00000000000..49c65b0722a --- /dev/null +++ b/tests/playwright/support/sections/FilesNavigationPage.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { Locator, Page } from '@playwright/test' + +/** + * The left-hand files navigation (the view list: All files, Favorites, Recent, …). + * Distinct from {@link NavigationHeaderPage}, which models the top app bar. + */ +export class FilesNavigationPage { + constructor(private readonly page: Page) {} + + /** + * A navigation entry, e.g. the "favorites" view. + * Uses the product-owned data-cy attribute set on NcAppNavigationItem. + */ + getNavigationItem(viewId: string): Locator { + return this.page.locator(`[data-cy-files-navigation-item="${viewId}"]`) + } + + /** + * Expand a collapsible navigation view to reveal its child entries. + * Collapsed children are `display: none`, so they must be expanded to be visible. + * "Open menu" is the accessible name of NcAppNavigationItem's collapse toggle. + */ + async expandNavigationItem(viewId: string): Promise { + await this.getNavigationItem(viewId) + .getByRole('button', { name: 'Open menu' }) + .click() + } +} diff --git a/tests/playwright/support/sections/FilesSidebarPage.ts b/tests/playwright/support/sections/FilesSidebarPage.ts index dbb9de8a75c..531a6d98f33 100644 --- a/tests/playwright/support/sections/FilesSidebarPage.ts +++ b/tests/playwright/support/sections/FilesSidebarPage.ts @@ -15,4 +15,19 @@ export class FilesSidebarPage { heading(name: string): Locator { return this.sidebar().getByRole('heading', { name }) } + + /** + * Open the sidebar "Actions" menu and click the entry with the given name + * (e.g. "Favorite" / "Unfavorite"). + */ + async triggerAction(name: string): Promise { + await this.sidebar().getByRole('button', { name: 'Actions' }).click() + const action = this.page.getByRole('menuitem', { name }) + await action.waitFor({ state: 'visible' }) + await action.click() + } + + async close(): Promise { + await this.sidebar().getByRole('button', { name: 'Close sidebar' }).click() + } }