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
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
merge-multiple: true

- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
run: npx playwright merge-reports --config tests/playwright/merge.config.ts ./all-blob-reports

- name: Upload HTML report
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
Expand Down
88 changes: 88 additions & 0 deletions tests/playwright/e2e/create-new-folder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { runOcc } from '@nextcloud/e2e-test-server/docker'
import { expect, mergeTests } from '@playwright/test'
import { test as e2eeTest } from '../support/fixtures/e2ee-user.ts'
import { test as filesTest } from '../support/fixtures/files-app.ts'
import { test as settingsTest } from '../support/fixtures/personal-settings.ts'

const test = mergeTests(e2eeTest, filesTest, settingsTest)

test.describe('creating subfolders', () => {
test.beforeAll(async () => {

Check failure on line 15 in tests/playwright/e2e/create-new-folder.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-tests (2, 2)

[webkit] › tests/playwright/e2e/create-new-folder.spec.ts:20:2 › creating subfolders › create a subfolder

1) [webkit] › tests/playwright/e2e/create-new-folder.spec.ts:20:2 › creating subfolders › create a subfolder "beforeAll" hook timeout of 30000ms exceeded. 13 | 14 | test.describe('creating subfolders', () => { > 15 | test.beforeAll(async () => { | ^ 16 | await runOcc(['config:system:set', 'skeletondirectory', '--value='], { verbose: true }) 17 | await runOcc(['config:system:set', 'templatedirectory', '--value='], { verbose: true }) 18 | }) at /home/runner/work/end_to_end_encryption/end_to_end_encryption/tests/playwright/e2e/create-new-folder.spec.ts:15:7
await runOcc(['config:system:set', 'skeletondirectory', '--value='], { verbose: true })
await runOcc(['config:system:set', 'templatedirectory', '--value='], { verbose: true })
})

test('create a subfolder', async ({ filesApp, page, mnemonic }) => {
const folderName = globalThis.crypto.randomUUID()

await filesApp.openFilesApp()
await filesApp.openNewMenu()
.then((menu) => menu.createNewE2eeFolder())
.then((dialog) => dialog.fillMnemonic(mnemonic))
.then((dialog) => dialog.createFolder(folderName))
await filesApp.openFileOrFolder(folderName)

// create subfolder
const metadataUpdate = page.waitForResponse((res) => res.request().method() === 'PUT'
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))
await filesApp.openNewMenu()
.then((menu) => menu.createNewFolder())
.then((dialog) => dialog.createFolder('subfolder'))
await metadataUpdate

// see subfolder is created
await expect(filesApp.getFileOrFolder('subfolder')).toBeVisible()

await filesApp.openFilesApp()
await filesApp.openFileOrFolder(folderName)
await filesApp.getMnemonicDialog()
.fillAndSubmit(mnemonic)

// still visible after reload
await expect(filesApp.getFileOrFolder('subfolder')).toBeVisible()
})

test('create a sub-subfolder', async ({ filesApp, page, mnemonic }) => {
const folderName = globalThis.crypto.randomUUID()

await filesApp.openFilesApp()
await filesApp.openNewMenu()
.then((menu) => menu.createNewE2eeFolder())
.then((dialog) => dialog.fillMnemonic(mnemonic))
.then((dialog) => dialog.createFolder(folderName))
await filesApp.openFileOrFolder(folderName)

// create subfolder
await filesApp.openNewMenu()
.then((menu) => menu.createNewFolder())
.then((dialog) => dialog.createFolder('subfolder'))
await page.waitForResponse((res) => res.request().method() === 'PUT'
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))

// create sub-subfolder
await filesApp.openFileOrFolder('subfolder')
await filesApp.openNewMenu()
.then((menu) => menu.createNewFolder())
.then((dialog) => dialog.createFolder('sub-subfolder'))
await page.waitForResponse((res) => res.request().method() === 'PUT'
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))

// see sub-subfolder is created
await expect(filesApp.getFileOrFolder('sub-subfolder')).toBeVisible()

await filesApp.openFilesApp()
await filesApp.openFileOrFolder(folderName)
await filesApp.getMnemonicDialog()
.fillAndSubmit(mnemonic)

// still visible after reload
await expect(filesApp.getFileOrFolder('subfolder')).toBeVisible()
await filesApp.openFileOrFolder('subfolder')
await expect(filesApp.getFileOrFolder('sub-subfolder')).toBeVisible()
})
})
11 changes: 11 additions & 0 deletions tests/playwright/merge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

