Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
533 changes: 103 additions & 430 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@
"@nextcloud/vite-config": "^1.7.2",
"@playwright/test": "^1.60.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^26.1.2",
"@vitejs/plugin-vue2": "^2.3.4",
"@vitest/coverage-v8": "^4.1.10",
"@vue/test-utils": "^1.3.0 <2",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^11.3.0",
"cypress": "^15.12.0",
"cypress": "^15.19.0",
"cypress-split": "^1.24.31",
"cypress-vite": "^1.8.0",
"eslint-config-prettier": "^10.1.8",
Expand Down
53 changes: 35 additions & 18 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,44 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

import { defineConfig, devices } from '@playwright/test'

/**
* Used locally - i.e. if `CI` is not set as an environment variable.
*/
const LOCAL_CONFIG = {
// Just the html report with the traces
reporter: 'list',
} as const

/**
* Used on CI - i.e. if `CI` is set as an environment variable.
*/
const CI_CONFIG = {
// ensure no `test.only` is left in the code causing false positives
forbidOnly: true,
// blob (so we can merge reports and download them for inspection),
// dot (so we have a quick overview in the logs while the tests are running)
// github (to have annotations in the PR)
reporter: [['blob'], ['line'], ['github']] as ReporterDescription[],
retries: 1,
timeout: 45_000,
// we shard to speed up the tests so no parallelism in workers
workers: 1,
} as const

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './playwright',

/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI ? 'github' : 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
...(process.env.CI ? CI_CONFIG : LOCAL_CONFIG),
use: {
/* Base URL to use in actions like `await page.goto('./')`. */
// Base URL to use in actions like `await page.goto('./')`.
baseURL: process.env.baseURL ?? 'http://localhost:8089/index.php/',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
// record traces but only keep them when the test fails
trace: 'on-first-retry',
},

Expand All @@ -40,6 +54,8 @@ export default defineConfig({
],

webServer: {
// Don't set `url` as it would take precedence over `wait.stdout` and tests start too early
// url: 'http://127.0.0.1:8089',
// Starts the Nextcloud docker container
command: 'npm run start:nextcloud',
// we use sigterm to notify the script to stop the container
Expand All @@ -48,11 +64,12 @@ export default defineConfig({
signal: 'SIGTERM',
timeout: 10000,
},
reuseExistingServer: !process.env.CI,
// `start-nextcloud-server.mjs` only starts the server if not reachable yet.
reuseExistingServer: false,
stderr: 'pipe',
stdout: 'pipe',
url: 'http://127.0.0.1:8089',
timeout: 5 * 60 * 1000, // max. 5 minutes for creating the container
// max. 5 minutes for creating the container
timeout: 5 * 60 * 1000,
wait: {
// we wait for this line to appear in the output of the webserver until consider it done
stdout: /Nextcloud is now ready to use/,
Expand Down
92 changes: 92 additions & 0 deletions playwright/e2e/autosave.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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 offlineTest } from '../support/fixtures/offline'
import { test as uploadFileTest } from '../support/fixtures/upload-file'

const test = mergeTests(editorTest, offlineTest, uploadFileTest)

// As we switch on and off the network
// we cannot run tests in parallel.
test.describe.configure({ mode: 'serial' })

// Files were created 10 seconds ago so there's no throttling to begin with.
test.use({ mtime: Date.now() / 1000 - 10 })

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

test('saves after 1 second', async ({ editor }) => {
await expect(editor.el).toBeVisible()
await editor.typeHeading('Hello world')
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/)
// TODO: Why does this not work? await expect(await file.getContent()).toBe('## Hello world')
})

/*
* 1 second autosave debounce
* 10 seconds waiting for server to be ready again
* 1 second for the save request
*/
test('saves again within 12 seconds', async ({ editor }) => {
test.slow()
await expect(editor.el).toBeVisible()
await editor.typeHeading('Hello')
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/)
await editor.type(' again')
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/, {
timeout: 12_000,
})
})

test('saves after being disconnected for 5 sec.', async ({
editor,
setOffline,
setOnline,
}) => {
await expect(editor.el).toBeVisible()
await editor.typeHeading('Hello')
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/)
await editor.type(' again')
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await setOffline()
await new Promise((resolve) => setTimeout(resolve, 5_000))
await setOnline()
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/, {
timeout: 10_000,
})
})

