Skip to content

Commit 9cfa326

Browse files
Merge pull request #7895 from nextcloud/chore/tweak-playwright-tests
Tweak playwright tests
2 parents 66a9afc + 46dde73 commit 9cfa326

10 files changed

Lines changed: 296 additions & 103 deletions

File tree

.github/workflows/playwright.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ jobs:
5454
if: always()
5555
with:
5656
name: playwright-report
57-
path: playwright-report/
57+
path: test-results/
5858
retention-days: 30
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, mergeTests } from '@playwright/test'
7+
import { test as editorTest } from '../support/fixtures/editor'
8+
import { test as randomUserTest } from '../support/fixtures/random-user'
9+
import { test as uploadFileTest } from '../support/fixtures/upload-file'
10+
11+
const test = mergeTests(editorTest, randomUserTest, uploadFileTest)
12+
13+
test.beforeEach(async ({ file }) => {
14+
await file.open()
15+
})
16+
17+
test.describe('Changing mimetype from markdown to plaintext', () => {
18+
test('resets the document session and indexed db', async ({ editor, file }) => {
19+
await editor.typeHeading('Hello world')
20+
await file.close()
21+
await file.move('test.txt')
22+
await file.open()
23+
await expect(editor.content).toHaveText('## Hello world')
24+
await expect(editor.getHeading()).not.toBeVisible()
25+
})
26+
})
27+
28+
test.describe('Changing mimetype from plain to markdown', () => {
29+
test.use({ fileName: 'empty.txt' })
30+
31+
test('resets the document session and indexed db', async ({ editor, file }) => {
32+
await editor.type('## Hello world')
33+
await expect(editor.content).toHaveText('## Hello world')
34+
await file.close()
35+
await file.move('test.md')
36+
await file.open()
37+
await expect(editor.getHeading({ name: 'Hello world' })).toBeVisible()
38+
})
39+
})

playwright/e2e/offline.spec.ts

Lines changed: 38 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,72 +3,55 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import { type CDPSession, expect, mergeTests } from '@playwright/test'
6+
import { expect, mergeTests } from '@playwright/test'
7+
import { test as editorTest } from '../support/fixtures/editor'
8+
import { test as offlineTest } from '../support/fixtures/offline'
79
import { test as randomUserTest } from '../support/fixtures/random-user'
810
import { test as uploadFileTest } from '../support/fixtures/upload-file'
911

10-
const test = mergeTests(randomUserTest, uploadFileTest)
12+
const test = mergeTests(editorTest, offlineTest, randomUserTest, uploadFileTest)
1113

12-
const setOnline = async (client: CDPSession, online: boolean): Promise<void> => {
13-
if (online) {
14-
await client.send('Network.emulateNetworkConditions', {
15-
offline: false,
16-
latency: 0,
17-
downloadThroughput: -1,
18-
uploadThroughput: -1,
19-
})
20-
await client.send('Network.disable')
21-
} else {
22-
await client.send('Network.enable')
23-
await client.send('Network.emulateNetworkConditions', {
24-
offline: true,
25-
latency: 0,
26-
downloadThroughput: 0,
27-
uploadThroughput: 0,
28-
})
29-
}
30-
}
14+
// As we switch on and off the network
15+
// we cannot run tests in parallel.
16+
test.describe.configure({ mode: 'serial' })
3117

