Skip to content

Commit 24e9ff4

Browse files
committed
test(playwright): add tests for creating subfolders
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 1ba7088 commit 24e9ff4

File tree

9 files changed

+279
-2
lines changed

9 files changed

+279
-2
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { runOcc } from '@nextcloud/e2e-test-server/docker'
7+
import { expect, mergeTests } from '@playwright/test'
8+
import { test as e2eeTest } from '../support/fixtures/e2ee-user.ts'
9+
import { test as filesTest } from '../support/fixtures/files-app.ts'
10+
import { test as settingsTest } from '../support/fixtures/personal-settings.ts'
11+
12+
const test = mergeTests(e2eeTest, filesTest, settingsTest)
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+
})
19+
20+
test('create a subfolder', async ({ filesApp, page, mnemonic }) => {
21+
const folderName = globalThis.crypto.randomUUID()
22+
23+
await filesApp.openFilesApp()
24+
await filesApp.openNewMenu()
25+
.then((menu) => menu.createNewE2eeFolder())
26+
.then((dialog) => dialog.fillMnemonic(mnemonic))
27+
.then((dialog) => dialog.createFolder(folderName))
28+
await filesApp.openFileOrFolder(folderName)
29+
30+
// create subfolder
31+
const metadataUpdate = page.waitForResponse((res) => res.request().method() === 'PUT'
32+
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))
33+
await filesApp.openNewMenu()
34+
.then((menu) => menu.createNewFolder())
35+
.then((dialog) => dialog.createFolder('subfolder'))
36+
await metadataUpdate
37+
38+
// see subfolder is created
39+
await expect(filesApp.getFileOrFolder('subfolder')).toBeVisible()
40+
41+
await filesApp.openFilesApp()
42+
await filesApp.openFileOrFolder(folderName)
43+
await filesApp.getMnemonicDialog()
44+
.fillAndSubmit(mnemonic)
45+
46+
// still visible after reload
47+
await expect(filesApp.getFileOrFolder('subfolder')).toBeVisible()
48+
})
49+
50+
test('create a sub-subfolder', async ({ filesApp, page, mnemonic }) => {
51+
const folderName = globalThis.crypto.randomUUID()
52+
53+
await filesApp.openFilesApp()
54+
await filesApp.openNewMenu()
55+
.then((menu) => menu.createNewE2eeFolder())
56+
.then((dialog) => dialog.fillMnemonic(mnemonic))
57+
.then((dialog) => dialog.createFolder(folderName))
58+
await filesApp.openFileOrFolder(folderName)
59+
60+
// create subfolder
61+
await filesApp.openNewMenu()
62+
.then((menu) => menu.createNewFolder())
63+
.then((dialog) => dialog.createFolder('subfolder'))
64+
await page.waitForResponse((res) => res.request().method() === 'PUT'
65+
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))
66+
67+
// create sub-subfolder
68+
await filesApp.openFileOrFolder('subfolder')
69+
await filesApp.openNewMenu()
70+
.then((menu) => menu.createNewFolder())
71+
.then((dialog) => dialog.createFolder('sub-subfolder'))
72+
await page.waitForResponse((res) => res.request().method() === 'PUT'
73+
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))
74+
75+
// see sub-subfolder is created
76+
await expect(filesApp.getFileOrFolder('sub-subfolder')).toBeVisible()
77+
78+
await filesApp.openFilesApp()
79+
await filesApp.openFileOrFolder(folderName)
80+
await filesApp.getMnemonicDialog()
81+
.fillAndSubmit(mnemonic)
82+
83+
// still visible after reload
84+
await expect(filesApp.getFileOrFolder('subfolder')).toBeVisible()
85+
await filesApp.openFileOrFolder('subfolder')
86+
await expect(filesApp.getFileOrFolder('sub-subfolder')).toBeVisible()
87+
})
88+
})
File renamed without changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
7+
import { test as baseTest, expect } from '@playwright/test'
8+
import { existsSync } from 'node:fs'
9+
import { readFile, writeFile } from 'node:fs/promises'
10+
import { resolve } from 'node:path'
11+
import { FilesAppPage } from '../sections/FilesAppPage.ts'
12+
import { PersonalSettingsPage } from '../sections/PersonalSettingsPage.ts'
13+
14+
export const test = baseTest.extend<{ mnemonic: string }, { workerStorageState: { path: string, mnemonic: string } }>({
15+
// Use the same storage state for all tests in this worker.
16+
storageState: ({ workerStorageState }, use) => use(workerStorageState.path),
17+
18+
mnemonic: ({ workerStorageState }, use) => use(workerStorageState.mnemonic),
19+
20+
// Authenticate once per worker with a worker-scoped fixture.
21+
workerStorageState: [async ({ browser }, use) => {
22+
// Use parallelIndex as a unique identifier for each worker.
23+
const id = test.info().parallelIndex
24+
const path = resolve(test.info().project.outputDir, `.auth/${id}.json`)
25+
26+
if (existsSync(path)) {
27+
// Reuse existing authentication state if any.
28+
await use({ path, mnemonic: await readFile(path.replace('.json', '.mnemonic'), 'utf-8') })
29+
return
30+
}
31+
32+
// Important: make sure we authenticate in a clean environment by unsetting storage state.
33+
const page = await browser.newPage({ storageState: undefined, baseURL: baseTest.info().project.use.baseURL })
34+
35+
// Acquire a unique account, for example create a new one.
36+
// Alternatively, you can have a list of precreated accounts for testing.
37+
// Make sure that accounts are unique, so that multiple team members
38+
// can run tests at the same time without interference.
39+
const account = await createRandomUser()
40+
await login(page.request, account)
41+
42+
// setup e2ee
43+
const personalSettings = new PersonalSettingsPage(page)
44+
await personalSettings.openSettingsPage()
45+
await personalSettings.enableBrowserE2ee()
46+
const filesApp = new FilesAppPage(page)
47+
await filesApp.openFilesApp()
48+
// setup e2ee
49+
const newMenu = await filesApp.openNewMenu()
50+
await expect(newMenu.getNewEncryptedFolderEntry()).toBeVisible()
51+
const dialog = await newMenu.createNewE2eeFolder()
52+
await dialog.buttonSetupEncryption.click()
53+
// see the recovery phrase
54+
await expect(dialog.codeRecoveryPhrase).toHaveText(/(\w+ ){11}\w+/)
55+
const mnemonic = (await dialog.codeRecoveryPhrase.textContent())!
56+
await expect(dialog.buttonContinue).not.toBeDisabled({ timeout: 10000 })
57+
await dialog.buttonContinue.click()
58+
await expect(dialog.inputFolderName).toBeVisible()
59+
await dialog.inputFolderName.fill('test-folder')
60+
await dialog.createFolder('test-folder')
61+
await expect(dialog.dialogLocator).toHaveCount(0)
62+
63+
// wait for the folder to appear in the file list
64+
await expect(filesApp.tableFilesList).toBeVisible()
65+
await expect(filesApp.getFileOrFolder('test-folder')).toBeVisible()
66+
67+
await page.context().storageState({ path })
68+
await page.close()
69+
await writeFile(path.replace('.json', '.mnemonic'), mnemonic)
70+
71+
await use({ path, mnemonic })
72+
}, { scope: 'worker' }],
73+
})

