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
278 changes: 267 additions & 11 deletions playwright/e2e/version-preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,263 @@
*/

import { Buffer } from 'buffer'
import { expect } from '@playwright/test'
import { expect, type Page } from '@playwright/test'
import { test } from '../support/fixtures/random-user'
import {
addTextElement,
captureBoardAuthFromSave,
createWhiteboard,
fetchBoardContent,
getBoardAuth,
openFilesApp,
resolveStoredFileName,
waitForCanvas,
} from '../support/utils'

const versionPropfindBody = `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:">
<d:prop>
<d:getlastmodified />
</d:prop>
</d:propfind>`

const extractVersionIds = (xml: string, userId: string, fileId: number | string): string[] => {
const prefix = `/remote.php/dav/versions/${userId}/versions/${fileId}/`
const hrefRegex = /<[^:>]*:href>([^<]+)<\/[^:>]*:href>/g
const versionIds = new Set<string>()
let match: RegExpExecArray | null = null

while ((match = hrefRegex.exec(xml)) !== null) {
const href = decodeURIComponent(match[1])
const index = href.indexOf(prefix)
if (index === -1) {
continue
}
const remainder = href.slice(index + prefix.length).replace(/\/$/, '')
if (remainder) {
versionIds.add(remainder)
}
}

return [...versionIds]
}

const findVersionByContent = async (
page: Page,
options: {
origin: string
userId: string
fileId: number
includeText: string
excludeText?: string
},
): Promise<{ versionId: string, versionSource: string }> => {
const { origin, userId, fileId, includeText, excludeText } = options
const listUrl = `${origin}/remote.php/dav/versions/${userId}/versions/${fileId}`
const maxAttempts = 20
const requestToken = await page.evaluate(() => (window as any).OC?.requestToken
|| (document.querySelector('head meta[name="requesttoken"]') as HTMLMetaElement | null)?.content
|| null)
const versionHeaders = {
Depth: '1',
Accept: 'application/xml',
'Content-Type': 'application/xml',
...(requestToken ? { requesttoken: requestToken } : {}),
'X-Requested-With': 'XMLHttpRequest',
}

for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await page.request.fetch(listUrl, {
method: 'PROPFIND',
headers: versionHeaders,
data: versionPropfindBody,
})

if (!response.ok()) {
throw new Error(`Version list request failed with status ${response.status()}`)
}

const xml = await response.text()
const versionIds = extractVersionIds(xml, userId, fileId)

for (const versionId of versionIds) {
const versionSource = `/remote.php/dav/versions/${userId}/versions/${fileId}/${versionId}`
const versionResponse = await page.request.get(`${origin}${versionSource}`, {
headers: {
...(requestToken ? { requesttoken: requestToken } : {}),
'X-Requested-With': 'XMLHttpRequest',
},
})
if (!versionResponse.ok()) {
continue
}
const rawContent = await versionResponse.text()
if (!rawContent.includes(includeText)) {
continue
}
if (excludeText && rawContent.includes(excludeText)) {
continue
}
return { versionId, versionSource }
}

await page.waitForTimeout(1000)
}

throw new Error(`No matching version found for ${includeText}`)
}

const waitForBoardContent = async (page: Page, auth: { fileId: number, jwt: string }, text: string) => {
await expect.poll(async () => JSON.stringify(await fetchBoardContent(page, auth)), {
timeout: 20000,
interval: 500,
}).toContain(text)
}

const prepareVersionScenario = async (
page: Page,
userId: string,
options: { boardName: string, initialText: string, updatedText: string },
) => {
const { boardName, initialText, updatedText } = options
await createWhiteboard(page, { name: boardName })
const authPromise = captureBoardAuthFromSave(page, { containsText: initialText })
await addTextElement(page, initialText)
const { fileId, jwt } = await authPromise
const baseAuth = { fileId, jwt }
await waitForBoardContent(page, baseAuth, initialText)

await addTextElement(page, updatedText, { x: 720, y: 520 })
await waitForBoardContent(page, baseAuth, updatedText)

const origin = new URL(await page.url()).origin
const versionEntry = await findVersionByContent(page, {
origin,
userId,
fileId: baseAuth.fileId,
includeText: initialText,
excludeText: updatedText,
})

await openFilesApp(page)
const storedName = await resolveStoredFileName(page, boardName)

return { baseAuth, origin, versionEntry, storedName }
}