test('saves after being disconnected for 2 minutes.', async ({
editor,
page,
setOffline,
setOnline,
}) => {
await page.clock.install()
await expect(editor.el).toBeVisible()
await editor.typeHeading('Hello')
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/)
await editor.type(' again')
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
await setOffline()
// Wait long enough for the server throttling to be over.
await new Promise((resolve) => setTimeout(resolve, 10_000))
await page.clock.fastForward(110_000)
await setOnline()
await expect(editor.offlineState).not.toBeVisible()

Check failure on line 87 in playwright/e2e/autosave.spec.ts

View workflow job for this annotation

GitHub Actions / playwright (1, 3)

[chromium] › playwright/e2e/autosave.spec.ts:69:1 › saves after being disconnected for 2 minutes.

1) [chromium] › playwright/e2e/autosave.spec.ts:69:1 › saves after being disconnected for 2 minutes. Error: expect(locator).not.toBeVisible() failed Locator: locator('.editor').first().locator('.offline-state') Expected: not visible Received: visible Timeout: 5000ms Call log: - Expect "not toBeVisible" with timeout 5000ms - waiting for locator('.editor').first().locator('.offline-state') 13 × locator resolved to <div data-v-a877e9f2="" data-v-d72b6dd0="" class="offline-state">…</div> - unexpected value "visible" 85 | await page.clock.fastForward(110_000) 86 | await setOnline() > 87 | await expect(editor.offlineState).not.toBeVisible() | ^ 88 | await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/) 89 | // Be sure to trigger at least one autosave 90 | await page.clock.fastForward(15_000) at /home/runner/actions-runner/_work/text/text/playwright/e2e/autosave.spec.ts:87:40
await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/)
// Be sure to trigger at least one autosave
await page.clock.fastForward(15_000)
await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/)
})
18 changes: 10 additions & 8 deletions playwright/support/fixtures/upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as base } from './random-user'
import { Node } from './Node'
import { ViewerSection } from '../sections/ViewerSection'
import type { Node } from './Node.ts'

import { ViewerSection } from '../sections/ViewerSection.ts'
import { test as base } from './random-user.ts'

export interface UploadFileFixture {
file: Node
fileName: string
fileContent: string
mtime?: number
oldVersions: { content?: string, mtime: number }[]
open: () => Promise<void>
close: () => Promise<void>
Expand All @@ -24,16 +26,16 @@ export interface UploadFileFixture {
export const test = base.extend<UploadFileFixture>({
fileContent: ['', { option: true }],
fileName: ['empty.md', { option: true }],
mtime: [undefined, { option: true }],
oldVersions: [[], { option: true }],

file: async ({ fileContent, fileName, oldVersions, user }, use) => {
const uploadVersion =
(opts: { content?: string, mtime?: number }) =>
user.uploadFile({ name: fileName, ...opts })
file: async ({ fileContent, fileName, oldVersions, mtime, user }, use) => {
const uploadVersion
= (opts: { content?: string, mtime?: number }) => user.uploadFile({ name: fileName, ...opts })
for (const version of oldVersions) {
await uploadVersion(version)
}
const file = await uploadVersion({ content: fileContent })
const file = await uploadVersion({ content: fileContent, mtime })
await use(file)
},

Expand Down
14 changes: 7 additions & 7 deletions src/apis/save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import { unref, type ShallowRef } from 'vue'
import type { Connection } from '../composables/useConnection'
import type { Document } from '../services/SyncService'

interface SaveData {
export interface SaveData {
version: number
autosaveContent: string
documentState: string
}

export interface SaveOptions {
force: boolean
manualSave: boolean
}

interface SaveResponse {
data: Document
data: { document: Document }
}

/**
Expand All @@ -29,7 +32,7 @@ interface SaveResponse {
*/
export function save(
connection: ShallowRef<Connection> | Connection,
data: SaveData,
data: SaveData & SaveOptions,
): Promise<SaveResponse> {
const con = unref(connection)
const pub = con.shareToken ? '/public' : ''
Expand All @@ -55,10 +58,7 @@ export function save(
* @param connection the active connection
* @param data data to save
*/
export function saveViaSendBeacon(
connection: Connection,
data: Omit<SaveData, 'force' | 'manualSave'>,
): boolean {
export function saveViaSendBeacon(connection: Connection, data: SaveData): boolean {
const con = unref(connection)
const pub = con.shareToken ? '/public' : ''
const url = generateUrl(`apps/text${pub}/session/${con.documentId}/save`)
Expand Down
Loading
Loading