// Needed to merge multiple Playwright reports
// when they are ran on self-hosted and github runners (different test directories are used)
export default {
testDir: 'tests/playwright/e2e',
reporter: [['html', { open: 'never' }]],
}
73 changes: 73 additions & 0 deletions tests/playwright/support/fixtures/e2ee-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
import { test as baseTest, expect } from '@playwright/test'
import { existsSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { FilesAppPage } from '../sections/FilesAppPage.ts'
import { PersonalSettingsPage } from '../sections/PersonalSettingsPage.ts'

export const test = baseTest.extend<{ mnemonic: string }, { workerStorageState: { path: string, mnemonic: string } }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState.path),

mnemonic: ({ workerStorageState }, use) => use(workerStorageState.mnemonic),

// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({ browser }, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex
const path = resolve(test.info().project.outputDir, `.auth/${id}.json`)

if (existsSync(path)) {
// Reuse existing authentication state if any.
await use({ path, mnemonic: await readFile(path.replace('.json', '.mnemonic'), 'utf-8') })
return
}

// Important: make sure we authenticate in a clean environment by unsetting storage state.
const page = await browser.newPage({ storageState: undefined, baseURL: baseTest.info().project.use.baseURL })

// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await createRandomUser()
await login(page.request, account)

// setup e2ee
const personalSettings = new PersonalSettingsPage(page)
await personalSettings.openSettingsPage()
await personalSettings.enableBrowserE2ee()
const filesApp = new FilesAppPage(page)
await filesApp.openFilesApp()
// setup e2ee
const newMenu = await filesApp.openNewMenu()
await expect(newMenu.getNewEncryptedFolderEntry()).toBeVisible()
const dialog = await newMenu.createNewE2eeFolder()
await dialog.buttonSetupEncryption.click()
// see the recovery phrase
await expect(dialog.codeRecoveryPhrase).toHaveText(/(\w+ ){11}\w+/)
const mnemonic = (await dialog.codeRecoveryPhrase.textContent())!
await expect(dialog.buttonContinue).not.toBeDisabled({ timeout: 10000 })
await dialog.buttonContinue.click()
await expect(dialog.inputFolderName).toBeVisible()
await dialog.inputFolderName.fill('test-folder')
await dialog.createFolder('test-folder')
await expect(dialog.dialogLocator).toHaveCount(0)

// wait for the folder to appear in the file list
await expect(filesApp.tableFilesList).toBeVisible()
await expect(filesApp.getFileOrFolder('test-folder')).toBeVisible()

await page.context().storageState({ path })
await page.close()
await writeFile(path.replace('.json', '.mnemonic'), mnemonic)

await use({ path, mnemonic })
}, { scope: 'worker' }],
})
20 changes: 18 additions & 2 deletions tests/playwright/support/sections/FilesAppPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,47 @@

import type { Locator, Page } from '@playwright/test'

import { expect } from '@playwright/test'
import { SectionMnemonicDialog } from './SectionMnemonicDialog.ts'
import { SectionNewMenu } from './SectionNewMenu.ts'

