main
ts 301 lines 14.4 KB
Raw
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
5 test.describe.configure({ mode: 'serial' }) // to disconnect the upload consistently, i need only 1 upload at a time
6
7 export const fileToUpload = {
8 name: 'upload-test.bin',
9 mimeType: 'application/octet-stream',
10 buffer: Buffer.alloc(100_000),
11 }
12
13 test('upload1', async ({ page, context, browserName }, testInfo) => {
14 if (browserName !== 'chromium') return // only chromium has cdpSession
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 openUploadRename(page)
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' }).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 async function openUploadRename(page: Page) {
74 const editButton = page.getByRole('button', { name: 'Edit' })
75 if (await editButton.isVisible())
76 return editButton.click()
77 await page.locator('.upload-list').getByRole('button', { name: 'Menu' }).click()
78 await page.getByRole('link', { name: 'Rename' }).click()
79 }
80
81 const MAX_DIAGNOSTIC_LINES = 300
82
83 async function startUpload1Diagnostics(page: Page) {
84 const lines: string[] = []
85 const started = Date.now()
86 addLine('diagnostics started')
87 await page.exposeBinding('__upload1Diag', (_source, entry: Record<string, unknown>) => {
88 addLine(`xhr ${formatEntry(entry)}`)
89 })
90 await page.addInitScript(() => {
91 const originalOpen = XMLHttpRequest.prototype.open
92 const originalSend = XMLHttpRequest.prototype.send
93 XMLHttpRequest.prototype.open = function(method: string, url: string | URL, ...args: any[]) {
94 ;(this as any).__upload1DiagRequest = { method: String(method), url: String(url) }
95 return Reflect.apply(originalOpen, this, [method, url, ...args])
96 }
97 XMLHttpRequest.prototype.send = function(...args: any[]) {
98 const req = (this as any).__upload1DiagRequest
99 if (req?.method === 'PUT' && req.url.includes('/for-admins/upload/')) {
100 const log = (event: string, progress?: ProgressEvent) => {
101 // record XHR state before Playwright closes the browser
102 Promise.resolve((window as any).__upload1Diag?.({
103 event,
104 readyState: this.readyState,
105 status: this.status,
106 url: req.url,
107 loaded: progress?.loaded,
108 total: progress?.lengthComputable ? progress.total : undefined,
109 })).catch(() => {})
110 }
111 for (const event of ['loadstart', 'abort', 'error', 'timeout', 'loadend'])
112 this.addEventListener(event, log.bind(null, event))
113 this.upload.addEventListener('progress', e => log('upload-progress', e))
114 this.addEventListener('readystatechange', () => log('readystatechange'))
115 }
116 return originalSend.apply(this, args)
117 }
118 })
119 trackPage(page, 'main')
120 return { attach, trackPage }
121
122 function trackPage(trackedPage: Page, label: string) {
123 trackedPage.on('console', msg => addLine(`console:${label} ${formatConsole(msg)}`))
124 trackedPage.on('requestfailed', request => addLine(`requestfailed:${label} ${formatRequest(request)} ${request.failure()?.errorText ?? ''}`))
125 trackedPage.on('response', response => {
126 if (isUploadResponse(response))
127 addLine(`response:${label} ${response.status()} ${response.request().method()} ${response.url()}`)
128 })
129 }
130
131 async function attach(testInfo: TestInfo) {
132 await testInfo.attach('upload1-diagnostics', {
133 body: lines.join('\n') + '\n',
134 contentType: 'text/plain',
135 })
136 }
137
138 function addLine(text: string) {
139 const offset = `${Date.now() - started}ms`.padStart(7)
140 lines.push(`${offset} ${text}`)
141 if (lines.length > MAX_DIAGNOSTIC_LINES)
142 lines.splice(0, lines.length - MAX_DIAGNOSTIC_LINES)
143 }
144
145 function formatConsole(msg: ConsoleMessage) {
146 return `${msg.type()} ${msg.text()}`
147 }
148
149 function formatRequest(request: Request) {
150 return `${request.method()} ${request.url()}`
151 }
152
153 function isUploadResponse(response: Response) {
154 const request = response.request()
155 return request.method() === 'PUT' && response.url().includes('/for-admins/upload/')
156 }
157
158 function formatEntry(entry: Record<string, unknown>) {
159 return Object.entries(entry)
160 .filter(([, value]) => value !== undefined)
161 .map(([key, value]) => `${key}=${String(value)}`)
162 .join(' ')
163 }
164
165 }
166
167 const NETWORK_PRESETS = {
168 Offline: {
169 offline: true,
170 downloadThroughput: 0,
171 uploadThroughput: 0,
172 latency: 0,
173 connectionType: 'none',
174 },
175 NoThrottle: {
176 offline: false,
177 downloadThroughput: -1,
178 uploadThroughput: -1,
179 latency: 0,
180 },
181 Regular2G: {
182 offline: false,
183 downloadThroughput: (250 * 1024) / 8,
184 uploadThroughput: (120 * 1024) / 8,
185 latency: 300,
186 connectionType: 'cellular2g',
187 },
188 } as const
189
190 // some interactions, no screenshots
191 test('admin2', async ({ page, browserName }) => {
192 const isPhone = await loginAdmin(page)
193
194 await clickAdminMenu(page, 'Accounts')
195 await expect(page.getByText('admins', { exact: true })).toBeVisible()
196 await page.getByRole('button', { name: 'Add' }).click()
197 await page.getByRole('menuitem', { name: 'user' }).click()
198 const usernameField = page.getByRole('textbox', { name: 'Username' })
199 const passwordField = page.getByRole('textbox', { name: 'Password', exact: true })
200 await usernameField.fill('admin2-temp-user')
201 await passwordField.fill('admin2-temp-pass')
202 await page.getByRole('textbox', { name: 'Repeat password' }).fill('admin2-temp-pass')
203 await expect(usernameField).toHaveValue('admin2-temp-user')
204 const adminAccess = page.getByRole('switch', { name: 'Admin-panel access' })
205 await adminAccess.check()
206 await expect(adminAccess).toBeChecked()
207 await page.getByRole('textbox', { name: 'Notes' }).fill('admin2 expanded interactions')
208 if (isPhone)
209 await clickIconBtn('Close', page)
210
211 await clickAdminMenu(page, 'Options')
212 await expect(page.getByText('Correctly working on port')).toBeVisible()
213 await page.getByRole('button', { name: 'Reload' }).click()
214 await page.getByRole('row', { name: /^Blocked/ }).getByRole('button', { name: /Add/ }).click()
215 const addDialog = page.getByRole('dialog', { name: /Add/ })
216 await addDialog.getByRole('textbox', { name: 'Blocked IP' }).fill('5.6.7.8')
217 if (!isPhone) {
218 // This field uses a masked input: selecting from picker is more reliable than typing.
219 await addDialog.getByRole('button', { name: 'Choose date' }).click()
220 const picker = page.getByRole('dialog', { name: 'Expire' })
221 // desktop calendars render hidden fillers and disabled days as gridcells too, so click a real enabled day button
222 await picker.locator('button[role="gridcell"]:not([disabled]):not([aria-disabled="true"])').first().click()
223 await page.keyboard.press('Escape')
224 }
225 await addDialog.getByRole('button').last().click()
226 await expect(addDialog).not.toBeVisible()
227 await expect(page.getByRole('heading', { name: 'Options', exact: true })).toBeVisible() // still on the same page
228 await expect(page.getByText('5.6.7.8')).toBeVisible()
229 await page.getByRole('button', { name: 'Reload' }).click()
230 await expect(page.getByText('5.6.7.8')).not.toBeVisible()
231
232 await clickAdminMenu(page, 'Logs')
233 await expect(page.getByRole('tab', { name: 'Served', exact: true })).toBeVisible()
234 const logTabs = page.getByRole('tab')
235 await logTabs.nth(1).click()
236 await logTabs.nth(2).click()
237 await logTabs.nth(3).click()
238 await logTabs.nth(4).click()
239 await logTabs.nth(0).click()
240 const pauseBtn = page.getByLabel('Pause').getByRole('button')
241 await expect(pauseBtn).toHaveAttribute('aria-pressed', 'true')
242 await pauseBtn.click()
243 await expect(pauseBtn).toHaveAttribute('aria-pressed', 'false')
244 await pauseBtn.click()
245 await expect(pauseBtn).toHaveAttribute('aria-pressed', 'true')
246 const showApisBtn = page.getByRole('button', { name: 'Show APIs' })
247 await expect(showApisBtn).toHaveAttribute('aria-pressed', 'true')
248 await showApisBtn.click()
249 await expect(showApisBtn).toHaveAttribute('aria-pressed', 'false')
250 await showApisBtn.click()
251 await expect(showApisBtn).toHaveAttribute('aria-pressed', 'true')
252 await clickIconBtn('Options', page)
253 if (!isPhone) {
254 const logsDialog = page.getByRole('dialog', { name: /Log options/ })
255 const logApisToggle = logsDialog.getByRole('switch', { name: 'Log API requests' })
256 await expect(logApisToggle).toBeChecked()
257 await logApisToggle.click()
258 await expect(logApisToggle).not.toBeChecked()
259 await logApisToggle.click()
260 await expect(logApisToggle).toBeChecked()
261 }
262 await clickIconBtn('Close', page)
263
264 await clickAdminMenu(page, 'Plugins')
265 await expect(page.getByText('antibrute')).toBeVisible()
266 const pluginTabs = page.getByRole('tab')
267 await pluginTabs.nth(0).click()
268 // plugin state is shared by all browser projects, so mutate it from one project to avoid cross-browser races
269 if (!isPhone && browserName === 'chromium') {
270 const downloadCounterRow = page.getByRole('row', { name: /download-counter/ })
271 const startDownloadCounter = downloadCounterRow.getByRole('button', { name: 'Start download-counter' })
272 const stopDownloadCounter = downloadCounterRow.getByRole('button', { name: 'Stop download-counter' })
273 // keep the test independent from whatever state a previous run left this plugin in
274 const wasRunning = await stopDownloadCounter.isVisible()
275 if (!wasRunning)
276 await startDownloadCounter.click()
277 await downloadCounterRow.getByRole('button', { name: 'Options' }).click()
278 const whereField = page.getByRole('combobox', { name: 'Where to display counter' })
279 await whereField.click()
280 await page.getByRole('option', { name: 'list', exact: true }).click()
281 const pluginOptionsDialog = page.getByRole('dialog')
282 const pluginSaveBtn = pluginOptionsDialog.locator('button:has-text("Save")').first()
283 await expect(pluginSaveBtn).toBeEnabled()
284 await clickIconBtn('Close', page)
285 if (!wasRunning)
286 await stopDownloadCounter.click()
287 }
288 await pluginTabs.nth(1).click() // get more
289 await page.getByRole('textbox', { name: 'Search text' }).fill('download')
290
291 await clickAdminMenu(page, 'Custom HTML')
292 const sectionStyle = page.getByRole('combobox', { name: 'Section Style' })
293 await expect(sectionStyle).toBeVisible()
294 await sectionStyle.click()
295 await page.getByRole('option').nth(1).click()
296
297 await clickAdminMenu(page, 'Internet')
298 await expect(page.getByText('Server')).toBeVisible()
299
300 await clickAdminMenu(page, 'Logout')
301 })