Skip to content

Commit 87f8d78

Browse files
committed
test(files): migrate favorites e2e from Cypress to Playwright
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
1 parent 426cbeb commit 87f8d78

6 files changed

Lines changed: 187 additions & 166 deletions

File tree

cypress/e2e/files/favorites.cy.ts

Lines changed: 0 additions & 163 deletions
This file was deleted.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 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 { test, expect } from '../../support/fixtures/files-page.ts'
8+
import { mkdir, rm, uploadContent } from '../../support/utils/dav.ts'
9+
10+
/**
11+
* Run an action that toggles a favorite and wait for the server to store it.
12+
* Toggling hits POST /apps/files/api/v1/files/<path>; the listener is registered
13+
* before the action and awaited after, so later assertions see the stored state.
14+
*/
15+
async function toggleFavorite(page: Page, path: string, action: () => Promise<void>): Promise<void> {
16+
const encoded = path.split('/').map(encodeURIComponent).join('/')
17+
const response = page.waitForResponse(
18+
(r) => r.url().includes(`/apps/files/api/v1/files/${encoded}`)
19+
&& r.request().method() === 'POST',
20+
)
21+
await action()
22+
await response
23+
}
24+
25+
test.describe('Files: Favorites', () => {
26+
test.beforeEach(async ({ page, user, filesListPage }) => {
27+
// New users get welcome.txt — remove it so the list contains only our test files
28+
await rm(page.request, user, '/welcome.txt')
29+
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt')
30+
await mkdir(page.request, user, '/new folder')
31+
await filesListPage.open()
32+
})
33+
34+
test('marks a file as favorite from the row actions', async ({ page, filesListPage }) => {
35+
await expect(filesListPage.getRowForFile('file.txt')).toBeVisible()
36+
37+
const menu = await filesListPage.openActionsMenuForFile('file.txt')
38+
const favoriteAction = filesListPage.getActionButtonInMenu(menu, 'favorite')
39+
await expect(favoriteAction).toContainText('Add to favorites')
40+
41+
await toggleFavorite(page, 'file.txt', () => favoriteAction.click())
42+
43+
await expect(filesListPage.getFavoriteIconForFile('file.txt')).toBeVisible()
44+
})
45+
46+
test('un-marks a file as favorite from the row actions', async ({ page, filesListPage }) => {
47+
await expect(filesListPage.getRowForFile('file.txt')).toBeVisible()
48+
49+
// Favorite it first
50+
await toggleFavorite(page, 'file.txt', () => filesListPage.triggerActionForFile('file.txt', 'favorite'))
51+
await expect(filesListPage.getFavoriteIconForFile('file.txt')).toBeVisible()
52+
53+
// Re-open the menu — the action now offers to remove the favorite
54+
const menu = await filesListPage.openActionsMenuForFile('file.txt')
55+
const favoriteAction = filesListPage.getActionButtonInMenu(menu, 'favorite')
56+
await expect(favoriteAction).toContainText('Remove from favorites')
57+
58+
await toggleFavorite(page, 'file.txt', () => favoriteAction.click())
59+
60+
await expect(filesListPage.getFavoriteIconForFile('file.txt')).toHaveCount(0)
61+
})
62+
63+
test('shows favorite folders in the navigation', async ({ page, filesListPage, filesNavigation }) => {
64+
const favoritesNav = filesNavigation.getNavigationItem('favorites')
65+
const favoriteEntry = favoritesNav.getByRole('link', { name: 'new folder' })
66+
67+
await expect(favoritesNav).toBeVisible()
68+
await expect(favoriteEntry).toHaveCount(0)
69+
70+
// Favorite the folder — it appears as a (collapsed) child of the favorites view
71+
await toggleFavorite(page, 'new folder', () => filesListPage.triggerActionForFile('new folder', 'favorite'))
72+
await filesNavigation.expandNavigationItem('favorites')
73+
await expect(favoriteEntry).toBeVisible()
74+
75+
// Un-favorite — it disappears again
76+
await toggleFavorite(page, 'new folder', () => filesListPage.triggerActionForFile('new folder', 'favorite'))
77+
await expect(favoriteEntry).toHaveCount(0)
78+
})
79+
80+
test('marks a folder as favorite from the sidebar', async ({ page, filesListPage, filesNavigation, filesSidebar }) => {
81+
await expect(filesListPage.getRowForFile('new folder')).toBeVisible()
82+
83+
const favoriteEntry = filesNavigation.getNavigationItem('favorites').getByRole('link', { name: 'new folder' })
84+
await expect(favoriteEntry).toHaveCount(0)
85+
86+
// Open the sidebar for the folder
87+
await filesListPage.triggerActionForFile('new folder', 'details')
88+
await expect(filesSidebar.sidebar()).toBeVisible()
89+
90+
await toggleFavorite(page, 'new folder', () => filesSidebar.triggerAction('Favorite'))
91+
92+
await filesSidebar.close()
93+
await expect(filesSidebar.sidebar()).not.toBeVisible()
94+
await expect(filesListPage.getFavoriteIconForFile('new folder')).toBeVisible()
95+
96+
// Favorite survives a reload
97+
await page.reload()
98+
await expect(filesListPage.getRowForFile('new folder')).toBeVisible()
99+
await expect(filesListPage.getFavoriteIconForFile('new folder')).toBeVisible()
100+
101+
// Un-favorite again from the sidebar
102+
await filesListPage.triggerActionForFile('new folder', 'details')
103+
await expect(filesSidebar.sidebar()).toBeVisible()
104+
105+
await toggleFavorite(page, 'new folder', () => filesSidebar.triggerAction('Unfavorite'))
106+
107+
await filesSidebar.close()
108+
await expect(filesSidebar.sidebar()).not.toBeVisible()
109+
await expect(filesListPage.getFavoriteIconForFile('new folder')).toHaveCount(0)
110+
})
111+
})