export class FilesAppPage {
public readonly buttonNewMenuLocator: Locator
public readonly dialogMnemonicLocator: Locator
public readonly tableFilesList: Locator

constructor(public readonly page: Page) {
this.tableFilesList = this.page.getByRole('table', { name: /List of your files and folders/i })
this.buttonNewMenuLocator = this.page.getByRole('button', {
name: 'New',
})
this.dialogMnemonicLocator = this.page.getByRole('dialog', { name: 'Enter your 12 words mnemonic' })
}

public async openFilesApp(): Promise<void> {
await this.page.goto('/apps/files')
}

public async openNewMenu(): Promise<SectionNewMenu> {
await this.buttonNewMenuLocator.click()
return new SectionNewMenu(this.page)
await this.buttonNewMenuLocator.first().click()
const menu = new SectionNewMenu(this.page)
await expect(menu.menuLocator).toBeVisible()
return menu
}

public getFileOrFolder(name: string): Locator {
return this.tableFilesList
.getByRole('row')
.filter({ has: this.page.getByRole('cell', { name }) })
}

public openFileOrFolder(name: string): Promise<void> {
return this.getFileOrFolder(name)
.getByRole('button', { name: `Open folder ${name}` })
.click()

Check failure on line 45 in tests/playwright/support/sections/FilesAppPage.ts

View workflow job for this annotation

GitHub Actions / playwright-tests (2, 2)

[webkit] › tests/playwright/e2e/create-new-folder.spec.ts:50:2 › creating subfolders › create a sub-subfolder

2) [webkit] › tests/playwright/e2e/create-new-folder.spec.ts:50:2 › creating subfolders › create a sub-subfolder Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('table', { name: /List of your files and folders/i }).getByRole('row').filter({ has: getByRole('cell', { name: 'fbe0d600-1a0a-423f-ac4e-b5bc562bf2e7' }) }).getByRole('button', { name: 'Open folder fbe0d600-1a0a-423f-ac4e-b5bc562bf2e7' }) at support/sections/FilesAppPage.ts:45 43 | return this.getFileOrFolder(name) 44 | .getByRole('button', { name: `Open folder ${name}` }) > 45 | .click() | ^ 46 | } 47 | 48 | public getMnemonicDialog(): SectionMnemonicDialog { at FilesAppPage.openFileOrFolder (/home/runner/work/end_to_end_encryption/end_to_end_encryption/tests/playwright/support/sections/FilesAppPage.ts:45:5) at /home/runner/work/end_to_end_encryption/end_to_end_encryption/tests/playwright/e2e/create-new-folder.spec.ts:79:18

Check failure on line 45 in tests/playwright/support/sections/FilesAppPage.ts

View workflow job for this annotation

GitHub Actions / playwright-tests (2, 2)

[webkit] › tests/playwright/e2e/create-new-folder.spec.ts:20:2 › creating subfolders › create a subfolder

1) [webkit] › tests/playwright/e2e/create-new-folder.spec.ts:20:2 › creating subfolders › create a subfolder Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for getByRole('table', { name: /List of your files and folders/i }).getByRole('row').filter({ has: getByRole('cell', { name: '641a30ce-fcef-45ef-8435-a83706538916' }) }).getByRole('button', { name: 'Open folder 641a30ce-fcef-45ef-8435-a83706538916' }) at support/sections/FilesAppPage.ts:45 43 | return this.getFileOrFolder(name) 44 | .getByRole('button', { name: `Open folder ${name}` }) > 45 | .click() | ^ 46 | } 47 | 48 | public getMnemonicDialog(): SectionMnemonicDialog { at FilesAppPage.openFileOrFolder (/home/runner/work/end_to_end_encryption/end_to_end_encryption/tests/playwright/support/sections/FilesAppPage.ts:45:5) at /home/runner/work/end_to_end_encryption/end_to_end_encryption/tests/playwright/e2e/create-new-folder.spec.ts:42:18
}

public getMnemonicDialog(): SectionMnemonicDialog {
return new SectionMnemonicDialog(this.dialogMnemonicLocator)
}
}
29 changes: 29 additions & 0 deletions tests/playwright/support/sections/SectionCreateE2eeFolderDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,49 @@

import type { Locator, Page } from '@playwright/test'

import { expect } from '@playwright/test'