32-
test.beforeEach(async ({ page, file }) => {
33-
await page.goto(`f/${file.fileId}`)
18+
test.beforeEach(async ({ file }) => {
19+
await file.open()
3420
})
3521

36-
test.describe('Offline', () => {
37-
test('Offline state indicator', async ({ context, page }) => {
38-
await expect(page.locator('.session-list')).toBeVisible()
39-
await expect(page.locator('.offline-state')).not.toBeVisible()
22+
test('Offline state indicator', async ({ editor, setOffline }) => {
23+
await expect(editor.sessionList).toBeVisible()
24+
await expect(editor.offlineState).not.toBeVisible()
4025

41-
const client = await context.newCDPSession(page)
42-
await setOnline(client, false)
26+
await setOffline()
4327

44-
await expect(page.locator('.session-list')).not.toBeVisible()
45-
await expect(page.locator('.offline-state')).toBeVisible()
46-
47-
await setOnline(client, true)
48-
})
28+
await expect(editor.sessionList).not.toBeVisible()
29+
await expect(editor.offlineState).toBeVisible()
30+
})
4931

50-
test('Disabled upload and link file when offline', async ({ context, page }) => {
51-
await page.locator('[data-text-action-entry="insert-link"]').click()
52-
await expect(
53-
page.locator('[data-text-action-entry="insert-link-file"] button'),
54-
).toBeEnabled()
55-
await page.locator('[data-text-action-entry="insert-link"]').click()
56-
await expect(
57-
page.locator('[data-text-action-entry="insert-attachment"] button'),
58-
).toBeEnabled()
32+
test('Disabled upload and link file when offline', async ({
33+
editor,
34+
setOffline,
35+
}) => {
36+
const linkToFile = editor.getMenu('insert-link-file')
37+
await editor.withOpenMenu('insert-link', () => expect(linkToFile).toBeEnabled())
38+
await expect(editor.getMenu('insert-attachment')).toBeEnabled()
5939

60-
const client = await context.newCDPSession(page)
61-
await setOnline(client, false)
40+
await setOffline()
6241

63-
await page.locator('[data-text-action-entry="insert-link"]').click()
64-
await expect(
65-
page.locator('[data-text-action-entry="insert-link-file"] button'),
66-
).toBeDisabled()
67-
await page.locator('[data-text-action-entry="insert-link"]').click()
68-
await expect(
69-
page.locator('[data-text-action-entry="insert-attachment"] button'),
70-
).toBeDisabled()
42+
await editor.withOpenMenu('insert-link', () => expect(linkToFile).toBeDisabled())
43+
await expect(editor.getMenu('insert-attachment')).toBeDisabled()
44+
})
7145

72-
await setOnline(client, true)
73-
})
46+
test('typing offline and coming back online', async ({
47+
editor,
48+
setOffline,
49+
setOnline,
50+
}) => {
51+
await expect(editor.locator).toBeVisible()
52+
await setOffline()
53+
await editor.typeHeading('Hello world')
54+
await setOnline()
55+
await expect(editor.offlineState).not.toBeVisible()
56+
await expect(editor.saveIndicator).toHaveAttribute('title', /Unsaved changes/)
7457
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, type Page } from '@playwright/test'
7+
8+
export class File {
9+
name: string
10+
page: Page
11+
requestToken: string
12+
id?: number
13+
14+
constructor(name: string, page: Page, requestToken: string) {
15+
this.name = name
16+
this.page = page
17+
this.requestToken = requestToken
18+
}
19+
20+
async upload(fileContent: string) {
21+
22+
// Upload file via WebDAV using page.request with requesttoken header
23+
const response = await this.page.request.put(
24+
`/remote.php/webdav/${this.name}`,
25+
{
26+
data: fileContent,
27+
headers: {
28+
'Content-Type': 'text/markdown',
29+
'requesttoken': this.requestToken,
30+
},
31+
},
32+
)
33+
34+
if (!response.ok()) {
35+
throw new Error(`Failed to upload file: ${response.status()} ${response.statusText()}`)
36+
}
37+
38+
// Extract file ID from response headers
39+
const ocFileId = response.headers()['oc-fileid']
40+
const fileId = ocFileId ? Number(ocFileId.split('oc')?.[0]) : 0
41+
this.id = fileId
42+
}
43+
44+
async open() {
45+
await this.page.goto(`f/${this.id}`)
46+
await expect(this.page.getByLabel(this.name, { exact: true }))
47+
.toBeVisible()
48+
}
49+
50+
async close() {
51+
await this.page.getByRole('button', { name: 'Close', exact: true }).click()
52+
await this.page.waitForRequest(/close/)
53+
await expect(this.page.getByLabel(this.name, { exact: true }))
54+
.not.toBeVisible()
55+
}
56+
57+
async move(newName: string) {
58+
await this.page.request.fetch(
59+
`/remote.php/webdav/${this.name}`,
60+
{
61+
headers: {
62+
Destination: `/remote.php/webdav/${newName}`,
63+
'requesttoken': this.requestToken,
64+
},
65+
method: 'MOVE',
66+
})
67+
this.name = newName
68+
}
69+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test as baseTest } from '@playwright/test'
7+
import { EditorSection } from '../sections/EditorSection'
8+
9+
interface EditorFixture {
10+
editor: EditorSection
11+
}
12+
13+
export const test = baseTest.extend<EditorFixture>({
14+
editor: async ({ page }, use) => {
15+
const editor = new EditorSection(page)
16+
await use(editor)
17+
},
18+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test as base, type CDPSession } from '@playwright/test'
7+
8+
interface OfflineFixture {
9+
setOffline: () => Promise<void>
10+
setOnline: () => Promise<void>
11+
}
12+
13+
const setClientOnline = async (client: CDPSession): Promise<void> => {
14+
await client.send('Network.emulateNetworkConditions', {
15+
offline: false,
16+
latency: 0,
17+
downloadThroughput: -1,
18+
uploadThroughput: -1,
19+
})
20+
await client.send('Network.disable')
21+
}
22+
23+
const setClientOffline = async (client: CDPSession): Promise<void> => {
24+
await client.send('Network.enable')
25+
await client.send('Network.emulateNetworkConditions', {
26+
offline: true,
27+
latency: 0,
28+
downloadThroughput: 0,
29+
uploadThroughput: 0,
30+
})
31+
}
32+
33+
/**
34+
* setOffline will turn the network off for the rest of the test and then on again.
35+
*/
36+
export const test = base.extend<OfflineFixture>({
37+
setOffline: async ({ context, page }, use) => {
38+
const client = await context.newCDPSession(page)
39+
await use (() => setClientOffline(client))
40+
await setClientOnline(client)
41+
},
42+
setOnline: async ({ context, page }, use) => {
43+
const client = await context.newCDPSession(page)
44+
await use (() => setClientOnline(client))
45+
},
46+
})

playwright/support/fixtures/random-user.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { type User } from '@nextcloud/e2e-test-server'
99

1010
interface RandomUserFixture {
1111
user: User
12-
requestToken: string
1312
}
1413

1514
/**
@@ -21,18 +20,6 @@ export const test = base.extend<RandomUserFixture>({
2120
const user = await createRandomUser()
2221
await use(user)
2322
},
24-
requestToken: async ({ page }, use) => {
25-
// Navigate to get the page context and extract request token
26-
await page.goto('/')
27-
28-
// Get the request token from the page context
29-
const token = await page.evaluate(() => {
30-
// @ts-expect-error - OC is a global variable
31-
return window.OC?.requestToken || ''
32-
})
33-
34-
await use(token)
35-
},
3623
page: async ({ browser, baseURL, user }, use) => {
3724
// Important: make sure we authenticate in a clean environment by unsetting storage state.
3825
const page = await browser.newPage({
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test as base } from '@playwright/test'
7+
8+
export interface RequestTokenFixture {
9+
requestToken: string
10+
}
11+
12+
/**
13+
* This test fixture ensures a new random user is created and used for the test (current page)
14+
*/
15+
export const test = base.extend<RequestTokenFixture>({
16+
requestToken: async ({ page }, use) => {
17+
const tokenResponse = await page.request.get('./csrftoken', {
18+
failOnStatusCode: true,
19+
})
20+
const token = (await tokenResponse.json()).token
21+
22+
await use(token)
23+
},
24+
})

0 commit comments

Comments
 (0)