| 1 | import { test, expect, Page } from '@playwright/test' |
| 2 | import fs from 'fs' |
| 3 | import { wait } from '../src/cross' |
| 4 | import { |
| 5 | clickAdminMenu, clickIconBtn, forwardConsole, loginAdmin, password, resetTimestamp, FRONTEND_URL, username, TEST_PORT |
| 6 | } from './common' |
| 7 | |
| 8 | const screenshotStyle = fs.readFileSync('e2e/screenshot.css', 'utf8') |
| 9 | const screenshotCounters = new WeakMap<object, number>() |
| 10 | |
| 11 | // a generic test touch several parts |
| 12 | test('around1', async ({ page }) => { |
| 13 | forwardConsole(page) |
| 14 | resetTimestamp() |
| 15 | await page.goto(FRONTEND_URL) |
| 16 | await expect(page).toHaveTitle(/File server/) |
| 17 | await screenshot(page) |
| 18 | await page.getByRole('button', { name: 'Login' }).click() |
| 19 | await expect(page.getByRole('dialog', {})).toBeVisible() |
| 20 | await screenshot(page) |
| 21 | |
| 22 | await page.getByRole('textbox', { name: 'Username' }).fill(username + '!') // wrong username |
| 23 | await page.getByRole('textbox', { name: 'Password' }).fill(password) |
| 24 | await page.getByRole('button', { name: 'Continue' }).click() |
| 25 | await expect(page.getByText('x!ErrorInvalid credentials')).toBeVisible() |
| 26 | await page.getByRole('alertdialog').getByRole('button', { name: 'Close' }).click() |
| 27 | |
| 28 | resetTimestamp() |
| 29 | await page.getByRole('textbox', { name: 'Username' }).fill(username) |
| 30 | await page.getByRole('button', { name: 'Continue' }).click() |
| 31 | await page.locator('div').filter({ hasText: 'Logged in' }).nth(3).click() |
| 32 | await screenshot(page) |
| 33 | |
| 34 | // selecting in this folder should enable bulk delete button |
| 35 | await page.getByRole('link', { name: 'for-admins, Folder' }).click() |
| 36 | await page.getByRole('link', { name: 'upload, Folder' }).click() |
| 37 | await page.getByRole('link', { name: 'alfa.txt' }).click() |
| 38 | await expect(page.getByText('Delete')).toBeVisible() // first check single-delete command |
| 39 | await page.getByRole('button', { name: 'Close' }).click() |
| 40 | await page.getByRole('button', { name: 'Select' }).click() |
| 41 | await page.getByRole('checkbox', { name: 'alfa.txt' }).check() |
| 42 | await expect(page.getByRole('button', { name: 'Delete' })).toBeEnabled() |
| 43 | await page.getByRole('button', { name: 'Select' }).click() |
| 44 | |
| 45 | await page.getByRole('link', { name: 'home' }).click() |
| 46 | await page.getByRole('button', { name: username }).click() |
| 47 | await page.getByRole('button', { name: 'Logout' }).click() |
| 48 | await page.getByText('Logged out').click() |
| 49 | await page.getByRole('link', { name: 'cantListBut, Folder' }).click() |
| 50 | await page.getByText('x!WarningForbidden').click() |
| 51 | await page.getByRole('button', { name: 'Close' }).click() |
| 52 | await page.getByRole('link', { name: 'cantListPage, Folder' }).click() |
| 53 | await page.getByRole('link', { name: 'alfa.txt' }).click() |
| 54 | await expect(page.getByRole('dialog')).toMatchAriaSnapshot(` |
| 55 | - dialog: |
| 56 | - button "Close" |
| 57 | - heading "File menu" [level=1] |
| 58 | - term: Name |
| 59 | - definition: alfa.txt |
| 60 | - term: Size |
| 61 | - definition: 6 B |
| 62 | - term: Timestamp |
| 63 | - definition: /\\d+\\/\\d+\\/\\d+, \\d+:\\d+:\\d+/ |
| 64 | - term: Creation |
| 65 | - definition: /\\d+\\/\\d+\\/\\d+, \\d+:\\d+:\\d+/ |
| 66 | - link "Download" |
| 67 | - link "Open" |
| 68 | `) |
| 69 | await page.getByRole('link', { name: 'Download' }).click() // this also closes the dialog |
| 70 | await page.getByRole('link', { name: 'alfa.txt', exact: true }).click() |
| 71 | await screenshot(page) |
| 72 | await page.getByRole('button', { name: 'Close' }).click() |
| 73 | |
| 74 | await page.getByRole('link', { name: 'cantListPage' }).click() |
| 75 | await page.getByRole('button', { name: 'Calculate' }).click() |
| 76 | await page.getByText('KB / 2 files').click() |
| 77 | await page.locator('#menu-prop-name').getByText('cantListPage').click() |
| 78 | const downloadPromise = page.waitForEvent('download') |
| 79 | await page.getByRole('link', { name: 'Download' }).click() |
| 80 | await downloadPromise |
| 81 | await page.getByRole('link', { name: 'cantListPage' }).click() |
| 82 | const pageListPromise = page.waitForEvent('popup') |
| 83 | await page.getByRole('link', { name: 'Get list' }).click() |
| 84 | const pageList = await pageListPromise |
| 85 | await expect(pageList.getByText('::1')).toBeVisible() |
| 86 | await pageList.close() |
| 87 | |
| 88 | await page.getByRole('link', { name: 'home' }).click() |
| 89 | await page.getByRole('button', { name: 'Select' }).click() |
| 90 | await page.getByRole('textbox', { name: 'Type here to filter the list' }).click() |
| 91 | await page.getByRole('textbox', { name: 'Type here to filter the list' }).fill('x') |
| 92 | await page.getByText('filtered').click() |
| 93 | await screenshot(page) |
| 94 | await page.getByRole('button', { name: 'Select' }).click() |
| 95 | await page.getByText('file, 10 folders, 6 B').click() |
| 96 | await page.getByRole('link', { name: 'cantListPageAlt, Folder' }).click() |
| 97 | await page.getByRole('link', { name: 'home' }).click() |
| 98 | await page.getByRole('link', { name: 'f1, Folder' }).click() |
| 99 | await page.getByRole('link', { name: 'page, Folder' }).click() |
| 100 | await page.getByRole('img', { name: 'gpl logo' }).click() |
| 101 | await page.getByRole('heading', { name: 'This is a test' }).click() |
| 102 | await page.goBack() |
| 103 | await page.getByRole('link', { name: 'home' }).click() |
| 104 | |
| 105 | const isPhone = await page.evaluate(() => window.matchMedia("(max-width: 600px)").matches) |
| 106 | if (isPhone) |
| 107 | await page.getByRole('listitem').filter({ hasText: 'for-disabled' }).getByRole('button').click() |
| 108 | else |
| 109 | await page.getByRole('listitem').filter({ hasText: 'for-disabledMenu' }).getByRole('button').click() |
| 110 | await expect(page.getByText('Missing permission')).toBeVisible() |
| 111 | await page.getByRole('button', { name: 'Close' }).click() |
| 112 | await page.getByRole('link', { name: 'cantSearchForMasks, Folder' }).click() |
| 113 | await expect(page.getByRole('link', { name: 'cantSearchForMasks' })).toBeVisible() |
| 114 | await page.getByRole('link', { name: 'cantSearchForMasks' }).click() |
| 115 | await expect(page.locator('#menu-prop-name').getByText('cantSearchForMasks')).toBeVisible() |
| 116 | await page.getByRole('button', { name: 'Close' }).click() |
| 117 | await page.getByRole('link', { name: 'cantSearchForMasks' }).click() |
| 118 | await expect(page.getByText('xFolder')).toBeVisible() |
| 119 | await page.getByRole('button', { name: 'Close' }).click() |
| 120 | await page.getByRole('link', { name: 'home' }).click() |
| 121 | |
| 122 | await page.getByRole('button', { name: 'Options' }).click() |
| 123 | await page.getByRole('slider').fill('6') |
| 124 | await page.getByRole('button', { name: 'Close' }).click() |
| 125 | await expect(page.locator('.list-wrapper')).toHaveClass(/tiles-mode/) |
| 126 | |
| 127 | const item = page.locator('.list-wrapper li').filter({ has: page.locator('.link-wrapper > a') }).first() |
| 128 | const link = item.locator('.link-wrapper > a') |
| 129 | const name = item.locator('.entry-name') |
| 130 | await expect(link).toBeVisible() |
| 131 | await expect(name).toBeVisible() |
| 132 | |
| 133 | // safari can shrink the inline link box, so compare the geometry directly against the name |
| 134 | const [linkBox, nameBox] = await Promise.all([link.boundingBox(), name.boundingBox()]) |
| 135 | expect(linkBox).not.toBeNull() |
| 136 | expect(nameBox).not.toBeNull() |
| 137 | expect(linkBox!.y + linkBox!.height).toBeGreaterThanOrEqual(nameBox!.y + nameBox!.height - 1) |
| 138 | }) |
| 139 | |
| 140 | test('search1', async ({ page }) => { |
| 141 | resetTimestamp() |
| 142 | await page.goto(FRONTEND_URL) |
| 143 | await page.getByRole('button', { name: 'Search' }).click() |
| 144 | await page.locator('input[name="name"]').fill('a') |
| 145 | await page.getByRole('button', { name: 'Continue' }).click() |
| 146 | await page.getByText('12 folders').click() |
| 147 | await page.getByRole('link', { name: 'cantListPage/ alfa.txt' }).click() |
| 148 | await page.getByRole('button', { name: 'Close' }).click() |
| 149 | await page.getByRole('button', { name: 'Clear search' }).click() |
| 150 | |
| 151 | await page.getByRole('button', { name: 'Search' }).click() |
| 152 | await page.locator('input[name="name"]').fill('a*') |
| 153 | await page.locator('input[name="name"]').press('Enter') |
| 154 | await page.getByText('files, 42 B').click() |
| 155 | |
| 156 | await page.getByRole('link', { name: 'home' }).click() |
| 157 | await page.getByRole('button', { name: 'Close' }).click() |
| 158 | await page.getByRole('link', { name: 'home' }).click() |
| 159 | await page.getByRole('button', { name: 'Close' }).click() |
| 160 | await page.getByRole('button', { name: 'Clear search' }).click() |
| 161 | await page.getByRole('link', { name: 'home' }).click() |
| 162 | await expect(page.getByText('xFolder')).toBeVisible() |
| 163 | await page.getByRole('button', { name: 'Close' }).click() |
| 164 | await page.getByRole('button', { name: 'Options' }).click() |
| 165 | await expect(page.locator('#option-sort-by')).toBeVisible() |
| 166 | await expect(page.locator('#option-sort-by')).toBeVisible() |
| 167 | await page.getByRole('dialog').locator('div').nth(2).click() |
| 168 | await page.locator('#option-sort-by').selectOption('size') |
| 169 | await page.getByRole('checkbox', { name: 'Invert order' }).check() |
| 170 | await page.getByRole('slider').fill('6') |
| 171 | await page.locator('#option-theme').selectOption('dark') |
| 172 | await page.getByRole('button', { name: 'Close' }).click() |
| 173 | await page.getByRole('link', { name: 'cantListPageAlt, Folder' }).click() |
| 174 | await expect(page.getByText('alfa.txt')).toBeVisible() // be sure the folder is loaded |
| 175 | await page.mouse.click(1, 1) // avoid focus inconsistencies |
| 176 | await screenshot(page) |
| 177 | |
| 178 | await page.getByRole('button', { name: 'Zip' }).click() |
| 179 | await expect(page.getByRole('dialog')).toMatchAriaSnapshot(` |
| 180 | - dialog: |
| 181 | - button "Close" |
| 182 | - heading "Confirm" [level=1] |
| 183 | - paragraph: Download WHOLE folder as ZIP archive? |
| 184 | - link "Yes": |
| 185 | - button "Yes" |
| 186 | - button "Don't" |
| 187 | - button "Select some files" |
| 188 | `) |
| 189 | await page.getByRole('button', { name: "Don't" }).click() |
| 190 | await page.getByRole('button', { name: 'Zip' }).click() |
| 191 | await page.getByRole('button', { name: 'Select some files' }).click() |
| 192 | await page.getByText('Use checkboxes to select the').click() |
| 193 | await page.getByRole('button', { name: 'Close' }).click() |
| 194 | await page.getByRole('textbox', { name: 'Type here to filter the list' }).click() |
| 195 | }) |
| 196 | |
| 197 | test('frontend-admin', async ({ page }) => { |
| 198 | await page.goto(FRONTEND_URL, { waitUntil: 'networkidle' }) |
| 199 | await page.evaluate(() => document.fonts.ready) // aspetta i font |
| 200 | await page.getByRole('button', { name: 'Options' }).click() |
| 201 | // no admin button yet, |
| 202 | await expect(page.getByRole('dialog')).toMatchAriaSnapshot(` |
| 203 | - dialog: |
| 204 | - button "Close" |
| 205 | - heading "Options" [level=1] |
| 206 | - combobox: |
| 207 | - 'option "Sort by: name" [selected]' |
| 208 | - 'option "Sort by: extension"' |
| 209 | - 'option "Sort by: size"' |
| 210 | - 'option "Sort by: time"' |
| 211 | - 'option "Sort by: creation"' |
| 212 | - checkbox "Invert order" |
| 213 | - text: Invert order |
| 214 | - checkbox "Folders first" [checked] |
| 215 | - text: Folders first |
| 216 | - checkbox "Numeric names" |
| 217 | - text: "Numeric names Tiles mode: off" |
| 218 | - slider: "0" |
| 219 | - combobox: |
| 220 | - 'option "Theme: auto" [selected]' |
| 221 | - 'option "Theme: light"' |
| 222 | - 'option "Theme: dark"' |
| 223 | `) |
| 224 | await page.getByRole('button', { name: 'Close' }).click() |
| 225 | await page.getByRole('button', { name: 'Login' }).click() |
| 226 | await page.getByRole('textbox', { name: 'Username' }).fill(username) |
| 227 | await page.getByRole('textbox', { name: 'Password' }).fill(password) |
| 228 | await page.getByRole('textbox', { name: 'Password' }).press('Enter') |
| 229 | await page.getByRole('button', { name: 'Options' }).click() |
| 230 | const page1Promise = page.waitForEvent('popup') |
| 231 | await page.getByRole('button', { name: 'Admin-panel' }).click() |
| 232 | const page1 = await page1Promise |
| 233 | await expect(page1).toHaveTitle(/HFS Admin-panel/) |
| 234 | }) |
| 235 | |
| 236 | test('admin1', async ({ page }) => { |
| 237 | await fs.promises.rm('tests/work/logs', { force: true, recursive: true }) // clear logs to have consistent screenshots |
| 238 | const isPhone = await loginAdmin(page) |
| 239 | |
| 240 | function dataTableLoading() { |
| 241 | return expect(page.getByRole('grid').getByRole('img')).toBeVisible({ visible: false }) |
| 242 | } |
| 243 | |
| 244 | await clickAdminMenu(page, 'Internet') // initiate the get_nat process, so we'll have to wait less, later |
| 245 | await clickAdminMenu(page, 'Shared files') |
| 246 | await expect(page.getByText('cantListBut')).toBeVisible() // wait for data |
| 247 | if (!isPhone) |
| 248 | await expect(page.getByText('Your shared files')).toBeVisible() // wait for data |
| 249 | await screenshot(page) |
| 250 | await clickAdminMenu(page, 'Accounts') |
| 251 | await expect(page.getByText('admins', { exact: true })).toBeVisible() // wait for data |
| 252 | await screenshot(page) |
| 253 | await page.getByText('rejetto(admins,').click() |
| 254 | await screenshot(page) |
| 255 | if (isPhone) |
| 256 | await clickIconBtn('Close', page) |
| 257 | await clickAdminMenu(page, 'Options') |
| 258 | await expect(page.getByText('Correctly working on port')).toBeVisible() // wait for data |
| 259 | if (!isPhone) |
| 260 | await expect(page.getByText('Expire', { exact: true })).toBeVisible() // wait for layout of 'block' table |
| 261 | await page.mouse.click(1, 1) // avoid focus inconsistencies |
| 262 | await screenshot(page) |
| 263 | |
| 264 | await clickAdminMenu(page, 'Logs') |
| 265 | await dataTableLoading() |
| 266 | await screenshot(page) |
| 267 | await clickIconBtn('Options', page) |
| 268 | await page.getByRole('textbox', { name: 'Served', exact: true }).click() |
| 269 | await clickIconBtn('Close', page) |
| 270 | await clickAdminMenu(page, 'Language') |
| 271 | await dataTableLoading() |
| 272 | if (!isPhone) |
| 273 | await expect(page.getByText('author', { exact: true })).toBeVisible() // wait for the layout to be stable |
| 274 | await screenshot(page, '.MuiDataGrid-root') |
| 275 | await clickAdminMenu(page, 'Plugins') |
| 276 | await expect(page.getByText('antibrute')).toBeVisible() // wait for data |
| 277 | // ensure Test plugin is running |
| 278 | const stopTest = page.getByRole('button', { name: 'Stop test' }) |
| 279 | if (!await stopTest.isVisible()) { |
| 280 | await page.getByRole('button', { name: 'Start test' }).click() |
| 281 | await expect(stopTest).toBeVisible() |
| 282 | } |
| 283 | // reload the list from the server so the plugin screenshot doesn't depend on SSE timing |
| 284 | await page.reload() |
| 285 | await expect(page.getByRole('heading', { name: 'Plugins', exact: true })).toBeVisible() |
| 286 | await expect(page.getByRole('button', { name: 'Stop test' })).toBeVisible() |
| 287 | |
| 288 | await screenshot(page) |
| 289 | await page.getByRole('tab', { name: 'Get more' }).click() |
| 290 | await page.getByRole('tab', { name: 'updates' }).click() |
| 291 | await clickAdminMenu(page, 'Custom HTML') |
| 292 | await expect(page.getByRole('combobox', { name: 'Section Style' })).toBeVisible() // wait for data to be loaded |
| 293 | await screenshot(page) |
| 294 | await page.getByRole('main').click() |
| 295 | await clickAdminMenu(page, 'Internet') |
| 296 | await expect(page.getByText(`port ${TEST_PORT}`)).toBeVisible({ timeout: 15000 }) // wait for data (get_nat can be very slow) |
| 297 | await page.mouse.click(1, 1) // avoid focus inconsistencies |
| 298 | await screenshot(page) |
| 299 | await clickAdminMenu(page, 'Logout') |
| 300 | await screenshot(page) |
| 301 | }) |
| 302 | |
| 303 | async function screenshot(page: Page, selectorForMask = '') { |
| 304 | if (process.env.NO_SS) return |
| 305 | const testInfo = test.info() |
| 306 | const snapshotName = nextScreenshotName(testInfo) |
| 307 | const snapshotPath = testInfo.snapshotPath(snapshotName, { kind: 'screenshot' }) |
| 308 | if (selectorForMask) |
| 309 | selectorForMask = ',' + selectorForMask |
| 310 | await wait(200) // this accounts especially for our DataTable component which takes time to set the layout |
| 311 | const mask = [page.locator(`.maskInTests${selectorForMask}`)] |
| 312 | // write the missing baseline ourselves so Playwright does not turn the first run into a failure |
| 313 | if (!fs.existsSync(snapshotPath)) { |
| 314 | await page.screenshot({ |
| 315 | path: snapshotPath, |
| 316 | fullPage: true, |
| 317 | mask, |
| 318 | animations: 'disabled', |
| 319 | caret: 'hide', |
| 320 | scale: 'css', |
| 321 | style: screenshotStyle, |
| 322 | }) |
| 323 | return |
| 324 | } |
| 325 | return expect(page).toHaveScreenshot(snapshotName, { fullPage: true, mask }) |
| 326 | } |
| 327 | |
| 328 | function nextScreenshotName(testInfo: ReturnType<typeof test.info>) { |
| 329 | const nextIndex = (screenshotCounters.get(testInfo) ?? 0) + 1 |
| 330 | screenshotCounters.set(testInfo, nextIndex) |
| 331 | const testName = testInfo.titlePath.slice(1).join(' ') |
| 332 | return `${testName}-${nextIndex}.png` |
| 333 | } |
| 334 | |
| 335 | test('anew', async ({ page, browserName }) => { |
| 336 | forwardConsole(page) |
| 337 | if (page.viewportSize()?.width! < 1000 || browserName !== 'chromium') return // test only for desktop chromium |
| 338 | // reset config so each run starts from the same default workspace state |
| 339 | const port = 8082 |
| 340 | do { |
| 341 | fs.writeFileSync('tests/work2/config.yaml', `port: ${port}\nopen_browser_at_start: false\n`) |
| 342 | await wait(500) |
| 343 | } while (await page.goto(`http://localhost:${port}/`).then(() => 0, () => 1)) |
| 344 | await expect(page.getByText('Nothing here')).toBeVisible() |
| 345 | await page.getByRole('button', { name: 'Options' }).click() |
| 346 | const page1Promise = page.waitForEvent('popup') |
| 347 | await page.getByRole('button', { name: 'Admin-panel' }).click() |
| 348 | const adminPage = await page1Promise |
| 349 | await adminPage.getByRole('link', { name: 'add some' }).click() |
| 350 | const addBtn = adminPage.getByRole('button', { name: 'Add item to virtual file system' }) |
| 351 | await addBtn.click() |
| 352 | await adminPage.getByRole('menuitem', { name: 'from disk' }).click() |
| 353 | await expect(adminPage.getByText('data.kv')).toBeVisible() |
| 354 | await adminPage.getByRole('textbox', { name: /Filter results/ }).fill('data') |
| 355 | await expect(adminPage.getByText('Filter results (1/')).toBeVisible() |
| 356 | await adminPage.getByRole('checkbox').first().check() |
| 357 | await adminPage.getByText('data.kv').first().click() |
| 358 | await addBtn.click() |
| 359 | await adminPage.getByRole('menuitem', { name: 'from disk' }).click() |
| 360 | await adminPage.getByRole('button', { name: 'Select this folder' }).click() |
| 361 | await addBtn.click() |
| 362 | await adminPage.getByRole('menuitem', { name: 'virtual folder' }).click() |
| 363 | await adminPage.getByRole('textbox').fill('folder1') |
| 364 | await adminPage.getByRole('textbox').press('Enter') |
| 365 | await adminPage.locator('#vfs').click() |
| 366 | await adminPage.getByText('folder1', { exact: true }).click() |
| 367 | await clickIconBtn('Cut', adminPage) |
| 368 | await adminPage.locator('div').filter({ hasText: 'InfoNow that this is marked' }).nth(1).click() |
| 369 | await clickIconBtn('Close', adminPage) |
| 370 | await adminPage.getByRole('treeitem', { name: 'Home folder', exact: true }) |
| 371 | .getByText('Home folder', { exact: true }).click() |
| 372 | await clickIconBtn('/work2/folder1/', adminPage) // paste button |
| 373 | await adminPage.getByText('data.kv').click() |
| 374 | await clickIconBtn('Cut', adminPage) |
| 375 | await clickIconBtn('Close', adminPage) |
| 376 | await adminPage.getByText('folder1').click() |
| 377 | await clickIconBtn('/data.kv', adminPage) // paste |
| 378 | await clickIconBtn('Save', adminPage) |
| 379 | await page.getByRole('button', { name: 'Close' }).click() |
| 380 | await page.getByRole('link', { name: 'home' }).click() |
| 381 | await page.getByRole('link', { name: 'Reload' }).click() |
| 382 | await page.getByRole('link', { name: 'folder1, Folder' }).click() |
| 383 | await page.getByRole('link', { name: 'data.kv' }).click() |
| 384 | await page.getByRole('button', { name: 'Close' }).click() |
| 385 | await page.locator('.list-wrapper > div').press('Control+Backspace') |
| 386 | await page.getByRole('link', { name: 'work2, Folder' }).click() |
| 387 | await page.getByRole('link', { name: 'config.yaml', exact: true }).click() |
| 388 | const page2Promise = page.waitForEvent('popup') |
| 389 | await page.getByRole('link', { name: 'Open' }).click() |
| 390 | const page2 = await page2Promise |
| 391 | await page2.getByText(/folder1/).click() |
| 392 | }) |
| 393 | |
| 394 | test('order field', async ({ page }) => { |
| 395 | await page.goto(FRONTEND_URL + 'renameChild/orderTest/') |
| 396 | await expect(page.getByText('orderTest')).toBeVisible() |
| 397 | await expect(page.locator('.entry-name')).toHaveText(['B', 'A', 'C']) |
| 398 | await page.goto(FRONTEND_URL + 'renameChild/') |
| 399 | await page.getByRole('link', { name: 'gui#%2, Folder' }).click() |
| 400 | await expect(page.getByRole('link', { name: 'alfa.txt' })).toBeVisible() |
| 401 | await expect(page.getByText('Not found')).not.toBeVisible() |
| 402 | }) |