Skip to content
Open
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
1 change: 0 additions & 1 deletion core/src/OC/msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export default {
return
}

// eslint-disable-next-line @stylistic/exp-list-style
const animation = el.animate?.(
[
{ opacity: 1 },
Expand Down
13 changes: 11 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,19 @@ export default defineConfig([
},
},

// Playwright tests setup
{
name: 'server/playwright',
files: ['tests/playwright/**'],
rules: {
'no-empty-pattern': 'off', // PW needs the destructuring syntax {} for fixtures!
},
},

// Forbid commiting .only in test files (skipping tests is very unexpected)
{
name: 'server/no-only-in-tests',
files: ['cypress/**', 'apps/**/*.spec.*', 'core/**/*.spec.*'],
files: ['cypress/**', 'tests/playwright/**', 'apps/**/*.spec.*', 'core/**/*.spec.*'],
plugins: {
'no-only-tests': noOnlyTests,
},
Expand All @@ -96,7 +105,7 @@ export default defineConfig([
'composer.json',
'**/*.php',
'3rdparty/',
'tests/', // PHP tests
'tests/!(playwright)/', // PHP tests, but not Playwright tests
'**/js/',
'**/l10n/', // all translations (config only ignored in root)
'**/vendor/', // different vendors
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"postinstall": "build/demi.sh ci",
"lint": "eslint --suppressions-location build/eslint-baseline.json --no-error-on-unmatched-pattern ./cypress ./tests/playwright",
"postlint": "build/demi.sh lint",
"lint:fix": "build/demi.sh lint:fix",
"lint:fix": "concurrently 'npm run lint -- --fix' 'build/demi.sh lint:fix'",
"playwright": "playwright test",
"playwright:install": "playwright install chromium-headless-shell",
"sass": "sass --style compressed --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then printf \"$cssdir \"; fi; done)",
Expand Down
38 changes: 17 additions & 21 deletions tests/playwright/e2e/appstore/admin-settings-apps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { runOcc } from '@nextcloud/e2e-test-server'
import { expect } from '@playwright/test'
import { test } from '../../support/fixtures/admin-appstore-page.ts'
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
import { runOcc } from '@nextcloud/e2e-test-server'

test.describe('Settings: App management', () => {
test.beforeEach(async ({ page, appstorePage }) => {
test.beforeEach(async ({ appstorePage }) => {
// Disable QA testing app if already enabled
expect(await runOcc(['app:disable', 'testing']))
.toMatch(/(No such app enabled|testing .+ disabled)/)
Expand All @@ -19,22 +19,20 @@

// Open the installed apps page
await appstorePage.openInstalledApps()

// Wait for the apps table to load
await appstorePage.appsTable().waitFor({ state: 'visible', timeout: 10000 })
})

test('Can enable an installed app', async ({ page, appstorePage }) => {
// Intercept the enable app request
const enableRequest = page.waitForResponse(
(response) => response.url().includes('/ocs/v2.php/apps/appstore/api/v1/apps/enable'),
)
const enableRequest = page.waitForResponse((response) => response.url().includes('/ocs/v2.php/apps/appstore/api/v1/apps/enable'))

// Find and click the enable button for the QA testing app
await expect(appstorePage.appsTable()).toBeVisible()
const qaTestingRow = appstorePage.appRow('QA testing')
await expect(qaTestingRow).toBeVisible({ timeout: 10000 })

await appstorePage.enableButton('QA testing').click({ force: true })

// Handle password confirmation if needed
Expand All @@ -57,15 +55,13 @@

test('Can disable an installed app', async ({ page, appstorePage }) => {
// Intercept the disable app request
const disableRequest = page.waitForResponse(
(response) => response.url().includes('/ocs/v2.php/apps/appstore/api/v1/apps/disable'),
)
const disableRequest = page.waitForResponse((response) => response.url().includes('/ocs/v2.php/apps/appstore/api/v1/apps/disable'))

// Find and click the disable button for the Update notification app
await expect(appstorePage.appsTable()).toBeVisible()
const updateRow = appstorePage.appRow('Update notification')
await expect(updateRow).toBeVisible({ timeout: 10000 })

await appstorePage.disableButton('Update notification').click({ force: true })

// Handle password confirmation if needed
Expand Down Expand Up @@ -95,15 +91,15 @@

// Verify that there are only enabled apps (all have "Disable" button, no "Enable" button)
await expect(appstorePage.appsTable()).toBeVisible()

// Get all rows and verify each has a disable button and no enable button
const rows = appstorePage.appsTable().locator('tr')
const rowCount = await rows.count()
for (let i = 1; i < rowCount; i++) { // Skip header row

for (let i = 1; i < rowCount; i++) { // Skip header row
const row = rows.nth(i)
const enableButton = row.getByRole('button', { name: 'Enable' })

// Enabled apps should not have an "Enable" button
await expect(enableButton).not.toBeVisible()
}
Expand All @@ -118,15 +114,15 @@

// Verify that there are only disabled apps (all have "Enable" button, no "Disable" button)
await expect(appstorePage.appsTable()).toBeVisible()

// Get all rows and verify each has an enable button and no disable button
const rows = appstorePage.appsTable().locator('tr')
const rowCount = await rows.count()
for (let i = 1; i < rowCount; i++) { // Skip header row

for (let i = 1; i < rowCount; i++) { // Skip header row
const row = rows.nth(i)
const disableButton = row.getByRole('button', { name: 'Disable' })

// Disabled apps should not have a "Disable" button
await expect(disableButton).not.toBeVisible()
}
Expand All @@ -152,12 +148,12 @@
const sidebar = appstorePage.appSidebar()
await expect(sidebar).toBeVisible()
await expect(appstorePage.appSidebarHeader()).toContainText('QA testing')

// Verify the sidebar contains expected elements
await expect(appstorePage.viewInStoreLink()).toBeVisible()
await expect(appstorePage.appSidebarEnableButton()).toBeVisible()
await expect(appstorePage.removeButton()).toBeVisible()

// Verify version information is displayed
await expect(appstorePage.versionText()).toBeVisible()
})
Expand All @@ -167,7 +163,7 @@
await appstorePage.openEnabledApps()

// Select the updatenotification app
await appstorePage.appLink('Update Notification').scrollIntoViewIfNeeded()

Check failure on line 166 in tests/playwright/e2e/appstore/admin-settings-apps.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright tests 1 / 4

[admin-settings] › tests/playwright/e2e/appstore/admin-settings-apps.spec.ts:161:2 › Settings: App management › Limit app usage to group

1) [admin-settings] › tests/playwright/e2e/appstore/admin-settings-apps.spec.ts:161:2 › Settings: App management › Limit app usage to group Error: locator.scrollIntoViewIfNeeded: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('table').getByRole('link', { name: 'Update Notification' }) 164 | 165 | // Select the updatenotification app > 166 | await appstorePage.appLink('Update Notification').scrollIntoViewIfNeeded() | ^ 167 | await appstorePage.appLink('Update Notification').click() 168 | 169 | // Click the "Limit to groups" button at /home/runner/actions-runner/_work/server/server/tests/playwright/e2e/appstore/admin-settings-apps.spec.ts:166:53

Check failure on line 166 in tests/playwright/e2e/appstore/admin-settings-apps.spec.ts

View workflow job for this annotation

GitHub Actions / merge-reports

[admin-settings] › tests/playwright/tests/playwright/e2e/appstore/admin-settings-apps.spec.ts:161:2 › Settings: App management › Limit app usage to group

1) [admin-settings] › tests/playwright/tests/playwright/e2e/appstore/admin-settings-apps.spec.ts:161:2 › Settings: App management › Limit app usage to group Error: locator.scrollIntoViewIfNeeded: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('table').getByRole('link', { name: 'Update Notification' }) 164 | 165 | // Select the updatenotification app > 166 | await appstorePage.appLink('Update Notification').scrollIntoViewIfNeeded() | ^ 167 | await appstorePage.appLink('Update Notification').click() 168 | 169 | // Click the "Limit to groups" button at /home/runner/actions-runner/_work/server/server/tests/playwright/e2e/appstore/admin-settings-apps.spec.ts:166:53
await appstorePage.appLink('Update Notification').click()

// Click the "Limit to groups" button
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright/e2e/core/404-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'

test.describe('404 error page', () => {
test('renders 404 page with a link back to login', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright/e2e/core/header-access-levels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*/

import { expect } from '@playwright/test'
import { test as userTest } from '../../support/fixtures/random-user-session.ts'
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
import { test as userTest } from '../../support/fixtures/random-user-session.ts'
import { AccountMenuPage } from '../../support/sections/AccountMenuPage.ts'

// Regular user tests — the page fixture is logged in as a fresh random user.
Expand Down
4 changes: 2 additions & 2 deletions tests/playwright/e2e/core/header-app-menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*/

import { expect } from '@playwright/test'
import { test as userTest } from '../../support/fixtures/random-user-session.ts'
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
import { test as userTest } from '../../support/fixtures/random-user-session.ts'
import { NavigationHeaderPage } from '../../support/sections/NavigationHeaderPage.ts'

// Regular-user tests — logged in as a fresh random user.
Expand Down Expand Up @@ -65,7 +65,7 @@ adminTest.describe('Header: App menu (waffle launcher) – admin', () => {
*/
async function expectWaffleMenuContainsApps(
navigationHeader: NavigationHeaderPage,
apps: Array<{ name: string; href: string }>,
apps: Array<{ name: string, href: string }>,
): Promise<void> {
await navigationHeader.openMenu()
await expect(navigationHeader.popover()).toBeVisible()
Expand Down
1 change: 1 addition & 0 deletions tests/playwright/e2e/core/header-contacts-menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import type { User } from '@nextcloud/e2e-test-server'

import { runOcc } from '@nextcloud/e2e-test-server/docker'
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
import { expect } from '@playwright/test'
Expand Down
14 changes: 4 additions & 10 deletions tests/playwright/e2e/dav/availability.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect } from '@playwright/test'
import { User } from '@nextcloud/e2e-test-server'
import { addUser, runOcc } from '@nextcloud/e2e-test-server/docker'
import { expect } from '@playwright/test'
import { test } from '../../support/fixtures/random-user-session.ts'

test.describe('Calendar: Availability', () => {
Expand Down Expand Up @@ -35,9 +35,7 @@ test.describe('Calendar: Availability', () => {
await fridayItem.getByLabel('Pick a end time for Friday').fill('18:00')

// Wait for the PROPPATCH save request before clicking
const saveResponse = page.waitForResponse(
(r) => r.url().includes('/remote.php/dav/calendars/') && r.url().includes('/inbox') && r.request().method() === 'PROPPATCH',
)
const saveResponse = page.waitForResponse((r) => r.url().includes('/remote.php/dav/calendars/') && r.url().includes('/inbox') && r.request().method() === 'PROPPATCH')
await page.locator('#availability').getByRole('button', { name: 'Save' }).click()
await saveResponse

Expand Down Expand Up @@ -70,19 +68,15 @@ test.describe('Calendar: Availability', () => {

// Search for the replacement user via NcSelectUsers
const userSearchInput = absenceSection.getByLabel('Out of office replacement (optional)')
const searchResponse = page.waitForResponse(
(r) => r.url().includes('/apps/files_sharing/api/v1/sharees') && r.url().includes('search=replacement'),
)
const searchResponse = page.waitForResponse((r) => r.url().includes('/apps/files_sharing/api/v1/sharees') && r.url().includes('search=replacement'))
await userSearchInput.click()
await userSearchInput.fill('replacement')
await searchResponse

await page.getByRole('option', { name: 'replacement-user' }).click()

// Save and wait for the OCS POST
const saveResponse = page.waitForResponse(
(r) => r.url().includes('/apps/dav/api/v1/outOfOffice/') && r.request().method() === 'POST',
)
const saveResponse = page.waitForResponse((r) => r.url().includes('/apps/dav/api/v1/outOfOffice/') && r.request().method() === 'POST')
await absenceSection.getByRole('button', { name: 'Save' }).click()
await saveResponse

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '../../support/fixtures/files-page.ts'
import { expect, test } from '../../support/fixtures/files-page.ts'
import { mkdir } from '../../support/utils/dav.ts'

test.describe('Files: Duplicated node regression', () => {
Expand All @@ -19,9 +19,7 @@ test.describe('Files: Duplicated node regression', () => {
test('does not duplicate a node after delete and recreate', async ({ page, filesListPage }) => {
await expect(filesListPage.getRowForFile('only once')).toBeVisible()

const deleted = page.waitForResponse(
(r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'),
)
const deleted = page.waitForResponse((r) => r.request().method() === 'DELETE' && r.url().includes('/remote.php/dav/files/'))
await filesListPage.triggerActionForFile('only once', 'delete')
await deleted
await expect(filesListPage.getRowForFile('only once')).toHaveCount(0)
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright/e2e/files/files-actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '../../support/fixtures/files-page.ts'
import { expect, test } from '../../support/fixtures/files-page.ts'
import { rm, uploadContent } from '../../support/utils/dav.ts'

// A representative subset of the default actions, not the full feature set.
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright/e2e/files/files-copy-move.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '../../support/fixtures/files-page.ts'
import { expect, test } from '../../support/fixtures/files-page.ts'
import { mkdir, uploadContent } from '../../support/utils/dav.ts'

const EMPTY = Buffer.alloc(0)
Expand Down
14 changes: 5 additions & 9 deletions tests/playwright/e2e/files/files-delete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '../../support/fixtures/files-page.ts'
import { expect, test } from '../../support/fixtures/files-page.ts'
import { mkdir, uploadContent } from '../../support/utils/dav.ts'

test.describe('Files: Delete', () => {
Expand Down Expand Up @@ -36,14 +36,10 @@ test.describe('Files: Delete', () => {
await expect(page.locator('.files-list__row-icon-preview--loaded')).toHaveCount(5)

// Set up listeners for all 5 DELETE responses before triggering the action
const deleteResponses = Promise.all(
Array.from({ length: 5 }, () =>
page.waitForResponse(
(r) => r.url().includes(`/remote.php/dav/files/${user.userId}/root/`) && r.request().method() === 'DELETE',
{ timeout: 15000 },
),
),
)
const deleteResponses = Promise.all(Array.from({ length: 5 }, () => page.waitForResponse(
(r) => r.url().includes(`/remote.php/dav/files/${user.userId}/root/`) && r.request().method() === 'DELETE',
{ timeout: 15000 },
)))

await filesListPage.selectAll()
await filesListPage.triggerSelectionAction('delete')
Expand Down
7 changes: 4 additions & 3 deletions tests/playwright/e2e/files/files-download.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
*/

import type { Download, Page } from '@playwright/test'
import { readFile } from 'node:fs/promises'

import { User } from '@nextcloud/e2e-test-server'
import { addUser, runOcc } from '@nextcloud/e2e-test-server/docker'
import { login } from '@nextcloud/e2e-test-server/playwright'
import { User } from '@nextcloud/e2e-test-server'
import { test, expect } from '../../support/fixtures/files-page.ts'
import { readFile } from 'node:fs/promises'
import { expect, test } from '../../support/fixtures/files-page.ts'
import { mkdir, uploadContent } from '../../support/utils/dav.ts'
import { getZipEntries } from '../../support/utils/zip.ts'

Expand Down
9 changes: 4 additions & 5 deletions tests/playwright/e2e/files/files-favorites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/

import type { Page } from '@playwright/test'
import { test, expect } from '../../support/fixtures/files-page.ts'

import { expect, test } from '../../support/fixtures/files-page.ts'
import { mkdir, rm, uploadContent } from '../../support/utils/dav.ts'

/**
Expand All @@ -14,10 +15,8 @@ import { mkdir, rm, uploadContent } from '../../support/utils/dav.ts'
*/
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',
)
const response = page.waitForResponse((r) => r.url().includes(`/apps/files/api/v1/files/${encoded}`)
&& r.request().method() === 'POST')
await action()
await response
}
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright/e2e/files/files-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '../../support/fixtures/files-page.ts'
import { expect, test } from '../../support/fixtures/files-page.ts'
import { mkdir } from '../../support/utils/dav.ts'

test.describe('Files: Navigation', () => {
Expand Down
Loading
Loading