tests/playwright/support/fixtures/files-page.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
88
import { test as baseTest } from '@playwright/test'
99
import type { User } from '@nextcloud/e2e-test-server'
1010
import { FilesListPage } from '../sections/FilesListPage.ts'
11+
import { FilesNavigationPage } from '../sections/FilesNavigationPage.ts'
1112
import { FilesSidebarPage } from '../sections/FilesSidebarPage.ts'
1213

1314
type FilesFixtures = {
1415
user: User
1516
filesListPage: FilesListPage
17+
filesNavigation: FilesNavigationPage
1618
filesSidebar: FilesSidebarPage
1719
}
1820

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

39+
filesNavigation: async ({ page }, use) => {
40+
await use(new FilesNavigationPage(page))
41+
},
42+
3743
filesSidebar: async ({ page }, use) => {
3844
await use(new FilesSidebarPage(page))
3945
},

tests/playwright/support/sections/FilesListPage.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ export class FilesListPage {
2626
.getByRole('button', { name: 'Actions' })
2727
}
2828

29-
async triggerActionForFile(filename: string, actionId: string): Promise<void> {
29+
/**
30+
* Open the row actions menu for a file and return the menu popover locator.
31+
* Use this when a test needs to inspect a menu entry (e.g. its label) before
32+
* clicking; for a plain "open and click" use {@link triggerActionForFile}.
33+
*/
34+
async openActionsMenuForFile(filename: string): Promise<Locator> {
3035
const row = this.getRowForFile(filename)
3136
await row.hover()
3237

@@ -36,13 +41,27 @@ export class FilesListPage {
3641
await actionsButton.click({ force: true })
3742

3843
const menuId = await actionsButton.getAttribute('aria-controls')
44+
const menu = this.page.locator(`#${menuId}`)
45+
await menu.waitFor({ state: 'visible' })
46+
return menu
47+
}
48+
49+
getActionButtonInMenu(menu: Locator, actionId: string): Locator {
3950
// The action button has role="menuitem", so use tag selector not getByRole
40-
const actionEntry = this.page
41-
.locator(`#${menuId} [data-cy-files-list-row-action="${actionId}"] button`)
51+
return menu.locator(`[data-cy-files-list-row-action="${actionId}"] button`)
52+
}
53+
54+
async triggerActionForFile(filename: string, actionId: string): Promise<void> {
55+
const menu = await this.openActionsMenuForFile(filename)
56+
const actionEntry = this.getActionButtonInMenu(menu, actionId)
4257
await actionEntry.waitFor({ state: 'visible' })
4358
await actionEntry.click()
4459
}
4560

61+
getFavoriteIconForFile(filename: string): Locator {
62+
return this.getRowForFile(filename).getByRole('img', { name: 'Favorite' })
63+
}
64+
4665
async selectAll(): Promise<void> {
4766
await this.page.locator('[data-cy-files-list-selection-checkbox]')
4867
.getByRole('checkbox')
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Locator, Page } from '@playwright/test'
7+
8+
/**
9+
* The left-hand files navigation (the view list: All files, Favorites, Recent, …).
10+
* Distinct from {@link NavigationHeaderPage}, which models the top app bar.
11+
*/
12+
export class FilesNavigationPage {
13+
constructor(private readonly page: Page) {}
14+
15+
/**
16+
* A navigation entry, e.g. the "favorites" view.
17+
* Uses the product-owned data-cy attribute set on NcAppNavigationItem.
18+
*/
19+
getNavigationItem(viewId: string): Locator {
20+
return this.page.locator(`[data-cy-files-navigation-item="${viewId}"]`)
21+
}
22+
23+
/**
24+
* Expand a collapsible navigation view to reveal its child entries.
25+
* Collapsed children are `display: none`, so they must be expanded to be visible.
26+
* "Open menu" is the accessible name of NcAppNavigationItem's collapse toggle.
27+
*/
28+
async expandNavigationItem(viewId: string): Promise<void> {
29+
await this.getNavigationItem(viewId)
30+
.getByRole('button', { name: 'Open menu' })
31+
.click()
32+
}
33+
}

tests/playwright/support/sections/FilesSidebarPage.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,19 @@ export class FilesSidebarPage {
1515
heading(name: string): Locator {
1616
return this.sidebar().getByRole('heading', { name })
1717
}
18+
19+
/**
20+
* Open the sidebar "Actions" menu and click the entry with the given name
21+
* (e.g. "Favorite" / "Unfavorite").
22+
*/
23+
async triggerAction(name: string): Promise<void> {
24+
await this.sidebar().getByRole('button', { name: 'Actions' }).click()
25+
const action = this.page.getByRole('menuitem', { name })
26+
await action.waitFor({ state: 'visible' })
27+
await action.click()
28+
}
29+
30+
async close(): Promise<void> {
31+
await this.sidebar().getByRole('button', { name: 'Close sidebar' }).click()
32+
}
1833
}

0 commit comments

Comments
 (0)