mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
test(files): migrate favorites e2e from Cypress to Playwright
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
parent
426cbeb192
commit
87f8d78424
6 changed files with 187 additions and 166 deletions
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
111
tests/playwright/e2e/files/files-favorites.spec.ts
Normal file
111
tests/playwright/e2e/files/files-favorites.spec.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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<FilesFixtures>({
|
|||
await use(new FilesListPage(page))
|
||||
},
|
||||
|
||||
filesNavigation: async ({ page }, use) => {
|
||||
await use(new FilesNavigationPage(page))
|
||||
},
|
||||
|
||||
filesSidebar: async ({ page }, use) => {
|
||||
await use(new FilesSidebarPage(page))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
33
tests/playwright/support/sections/FilesNavigationPage.ts
Normal file
33
tests/playwright/support/sections/FilesNavigationPage.ts
Normal file
|
|
@ -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,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…
Reference in a new issue