Skip to content

Commit 5416c02

Browse files
authored
Merge pull request #247 from nextcloud/tests/reference-and-search
test: cover the link preview widgets and the search providers
2 parents 087112a + ad08450 commit 5416c02

2 files changed

Lines changed: 169 additions & 1 deletion

File tree

‎playwright.config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default defineConfig({
4949
reuseExistingServer: !process.env.CI,
5050
stdout: 'pipe',
5151
stderr: 'pipe',
52-
timeout: 5 * 60 * 1000,
52+
// the shallow server image sometimes needs several minutes to boot on a slow runner
53+
timeout: 10 * 60 * 1000,
5354
},
5455
})
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Page } from '@playwright/test'
7+
8+
import { login } from '@nextcloud/e2e-test-server/playwright'
9+
import { test as base, expect } from '@playwright/test'
10+
11+
// the test container always has this admin user
12+
const admin = { userId: 'admin', password: 'admin' }
13+
const ocs = { 'OCS-APIRequest': 'true', Accept: 'application/json' }
14+
15+
// served as the avatar of the mocked author
16+
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', 'base64')
17+
18+
// an issue as the app puts it into the rich object of a link preview
19+
const issue = {
20+
github_type: 'issue',
21+
github_repo_owner: 'nextcloud',
22+
github_repo: 'integration_github',
23+
github_issue_id: '243',
24+
id: 243,
25+
number: 243,
26+
title: 'The dashboard widget stays empty',
27+
html_url: 'https://github.com/nextcloud/integration_github/issues/243',
28+
state: 'closed',
29+
state_reason: 'completed',
30+
comments: 2,
31+
created_at: '2026-09-18T10:00:00Z',
32+
updated_at: '2026-09-18T11:00:00Z',
33+
user: { login: 'janedoe' },
34+
labels: [{ name: 'bug', color: 'd73a4a' }],
35+
assignees: [],
36+
reactions: { total_count: 0 },
37+
}
38+
39+
// a code permalink as the app puts it into the rich object of a link preview
40+
const permalink = {
41+
github_type: 'code',
42+
owner: 'nextcloud',
43+
repo: 'integration_github',
44+
filePath: 'src/reference.js',
45+
link: 'https://github.com/nextcloud/integration_github/blob/5f3c2a1/src/reference.js#L10-L12',
46+
html_url: 'https://github.com/nextcloud/integration_github/blob/5f3c2a1/src/reference.js#L10-L12',
47+
ref: { original_ref: 'main', sha: '5f3c2a1b8d4e6f7a9c0b1d2e3f4a5b6c7d8e9f01' },
48+
lineBegin: 10,
49+
lineEnd: 12,
50+
lines: ['const first = 1', 'const second = 2', 'const third = 3'],
51+
}
52+
53+
// Every test also fails on an uncaught exception, or on an unexpected failing request to one of the app's own routes.
54+
// Errors of other apps on the instance are ignored on purpose.
55+
const test = base.extend<{ appErrors: void }>({
56+
appErrors: [async ({ page }, use) => {
57+
const errors: string[] = []
58+
page.on('pageerror', (error) => errors.push(`uncaught: ${error.message}`))
59+
page.on('response', (response) => {
60+
if (response.status() >= 400 && response.url().includes('/integration_github/')) {
61+
errors.push(`${response.status()} ${response.request().method()} ${new URL(response.url()).pathname}`)
62+
}
63+
})
64+
await use()
65+
expect(errors).toEqual([])
66+
}, { auto: true }],
67+
})
68+
69+
/**
70+
* Load the reference bundle of the app and render one of its widgets, the way Nextcloud does
71+
* wherever a link preview appears.
72+
*
73+
* @param page a page of a logged in user
74+
* @param type the rich object type of the widget
75+
* @param richObject the rich object to render
76+
*/
77+
async function renderReferenceWidget(page: Page, type: string, richObject: object) {
78+
// the widget asks the app for the avatar of a GitHub user, which needs an account
79+
await page.route('**/apps/integration_github/avatar**', (route) => route.fulfill({ contentType: 'image/png', body: png }))
80+
await page.goto('apps/files/')
81+
const registered = await page.evaluate(async ([widgetType, object]) => {
82+
const globals = window as unknown as {
83+
OC: { appswebroots: Record<string, string> }
84+
_vue_richtext_widgets: Record<string, { callback: (element: HTMLElement, data: object) => void }>
85+
}
86+
await import(/* @vite-ignore */ `${globals.OC.appswebroots.integration_github}/js/integration_github-reference.mjs`)
87+
const element = document.createElement('div')
88+
element.id = 'reference-widget'
89+
document.body.appendChild(element)
90+
globals._vue_richtext_widgets[widgetType as string].callback(element, {
91+
richObjectType: widgetType,
92+
richObject: object,
93+
accessible: false,
94+
})
95+
return Object.keys(globals._vue_richtext_widgets)
96+
}, [type, richObject] as [string, object])
97+
expect(registered).toEqual(expect.arrayContaining(['integration_github_issue_pr', 'integration_github_code_permalink']))
98+
return page.locator('#reference-widget')
99+
}
100+
101+
test.beforeEach(async ({ page }) => {
102+
await login(page.request, admin)
103+
})
104+
105+
test.describe('Link previews', () => {
106+
test('offer the providers to the smart picker', async ({ page }) => {
107+
const response = await page.request.get('../ocs/v2.php/references/providers', { headers: ocs })
108+
expect(response.ok()).toBe(true)
109+
const providers = (await response.json()).ocs.data as Array<{ id: string, title: string, icon_url: string }>
110+
const expected = {
111+
'github-issue-pr': 'GitHub issues, pull requests and comments',
112+
'github-permalink': 'GitHub code permalink',
113+
}
114+
for (const [id, title] of Object.entries(expected)) {
115+
const provider = providers.find((candidate) => candidate.id === id)
116+
expect(provider, `provider ${id}`).toBeDefined()
117+
expect(provider?.title).toBe(title)
118+
// the provider icon is served by the app
119+
expect((await page.request.get(provider!.icon_url)).ok()).toBe(true)
120+
}
121+
})
122+
123+
test('render an issue in the reference widget', async ({ page }) => {
124+
const widget = await renderReferenceWidget(page, 'integration_github_issue_pr', issue)
125+
126+
const link = widget.getByRole('link', { name: issue.title })
127+
await expect(link).toBeVisible()
128+
await expect(link).toHaveAttribute('href', issue.html_url)
129+
await expect(widget.getByText('nextcloud/integration_github #243')).toBeVisible()
130+
await expect(widget.getByText('by janedoe')).toBeVisible()
131+
await expect(widget.getByText('bug', { exact: true })).toBeVisible()
132+
})
133+
134+
test('render a code permalink in the reference widget', async ({ page }) => {
135+
const widget = await renderReferenceWidget(page, 'integration_github_code_permalink', permalink)
136+
137+
await expect(widget.getByText('nextcloud/integration_github/src/reference.js')).toBeVisible()
138+
await expect(widget.getByText('Line 10 to 12')).toBeVisible()
139+
await expect(widget.getByText('const second = 2')).toBeVisible()
140+
})
141+
})
142+
143+
test.describe('Search providers', () => {
144+
test('offer the providers to unified search', async ({ page }) => {
145+
const response = await page.request.get('../ocs/v2.php/search/providers', { headers: ocs })
146+
expect(response.ok()).toBe(true)
147+
const providers = (await response.json()).ocs.data as Array<{ id: string, appId: string, name: string }>
148+
const expected = {
149+
'github-search-issues': 'GitHub issues and pull requests',
150+
'github-search-repos': 'GitHub repositories',
151+
}
152+
for (const [id, name] of Object.entries(expected)) {
153+
expect(providers.find((candidate) => candidate.id === id), `provider ${id}`).toMatchObject({
154+
appId: 'integration_github',
155+
name,
156+
})
157+
}
158+
})
159+
160+
test('answer an empty result for a user without a GitHub account', async ({ page }) => {
161+
for (const id of ['github-search-issues', 'github-search-repos']) {
162+
const response = await page.request.get(`../ocs/v2.php/search/providers/${id}/search?term=nextcloud`, { headers: ocs })
163+
expect(response.ok(), id).toBe(true)
164+
expect((await response.json()).ocs.data.entries, id).toEqual([])
165+
}
166+
})
167+
})

0 commit comments

Comments
 (0)