Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f354a67
chore(test): split setOnline and setOffline
max-nextcloud Nov 3, 2025
79a075b
chore(test): add offline fixture to playwright
max-nextcloud Nov 3, 2025
c612b18
chore(test): get request token from /csrftoken
max-nextcloud Nov 3, 2025
f22c61c
chore(test): split request token fixture from random user
max-nextcloud Nov 3, 2025
2e96c26
chore(test): cleanup unused type and context
max-nextcloud Nov 3, 2025
1df8359
chore(test): mimetype change in playwright
max-nextcloud Nov 3, 2025
5c00d5a
chore(test): basic editor page object model
max-nextcloud Nov 3, 2025
8035dfb
chore(test): file class for handling uploaded file
max-nextcloud Nov 3, 2025
a9f995f
chore(test): file.open()
max-nextcloud Nov 3, 2025
e2f52b3
chore(test): with file.move() and file.close()
max-nextcloud Nov 3, 2025
93652d7
chore(test): with editor.typeHeading()
max-nextcloud Nov 3, 2025
d8eb8f5
chore(test): change mimetype both ways
max-nextcloud Nov 3, 2025
b227e3a
chore(test): store test-results folder as artifacts
max-nextcloud Nov 4, 2025
86ecbe3
chore(test): move File class to separate file
max-nextcloud Nov 4, 2025
4b6ebc0
chore(test): use EditorSection in offline test
max-nextcloud Nov 7, 2025
e174949
chore(test) .withOpenMenu() to access submenus
max-nextcloud Nov 7, 2025
5c0da42
chore(test): write offline and come back online
max-nextcloud Nov 7, 2025
3f211d5
chore(test): reduce nesting in offline test
max-nextcloud Nov 7, 2025
46dde73
chore(test): wait for close request when closing
max-nextcloud Nov 7, 2025
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 @@ -54,5 +54,5 @@ jobs:
if: always()
with:
name: playwright-report
path: playwright-report/
path: test-results/
retention-days: 30
39 changes: 39 additions & 0 deletions playwright/e2e/change-mime-type.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect, mergeTests } from '@playwright/test'
import { test as editorTest } from '../support/fixtures/editor'
import { test as randomUserTest } from '../support/fixtures/random-user'
import { test as uploadFileTest } from '../support/fixtures/upload-file'

const test = mergeTests(editorTest, randomUserTest, uploadFileTest)

test.beforeEach(async ({ file }) => {
await file.open()
})

test.describe('Changing mimetype from markdown to plaintext', () => {
test('resets the document session and indexed db', async ({ editor, file }) => {
await editor.typeHeading('Hello world')
await file.close()
await file.move('test.txt')
await file.open()
await expect(editor.content).toHaveText('## Hello world')
await expect(editor.getHeading()).not.toBeVisible()
})
})

test.describe('Changing mimetype from plain to markdown', () => {
test.use({ fileName: 'empty.txt' })

test('resets the document session and indexed db', async ({ editor, file }) => {
await editor.type('## Hello world')
await expect(editor.content).toHaveText('## Hello world')
await file.close()
await file.move('test.md')
await file.open()
await expect(editor.getHeading({ name: 'Hello world' })).toBeVisible()
})
})
93 changes: 38 additions & 55 deletions playwright/e2e/offline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,55 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

const test = mergeTests(randomUserTest, uploadFileTest)
const test = mergeTests(editorTest, offlineTest, randomUserTest, uploadFileTest)

const setOnline = async (client: CDPSession, online: boolean): Promise<void> => {
if (online) {
await client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await client.send('Network.disable')
} else {
await client.send('Network.enable')
await client.send('Network.emulateNetworkConditions', {
offline: true,
latency: 0,
downloadThroughput: 0,
uploadThroughput: 0,
})
}
}
// As we switch on and off the network
// we cannot run tests in parallel.
test.describe.configure({ mode: 'serial' })

