Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 0 additions & 163 deletions cypress/e2e/files/favorites.cy.ts

This file was deleted.

111 changes: 111 additions & 0 deletions tests/playwright/e2e/files/files-favorites.spec.ts
Original file line number Diff line number Diff line change
@@ -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/<path>; 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<void>): Promise<void> {
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)
})
})
6 changes: 6 additions & 0 deletions tests/playwright/support/fixtures/files-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -34,6 +36,10 @@ export const test = baseTest.extend<FilesFixtures>({
await use(new FilesListPage(page))
},

filesNavigation: async ({ page }, use) => {
await use(new FilesNavigationPage(page))
},

filesSidebar: async ({ page }, use) => {
await use(new FilesSidebarPage(page))
},
Expand Down
25 changes: 22 additions & 3 deletions tests/playwright/support/sections/FilesListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export class FilesListPage {
.getByRole('button', { name: 'Actions' })
}

async triggerActionForFile(filename: string, actionId: string): Promise<void> {
/**
* 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<Locator> {
const row = this.getRowForFile(filename)
await row.hover()

Expand All @@ -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<void> {
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<void> {
await this.page.locator('[data-cy-files-list-selection-checkbox]')
.getByRole('checkbox')
Expand Down
33 changes: 33 additions & 0 deletions tests/playwright/support/sections/FilesNavigationPage.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await this.getNavigationItem(viewId)
.getByRole('button', { name: 'Open menu' })
.click()
}
}
15 changes: 15 additions & 0 deletions tests/playwright/support/sections/FilesSidebarPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
await this.sidebar().getByRole('button', { name: 'Close sidebar' }).click()
}
}
Loading