export class SectionCreateE2eeFolderDialog {
public readonly dialogLocator: Locator
public readonly buttonContinue: Locator
public readonly buttonCreateFolder: Locator
public readonly buttonSetupEncryption: Locator
public readonly buttonSubmitMnemonic: Locator
public readonly checkboxConsent: Locator
public readonly codeRecoveryPhrase: Locator
public readonly inputFolderName: Locator
public readonly inputMnemonic: Locator

constructor(public readonly page: Page) {
this.dialogLocator = page.getByRole('dialog', { name: 'Create new encrypted folder' })
this.buttonSetupEncryption = this.dialogLocator.getByRole('button', { name: /Setup encryption/i })
this.buttonContinue = this.dialogLocator.getByRole('button', { name: /Continue/i })
this.buttonCreateFolder = this.dialogLocator.getByRole('button', { name: /Create folder/i })
this.buttonSubmitMnemonic = this.dialogLocator.getByRole('button', { name: /Submit/i })
this.checkboxConsent = this.dialogLocator.getByRole('checkbox', { name: /I understand the risks/i })
this.codeRecoveryPhrase = this.dialogLocator.getByRole('code')
this.inputFolderName = this.dialogLocator.getByRole('textbox', { name: /Folder name/i })
this.inputMnemonic = this.dialogLocator.getByRole('textbox', { name: /Mnemonic/i })
}

public async fillMnemonic(mnemonic: string): Promise<this> {
await expect(this.inputMnemonic).toBeVisible()
await this.inputMnemonic.fill(mnemonic)
await this.checkboxConsent.click({ force: true })
await expect(this.buttonSubmitMnemonic).not.toBeDisabled()
await this.buttonSubmitMnemonic.click()
return this
}

public async createFolder(folderName: string): Promise<this> {
const response = this.page.waitForResponse((res) => res.request().method() === 'POST'
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))

await expect(this.inputFolderName).toBeVisible()
await this.inputFolderName.fill(folderName)
await this.buttonCreateFolder.click()
await response

return this
}
}
30 changes: 30 additions & 0 deletions tests/playwright/support/sections/SectionCreateFolderDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

import type { Locator, Page } from '@playwright/test'

import { expect } from '@playwright/test'

export class SectionCreateFolderDialog {
public readonly dialogLocator: Locator
public readonly buttonCreateFolder: Locator
public readonly inputFolderName: Locator

constructor(public readonly page: Page) {
this.dialogLocator = page.getByRole('dialog', { name: 'Create new folder' })
this.buttonCreateFolder = this.dialogLocator.getByRole('button', { name: /Create/i })
this.inputFolderName = this.dialogLocator.getByRole('textbox', { name: /Folder name/i })
}

public async createFolder(folderName: string): Promise<this> {
const response = this.page.waitForResponse((res) => res.request().method() === 'MKCOL')
await this.inputFolderName.fill(folderName)
await expect(this.buttonCreateFolder).not.toBeDisabled()
await this.buttonCreateFolder.click()
await response

return this
}
}
29 changes: 29 additions & 0 deletions tests/playwright/support/sections/SectionMnemonicDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/

import type { Locator } from '@playwright/test'

import { expect } from '@playwright/test'

export class SectionMnemonicDialog {
public readonly buttonSubmit: Locator
public readonly checkboxConsent: Locator
public readonly inputMnemonic: Locator

constructor(public readonly dialogLocator: Locator) {
this.buttonSubmit = this.dialogLocator.getByRole('button', { name: /Submit/i })
this.checkboxConsent = this.dialogLocator.getByRole('checkbox', { name: /I understand the risks/i })
this.inputMnemonic = this.dialogLocator.getByRole('textbox', { name: /Mnemonic/i })
}

public async fillAndSubmit(mnemonic: string): Promise<void> {
await expect(this.dialogLocator).toBeVisible()
await this.inputMnemonic.fill(mnemonic)
await this.checkboxConsent.check({ force: true })
await expect(this.buttonSubmit).not.toBeDisabled()
await this.buttonSubmit.click()
await expect(this.dialogLocator).toHaveCount(0)
}
}
12 changes: 12 additions & 0 deletions tests/playwright/support/sections/SectionNewMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Locator, Page } from '@playwright/test'

import { expect } from '@playwright/test'
import { SectionCreateE2eeFolderDialog } from './SectionCreateE2eeFolderDialog.ts'
import { SectionCreateFolderDialog } from './SectionCreateFolderDialog.ts'

export class SectionNewMenu {
public readonly menuLocator: Locator
Expand All @@ -23,10 +24,21 @@ export class SectionNewMenu {
return this.getMenuEntry(/New encrypted folder/i)
}

public getNewFolderEntry(): Locator {
return this.getMenuEntry(/New folder/i)
}

public async createNewE2eeFolder(): Promise<SectionCreateE2eeFolderDialog> {
this.getNewEncryptedFolderEntry().click()
const section = new SectionCreateE2eeFolderDialog(this.page)
await expect(section.dialogLocator).toBeVisible()
return section
}

public async createNewFolder(): Promise<SectionCreateFolderDialog> {
this.getNewFolderEntry().click()
const section = new SectionCreateFolderDialog(this.page)
await expect(section.dialogLocator).toBeVisible()
return section
}
}
Loading