test.beforeEach(async ({ page, file }) => {
await page.goto(`f/${file.fileId}`)
test.beforeEach(async ({ file }) => {
await file.open()
})

test.describe('Offline', () => {
test('Offline state indicator', async ({ context, page }) => {
await expect(page.locator('.session-list')).toBeVisible()
await expect(page.locator('.offline-state')).not.toBeVisible()
test('Offline state indicator', async ({ editor, setOffline }) => {
await expect(editor.sessionList).toBeVisible()
await expect(editor.offlineState).not.toBeVisible()

const client = await context.newCDPSession(page)
await setOnline(client, false)
await setOffline()

await expect(page.locator('.session-list')).not.toBeVisible()
await expect(page.locator('.offline-state')).toBeVisible()

await setOnline(client, true)
})
await expect(editor.sessionList).not.toBeVisible()
await expect(editor.offlineState).toBeVisible()
})

test('Disabled upload and link file when offline', async ({ context, page }) => {
await page.locator('[data-text-action-entry="insert-link"]').click()
await expect(
page.locator('[data-text-action-entry="insert-link-file"] button'),
).toBeEnabled()
await page.locator('[data-text-action-entry="insert-link"]').click()
await expect(
page.locator('[data-text-action-entry="insert-attachment"] button'),
).toBeEnabled()
test('Disabled upload and link file when offline', async ({
editor,
setOffline,
}) => {
const linkToFile = editor.getMenu('insert-link-file')
await editor.withOpenMenu('insert-link', () => expect(linkToFile).toBeEnabled())
await expect(editor.getMenu('insert-attachment')).toBeEnabled()

const client = await context.newCDPSession(page)
await setOnline(client, false)
await setOffline()

await page.locator('[data-text-action-entry="insert-link"]').click()
await expect(
page.locator('[data-text-action-entry="insert-link-file"] button'),
).toBeDisabled()
await page.locator('[data-text-action-entry="insert-link"]').click()
await expect(
page.locator('[data-text-action-entry="insert-attachment"] button'),
).toBeDisabled()
await editor.withOpenMenu('insert-link', () => expect(linkToFile).toBeDisabled())
await expect(editor.getMenu('insert-attachment')).toBeDisabled()
})

await setOnline(client, true)
})
test('typing offline and coming back online', async ({
editor,
setOffline,
setOnline,
}) => {
await expect(editor.locator).toBeVisible()
await setOffline()
await editor.typeHeading('Hello world')
await setOnline()
await expect(editor.offlineState).not.toBeVisible()
await expect(editor.saveIndicator).toHaveAttribute('title', /Unsaved changes/)
})
69 changes: 69 additions & 0 deletions playwright/support/fixtures/File.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

export class File {
name: string
page: Page
requestToken: string
id?: number

constructor(name: string, page: Page, requestToken: string) {
this.name = name
this.page = page
this.requestToken = requestToken
}

async upload(fileContent: string) {

// Upload file via WebDAV using page.request with requesttoken header
const response = await this.page.request.put(
`/remote.php/webdav/${this.name}`,
{
data: fileContent,
headers: {
'Content-Type': 'text/markdown',
'requesttoken': this.requestToken,
},
},
)

if (!response.ok()) {
throw new Error(`Failed to upload file: ${response.status()} ${response.statusText()}`)
}

// Extract file ID from response headers
const ocFileId = response.headers()['oc-fileid']
const fileId = ocFileId ? Number(ocFileId.split('oc')?.[0]) : 0
this.id = fileId
}

async open() {
await this.page.goto(`f/${this.id}`)
await expect(this.page.getByLabel(this.name, { exact: true }))
.toBeVisible()
}

async close() {
await this.page.getByRole('button', { name: 'Close', exact: true }).click()
await this.page.waitForRequest(/close/)
await expect(this.page.getByLabel(this.name, { exact: true }))
.not.toBeVisible()
}

async move(newName: string) {
await this.page.request.fetch(
`/remote.php/webdav/${this.name}`,
{
headers: {
Destination: `/remote.php/webdav/${newName}`,
'requesttoken': this.requestToken,
},
method: 'MOVE',
})
this.name = newName
}
}
18 changes: 18 additions & 0 deletions playwright/support/fixtures/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as baseTest } from '@playwright/test'
import { EditorSection } from '../sections/EditorSection'

interface EditorFixture {
editor: EditorSection
}

export const test = baseTest.extend<EditorFixture>({
editor: async ({ page }, use) => {
const editor = new EditorSection(page)
await use(editor)
},
})
46 changes: 46 additions & 0 deletions playwright/support/fixtures/offline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as base, type CDPSession } from '@playwright/test'

interface OfflineFixture {
setOffline: () => Promise<void>
setOnline: () => Promise<void>
}

const setClientOnline = async (client: CDPSession): Promise<void> => {
await client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await client.send('Network.disable')
}

const setClientOffline = async (client: CDPSession): Promise<void> => {
await client.send('Network.enable')
await client.send('Network.emulateNetworkConditions', {
offline: true,
latency: 0,
downloadThroughput: 0,
uploadThroughput: 0,
})
}

/**
* setOffline will turn the network off for the rest of the test and then on again.
*/
export const test = base.extend<OfflineFixture>({
setOffline: async ({ context, page }, use) => {
const client = await context.newCDPSession(page)
await use (() => setClientOffline(client))
await setClientOnline(client)
},
setOnline: async ({ context, page }, use) => {
const client = await context.newCDPSession(page)
await use (() => setClientOnline(client))
},
})
13 changes: 0 additions & 13 deletions playwright/support/fixtures/random-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { type User } from '@nextcloud/e2e-test-server'

interface RandomUserFixture {
user: User
requestToken: string
}

/**
Expand All @@ -21,18 +20,6 @@ export const test = base.extend<RandomUserFixture>({
const user = await createRandomUser()
await use(user)
},
requestToken: async ({ page }, use) => {
// Navigate to get the page context and extract request token
await page.goto('/')

// Get the request token from the page context
const token = await page.evaluate(() => {
// @ts-expect-error - OC is a global variable
return window.OC?.requestToken || ''
})

await use(token)
},
page: async ({ browser, baseURL, user }, use) => {
// Important: make sure we authenticate in a clean environment by unsetting storage state.
const page = await browser.newPage({
Expand Down
24 changes: 24 additions & 0 deletions playwright/support/fixtures/request-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as base } from '@playwright/test'

export interface RequestTokenFixture {
requestToken: string
}

/**
* This test fixture ensures a new random user is created and used for the test (current page)
*/
export const test = base.extend<RequestTokenFixture>({
requestToken: async ({ page }, use) => {
const tokenResponse = await page.request.get('./csrftoken', {
failOnStatusCode: true,
})
const token = (await tokenResponse.json()).token

await use(token)
},
})
Loading
Loading