const openWhiteboardInViewer = async (
page: Page,
options: { fileId: number, fileName: string, source?: string | null, fileVersion?: string | null },
) => {
const filePath = options.fileName.startsWith('/') ? options.fileName : `/${options.fileName}`
await page.waitForFunction(() => Boolean((window as any).OCA?.Viewer?.openWith), { timeout: 10000 })
await page.evaluate(({ fileId, filePathValue, fileName, source, fileVersion }) => {
const viewer = (window as any).OCA?.Viewer
if (!viewer?.openWith) {
throw new Error('Viewer openWith unavailable')
}
viewer.openWith('whiteboard', {
fileInfo: {
fileid: Number(fileId),
filename: filePathValue,
basename: fileName,
source: source ?? null,
fileVersion: fileVersion ?? null,
mime: 'application/vnd.excalidraw+json',
size: 0,
type: 'file',
},
enableSidebar: false,
})
}, {
fileId: options.fileId,
filePathValue: filePath,
fileName: options.fileName,
source: options.source ?? null,
fileVersion: options.fileVersion ?? null,
})
}

test.beforeEach(async ({ page }) => {
await openFilesApp(page)
})

test('version preview params still load board content', async ({ page, user }) => {
test('version preview banner shows and exits to live board', async ({
page,
user,
}) => {
test.setTimeout(120000)
const boardName = `Version preview ${Date.now()}`
const initialText = 'Version one'
const updatedText = 'Version two'

const { baseAuth, versionEntry, storedName } = await prepareVersionScenario(page, user.userId, {
boardName,
initialText,
updatedText,
})
await openWhiteboardInViewer(page, {
fileId: baseAuth.fileId,
fileName: storedName,
source: versionEntry.versionSource,
fileVersion: versionEntry.versionId,
})
await waitForCanvas(page)

const banner = page.locator('.version-preview-banner')
await expect(banner).toBeVisible({ timeout: 20000 })
await expect(page.getByRole('button', { name: 'Restore this version' })).toBeVisible()

const backButton = page.getByRole('button', { name: 'Back to latest version' })
await expect(backButton).toBeVisible()
await backButton.click()

await expect(banner).toBeHidden({ timeout: 20000 })
await waitForBoardContent(page, baseAuth, updatedText)
})

test('restore version replaces current content', async ({
page,
user,
}) => {
test.setTimeout(120000)
const boardName = `Version restore ${Date.now()}`
const initialText = 'Restore one'
const updatedText = 'Restore two'

const { baseAuth, versionEntry, storedName } = await prepareVersionScenario(page, user.userId, {
boardName,
initialText,
updatedText,
})
await openWhiteboardInViewer(page, {
fileId: baseAuth.fileId,
fileName: storedName,
source: versionEntry.versionSource,
fileVersion: versionEntry.versionId,
})
await waitForCanvas(page)

const restoreButton = page.getByRole('button', { name: 'Restore this version' })
await expect(restoreButton).toBeVisible()
await restoreButton.click()

const banner = page.locator('.version-preview-banner')
await expect(banner).toBeHidden({ timeout: 20000 })

await expect.poll(async () => JSON.stringify(await fetchBoardContent(page, baseAuth)), {
timeout: 30000,
interval: 500,
}).toContain(initialText)
await expect.poll(async () => JSON.stringify(await fetchBoardContent(page, baseAuth)), {
timeout: 30000,
interval: 500,
}).not.toContain(updatedText)
})

test('version preview params still load board content', async ({
page,
user,
}) => {
test.setTimeout(90000)
const boardName = `Version preview ${Date.now()}`

Expand All @@ -32,7 +271,9 @@ test('version preview params still load board content', async ({ page, user }) =
try {
return await getBoardAuth(page)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My IDE is complaining about getBoardAuth not being listed in the imports here

} catch {
const { fileId, jwt } = await captureBoardAuthFromSave(page, { containsText: 'Live content' })
const { fileId, jwt } = await captureBoardAuthFromSave(page, {
containsText: 'Live content',
})
return { fileId, jwt }
}
}
Expand All @@ -52,19 +293,34 @@ test('version preview params still load board content', async ({ page, user }) =
const escapedName = storedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const row = page.getByRole('row', { name: new RegExp(escapedName, 'i') })
await expect(row).toBeVisible({ timeout: 30000 })
await row.click()
await openWhiteboardInViewer(page, {
fileId: baseAuth.fileId,
fileName: storedName,
source: versionSource,
fileVersion: '1.0',
})
await waitForCanvas(page)

const tokenResponse = await page.request.get(`apps/whiteboard/${baseAuth.fileId}/token`)
const tokenResponse = await page.request.get(
`apps/whiteboard/${baseAuth.fileId}/token`,
)
expect(tokenResponse.ok()).toBeTruthy()
const token = (await tokenResponse.json()).token

const previewAuth = { fileId: baseAuth.fileId, jwt: token }
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
const payload = JSON.parse(
Buffer.from(token.split('.')[1], 'base64').toString(),
)
expect(payload?.isFileReadOnly).toBeFalsy()

await expect.poll(async () => JSON.stringify(await fetchBoardContent(page, previewAuth)), {
timeout: 20000,
interval: 500,
}).toContain('Live content')
await expect
.poll(
async () =>
JSON.stringify(await fetchBoardContent(page, previewAuth)),
{
timeout: 20000,
interval: 500,
},
)
.toContain('Live content')
})
Loading
Loading