tests/playwright/support/sections/FilesAppPage.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,47 @@
55

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

8+
import { expect } from '@playwright/test'
9+
import { SectionMnemonicDialog } from './SectionMnemonicDialog.ts'
810
import { SectionNewMenu } from './SectionNewMenu.ts'
911

1012
export class FilesAppPage {
1113
public readonly buttonNewMenuLocator: Locator
14+
public readonly dialogMnemonicLocator: Locator
1215
public readonly tableFilesList: Locator
1316

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

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

2529
public async openNewMenu(): Promise<SectionNewMenu> {
26-
await this.buttonNewMenuLocator.click()
27-
return new SectionNewMenu(this.page)
30+
await this.buttonNewMenuLocator.first().click()
31+
const menu = new SectionNewMenu(this.page)
32+
await expect(menu.menuLocator).toBeVisible()
33+
return menu
2834
}
2935

3036
public getFileOrFolder(name: string): Locator {
3137
return this.tableFilesList
3238
.getByRole('row')
3339
.filter({ has: this.page.getByRole('cell', { name }) })
3440
}
41+
42+
public openFileOrFolder(name: string): Promise<void> {
43+
return this.getFileOrFolder(name)
44+
.getByRole('button', { name: `Open folder ${name}` })
45+
.click()
46+
}
47+
48+
public getMnemonicDialog(): SectionMnemonicDialog {
49+
return new SectionMnemonicDialog(this.dialogMnemonicLocator)
50+
}
3551
}

tests/playwright/support/sections/SectionCreateE2eeFolderDialog.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,49 @@
55

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

