dx: collect diagnostics or flaky test upload1
Massimo Melina committed
May 2, 2026 at 00:36 UTC
7b064ec3d614d4aeb7b806fad33355947a047098
1 file changed
+144
-50
e2e/serial.spec.ts
+144
-50
@@ -1,4 +1,4 @@
1
-import { expect, test } from '@playwright/test'
1
+import { expect, test, type ConsoleMessage, type Page, type Request, type Response, type TestInfo } from '@playwright/test'
2
import { ADMIN_URL, clearUploads, clickAdminMenu, clickIconBtn, loginAdmin, password, uploadName, FRONTEND_URL, username } from './common'
3
4
// this test is separated to run serially, as it will modify folder timestamp for a few seconds, during which other tests may fail
@@ -10,58 +10,152 @@ export const fileToUpload = {
10
buffer: Buffer.alloc(100_000),
11
}
12
13
-test('upload1', async ({ page, context, browserName }) => {
13
+test('upload1', async ({ page, context, browserName }, testInfo) => {
14
if (browserName !== 'chromium') return // only chromium has cdpSession
15
- await page.goto(FRONTEND_URL)
16
- await page.getByRole('button', { name: 'Login' }).click()
17
- await page.getByRole('textbox', { name: 'Username' }).fill(username)
18
- await page.getByRole('textbox', { name: 'Password' }).fill(password)
19
- await page.getByRole('button', { name: 'Continue' }).click()
20
- await page.locator('div').filter({ hasText: 'Logged in' }).nth(3).click()
21
-
22
- await page.getByRole('link', { name: 'for-admins, Folder' }).click()
23
- await page.getByRole('link', { name: 'upload, Folder' }).click()
24
-
25
- await page.getByRole('button', { name: 'Options' }).click()
26
- const pageAdminPromise = page.waitForEvent('popup')
27
- await page.getByRole('button', { name: 'Admin-panel' }).click()
28
- const pageAdmin = await pageAdminPromise
29
- await pageAdmin.goto(ADMIN_URL + '#/monitoring'); // cross-device way of changing page
30
- await page.locator('div').filter({ hasText: 'xOptionsAdmin-panelSort by:' }).nth(2).click()
31
- await page.getByRole('button', { name: 'Close' }).click()
32
- await page.getByRole('button', { name: 'Upload' }).click()
33
- const fileChooserPromise = page.waitForEvent('filechooser')
34
- await page.getByRole('button', { name: 'Pick files' }).click()
35
- const fileChooser = await fileChooserPromise
36
- await fileChooser.setFiles(fileToUpload)
37
- // can't do without cdp to slow down the upload. I tried using route.continue, but i can't send half-body keeping the full content-length, and i also cannot pass a stream (to throttle)
38
- const cdpSession = await context.newCDPSession(page)
39
- await cdpSession.send('Network.emulateNetworkConditions', NETWORK_PRESETS.Regular2G)
40
- await page.getByRole('button', { name: 'Edit' }).click()
41
- const renameDialog = page.locator('.dialog-prompt')
42
- const renameInput = renameDialog.getByRole('textbox')
43
- await expect(renameInput).toHaveValue(fileToUpload.name) // promptDialog initializes the field value in useEffect, so we wait for that init to avoid our fill being overwritten
44
- await renameInput.fill(uploadName)
45
- await renameDialog.getByRole('button', { name: 'Continue' }).click()
46
- await expect(page.getByText(uploadName)).toBeVisible() // rename was effective
47
- // we send the upload, slowly, so that we can interrupt it in the admin-panel to test the upload resume
48
- await page.getByRole('button', { name: 'Send 1 file' }).click()
49
- const uploadCells = pageAdmin.locator('.MuiDataGrid-cell')
50
- .filter({ hasText: uploadName })
51
- .filter({ hasText: '/for-admins/upload' })
52
- await expect(uploadCells.first()).toBeVisible()
53
- // during upload resume, monitoring can briefly show two rows for the same path
54
- await uploadCells.last().click()
55
- await clickIconBtn('Disconnect', pageAdmin)
56
- await clickIconBtn('Close', pageAdmin)
57
- await pageAdmin.close()
58
- await page.getByText('Copy links').click()
59
- await page.getByText('Operation successful').click()
60
- await page.getByRole('button', { name: 'Close' }).click()
61
- await cdpSession?.send('Network.emulateNetworkConditions', NETWORK_PRESETS.NoThrottle)
62
- clearUploads()
15
+ const diagnostics = await startUpload1Diagnostics(page)
16
+ try {
17
+ await page.goto(FRONTEND_URL)
18
+ await page.getByRole('button', { name: 'Login' }).click()
19
+ await page.getByRole('textbox', { name: 'Username' }).fill(username)
20
+ await page.getByRole('textbox', { name: 'Password' }).fill(password)
21
+ await page.getByRole('button', { name: 'Continue' }).click()
22
+ await page.locator('div').filter({ hasText: 'Logged in' }).nth(3).click()
23
+
24
+ await page.getByRole('link', { name: 'for-admins, Folder' }).click()
25
+ await page.getByRole('link', { name: 'upload, Folder' }).click()
26
+
27
+ await page.getByRole('button', { name: 'Options' }).click()
28
+ const pageAdminPromise = page.waitForEvent('popup')
29
+ await page.getByRole('button', { name: 'Admin-panel' }).click()
30
+ const pageAdmin = await pageAdminPromise
31
+ diagnostics.trackPage(pageAdmin, 'admin')
32
+ await pageAdmin.goto(ADMIN_URL + '#/monitoring'); // cross-device way of changing page
33
+ await page.locator('div').filter({ hasText: 'xOptionsAdmin-panelSort by:' }).nth(2).click()
34
+ await page.getByRole('button', { name: 'Close' }).click()
35
+ await page.getByRole('button', { name: 'Upload' }).click()
36
+ const fileChooserPromise = page.waitForEvent('filechooser')
37
+ await page.getByRole('button', { name: 'Pick files' }).click()
38
+ const fileChooser = await fileChooserPromise
39
+ await fileChooser.setFiles(fileToUpload)
40
+ // can't do without cdp to slow down the upload. I tried using route.continue, but i can't send half-body keeping the full content-length, and i also cannot pass a stream (to throttle)
41
+ const cdpSession = await context.newCDPSession(page)
42
+ await cdpSession.send('Network.emulateNetworkConditions', NETWORK_PRESETS.Regular2G)
43
+ await page.getByRole('button', { name: 'Edit' }).click()
44
+ const renameDialog = page.locator('.dialog-prompt')
45
+ const renameInput = renameDialog.getByRole('textbox')
46
+ await expect(renameInput).toHaveValue(fileToUpload.name) // promptDialog initializes the field value in useEffect, so we wait for that init to avoid our fill being overwritten
47
+ await renameInput.fill(uploadName)
48
+ await renameDialog.getByRole('button', { name: 'Continue' }).click()
49
+ await expect(page.getByText(uploadName)).toBeVisible() // rename was effective
50
+ // we send the upload, slowly, so that we can interrupt it in the admin-panel to test the upload resume
51
+ await page.getByRole('button', { name: 'Send 1 file' }).click()
52
+ const uploadCells = pageAdmin.locator('.MuiDataGrid-cell')
53
+ .filter({ hasText: uploadName })
54
+ .filter({ hasText: '/for-admins/upload' })
55
+ await expect(uploadCells.first()).toBeVisible()
56
+ // during upload resume, monitoring can briefly show two rows for the same path
57
+ await uploadCells.last().click()
58
+ await clickIconBtn('Disconnect', pageAdmin)
59
+ await clickIconBtn('Close', pageAdmin)
60
+ await pageAdmin.close()
61
+ await page.getByText('Copy links').click()
62
+ await page.getByText('Operation successful').click()
63
+ await page.getByRole('button', { name: 'Close' }).click()
64
+ await cdpSession?.send('Network.emulateNetworkConditions', NETWORK_PRESETS.NoThrottle)
65
+ clearUploads()
66
+ }
67
+ catch (err) {
68
+ await diagnostics.attach(testInfo)
69
+ throw err
70
+ }
71
})
72
73
+const MAX_DIAGNOSTIC_LINES = 300
74
+
75
+async function startUpload1Diagnostics(page: Page) {
76
+ const lines: string[] = []
77
+ const started = Date.now()
78
+ addLine('diagnostics started')
79
+ await page.exposeBinding('__upload1Diag', (_source, entry: Record<string, unknown>) => {
80
+ addLine(`xhr ${formatEntry(entry)}`)
81
+ })
82
+ await page.addInitScript(() => {
83
+ const originalOpen = XMLHttpRequest.prototype.open
84
+ const originalSend = XMLHttpRequest.prototype.send
85
+ XMLHttpRequest.prototype.open = function(method: string, url: string | URL, ...args: any[]) {
86
+ ;(this as any).__upload1DiagRequest = { method: String(method), url: String(url) }
87
+ return Reflect.apply(originalOpen, this, [method, url, ...args])
88
+ }
89
+ XMLHttpRequest.prototype.send = function(...args: any[]) {
90
+ const req = (this as any).__upload1DiagRequest
91
+ if (req?.method === 'PUT' && req.url.includes('/for-admins/upload/')) {
92
+ const log = (event: string, progress?: ProgressEvent) => {
93
+ // record XHR state before Playwright closes the browser
94
+ Promise.resolve((window as any).__upload1Diag?.({
95
+ event,
96
+ readyState: this.readyState,
97
+ status: this.status,
98
+ url: req.url,
99
+ loaded: progress?.loaded,
100
+ total: progress?.lengthComputable ? progress.total : undefined,
101
+ })).catch(() => {})
102
+ }
103
+ for (const event of ['loadstart', 'abort', 'error', 'timeout', 'loadend'])
104
+ this.addEventListener(event, log.bind(null, event))
105
+ this.upload.addEventListener('progress', e => log('upload-progress', e))
106
+ this.addEventListener('readystatechange', () => log('readystatechange'))
107
+ }
108
+ return originalSend.apply(this, args)
109
+ }
110
+ })
111
+ trackPage(page, 'main')
112
+ return { attach, trackPage }
113
+
114
+ function trackPage(trackedPage: Page, label: string) {
115
+ trackedPage.on('console', msg => addLine(`console:${label} ${formatConsole(msg)}`))
116
+ trackedPage.on('requestfailed', request => addLine(`requestfailed:${label} ${formatRequest(request)} ${request.failure()?.errorText ?? ''}`))
117
+ trackedPage.on('response', response => {
118
+ if (isUploadResponse(response))
119
+ addLine(`response:${label} ${response.status()} ${response.request().method()} ${response.url()}`)
120
+ })
121
+ }
122
+
123
+ async function attach(testInfo: TestInfo) {
124
+ await testInfo.attach('upload1-diagnostics', {
125
+ body: lines.join('\n') + '\n',
126
+ contentType: 'text/plain',
127
+ })
128
+ }
129
+
130
+ function addLine(text: string) {
131
+ const offset = `${Date.now() - started}ms`.padStart(7)
132
+ lines.push(`${offset} ${text}`)
133
+ if (lines.length > MAX_DIAGNOSTIC_LINES)
134
+ lines.splice(0, lines.length - MAX_DIAGNOSTIC_LINES)
135
+ }
136
+
137
+ function formatConsole(msg: ConsoleMessage) {
138
+ return `${msg.type()} ${msg.text()}`
139
+ }
140
+
141
+ function formatRequest(request: Request) {
142
+ return `${request.method()} ${request.url()}`
143
+ }
144
+
145
+ function isUploadResponse(response: Response) {
146
+ const request = response.request()
147
+ return request.method() === 'PUT' && response.url().includes('/for-admins/upload/')
148
+ }
149
+
150
+ function formatEntry(entry: Record<string, unknown>) {
151
+ return Object.entries(entry)
152
+ .filter(([, value]) => value !== undefined)
153
+ .map(([key, value]) => `${key}=${String(value)}`)
154
+ .join(' ')
155
+ }
156
+
157
+}
158
+
159
const NETWORK_PRESETS = {
160
Offline: {
161
offline: true,