8+
import { expect } from '@playwright/test'
9+
810
export class SectionCreateE2eeFolderDialog {
911
public readonly dialogLocator: Locator
1012
public readonly buttonContinue: Locator
1113
public readonly buttonCreateFolder: Locator
1214
public readonly buttonSetupEncryption: Locator
15+
public readonly buttonSubmitMnemonic: Locator
16+
public readonly checkboxConsent: Locator
1317
public readonly codeRecoveryPhrase: Locator
1418
public readonly inputFolderName: Locator
19+
public readonly inputMnemonic: Locator
1520

1621
constructor(public readonly page: Page) {
1722
this.dialogLocator = page.getByRole('dialog', { name: 'Create new encrypted folder' })
1823
this.buttonSetupEncryption = this.dialogLocator.getByRole('button', { name: /Setup encryption/i })
1924
this.buttonContinue = this.dialogLocator.getByRole('button', { name: /Continue/i })
2025
this.buttonCreateFolder = this.dialogLocator.getByRole('button', { name: /Create folder/i })
26+
this.buttonSubmitMnemonic = this.dialogLocator.getByRole('button', { name: /Submit/i })
27+
this.checkboxConsent = this.dialogLocator.getByRole('checkbox', { name: /I understand the risks/i })
2128
this.codeRecoveryPhrase = this.dialogLocator.getByRole('code')
2229
this.inputFolderName = this.dialogLocator.getByRole('textbox', { name: /Folder name/i })
30+
this.inputMnemonic = this.dialogLocator.getByRole('textbox', { name: /Mnemonic/i })
31+
}
32+
33+
public async fillMnemonic(mnemonic: string): Promise<this> {
34+
await expect(this.inputMnemonic).toBeVisible()
35+
await this.inputMnemonic.fill(mnemonic)
36+
await this.checkboxConsent.click({ force: true })
37+
await expect(this.buttonSubmitMnemonic).not.toBeDisabled()
38+
await this.buttonSubmitMnemonic.click()
39+
return this
40+
}
41+
42+
public async createFolder(folderName: string): Promise<this> {
43+
const response = this.page.waitForResponse((res) => res.request().method() === 'POST'
44+
&& res.url().includes('/ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/'))
45+
46+
await expect(this.inputFolderName).toBeVisible()
47+
await this.inputFolderName.fill(folderName)
48+
await this.buttonCreateFolder.click()
49+
await response
50+
51+
return this
2352
}
2453
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
import type { Locator, Page } from '@playwright/test'
7+
8+
import { expect } from '@playwright/test'
9+
10+
export class SectionCreateFolderDialog {
11+
public readonly dialogLocator: Locator
12+
public readonly buttonCreateFolder: Locator
13+
public readonly inputFolderName: Locator
14+
15+
constructor(public readonly page: Page) {
16+
this.dialogLocator = page.getByRole('dialog', { name: 'Create new folder' })
17+
this.buttonCreateFolder = this.dialogLocator.getByRole('button', { name: /Create/i })
18+
this.inputFolderName = this.dialogLocator.getByRole('textbox', { name: /Folder name/i })
19+
}
20+
21+
public async createFolder(folderName: string): Promise<this> {
22+
const response = this.page.waitForResponse((res) => res.request().method() === 'MKCOL')
23+
await this.inputFolderName.fill(folderName)
24+
await expect(this.buttonCreateFolder).not.toBeDisabled()
25+
await this.buttonCreateFolder.click()
26+
await response
27+
28+
return this
29+
}
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
import type { Locator } from '@playwright/test'
7+
8+
import { expect } from '@playwright/test'
9+
10+
export class SectionMnemonicDialog {
11+
public readonly buttonSubmit: Locator
12+
public readonly checkboxConsent: Locator
13+
public readonly inputMnemonic: Locator
14+
15+
constructor(public readonly dialogLocator: Locator) {
16+
this.buttonSubmit = this.dialogLocator.getByRole('button', { name: /Submit/i })
17+
this.checkboxConsent = this.dialogLocator.getByRole('checkbox', { name: /I understand the risks/i })
18+
this.inputMnemonic = this.dialogLocator.getByRole('textbox', { name: /Mnemonic/i })
19+
}
20+
21+
public async fillAndSubmit(mnemonic: string): Promise<void> {
22+
await expect(this.dialogLocator).toBeVisible()
23+
await this.inputMnemonic.fill(mnemonic)
24+
await this.checkboxConsent.check({ force: true })
25+
await expect(this.buttonSubmit).not.toBeDisabled()
26+
await this.buttonSubmit.click()
27+
await expect(this.dialogLocator).toHaveCount(0)
28+
}
29+
}

tests/playwright/support/sections/SectionNewMenu.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Locator, Page } from '@playwright/test'
77

88
import { expect } from '@playwright/test'
99
import { SectionCreateE2eeFolderDialog } from './SectionCreateE2eeFolderDialog.ts'
10+
import { SectionCreateFolderDialog } from './SectionCreateFolderDialog.ts'
1011

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

27+
public getNewFolderEntry(): Locator {
28+
return this.getMenuEntry(/New folder/i)
29+
}
30+
2631
public async createNewE2eeFolder(): Promise<SectionCreateE2eeFolderDialog> {
2732
this.getNewEncryptedFolderEntry().click()
2833
const section = new SectionCreateE2eeFolderDialog(this.page)
2934
await expect(section.dialogLocator).toBeVisible()
3035
return section
3136
}
37+
38+
public async createNewFolder(): Promise<SectionCreateFolderDialog> {
39+
this.getNewFolderEntry().click()
40+
const section = new SectionCreateFolderDialog(this.page)
41+
await expect(section.dialogLocator).toBeVisible()
42+
return section
43+
}
3244
}

0 commit comments

Comments
 (0)