forked from ipfs/ipfs-webui
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfiles.test.js
More file actions
561 lines (441 loc) · 23.6 KB
/
Copy pathfiles.test.js
File metadata and controls
561 lines (441 loc) · 23.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
import { test, expect } from './setup/coverage.js'
import { fixtureData } from './fixtures/index.js'
import { files, explore, modal, nav, dismissImportNotification } from './setup/locators.js'
import { selectViewMode, toggleSearchFilter } from '../helpers/grid'
import all from 'it-all'
import filesize from 'filesize'
import * as kuboRpcModule from 'kubo-rpc-client'
test.describe('Files screen', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/#/files')
// dismiss any lingering import notification from previous tests
await dismissImportNotification(page)
})
test('should have the active Add menu', async ({ page }) => {
const importButton = files.importButton(page)
await expect(importButton).toBeVisible()
await importButton.click()
await expect(files.addFileOption(page)).toBeVisible()
await expect(files.addFolderOption(page)).toBeVisible()
await expect(files.addByPathOption(page)).toBeVisible()
await expect(files.addNewFolderOption(page)).toBeVisible()
await expect(page.getByText('Bulk import')).toBeVisible()
// close menu with Escape key
await page.keyboard.press('Escape')
})
test('should allow for a successful import of two files', async ({ page }) => {
const importButton = files.importButton(page)
await expect(importButton).toBeVisible()
await importButton.click()
await expect(files.addFileOption(page)).toBeVisible()
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
files.addFileOption(page).click()
])
// select static text files via fileChooser
const file1 = fixtureData('file.txt')
const file2 = fixtureData('file2.txt')
await fileChooser.setFiles([file1.path, file2.path])
// expect files to be added to the file list
const fileRow1 = page.getByTestId('file-row').filter({ hasText: 'file.txt' })
const fileRow2 = page.getByTestId('file-row').filter({ hasText: 'file2.txt' })
await expect(fileRow1).toBeVisible()
await expect(fileRow2).toBeVisible()
// expect valid CID to be present on the page
const ipfs = kuboRpcModule.create(process.env.IPFS_RPC_ADDR)
const [result1, result2] = await all(ipfs.addAll([file1.data, file2.data]))
await expect(page.getByText(result1.cid.toString()).first()).toBeVisible()
await expect(page.getByText(result2.cid.toString()).first()).toBeVisible()
// expect human readable sizes in format from ./src/lib/files.js#humanSize
const human = (b) => (b
? filesize(b, {
standard: 'iec',
base: 2,
round: b >= 1073741824 ? 1 : 0
})
: '-')
// only check the files we just uploaded
for (const fileName of ['file.txt', 'file2.txt']) {
const stat = await ipfs.files.stat(`/${fileName}`)
const fileRow = page.getByTestId('file-row').filter({ hasText: fileName })
await expect(fileRow).toBeVisible()
await expect(fileRow.getByText(human(stat.size))).toBeVisible()
}
})
test('should have active Context menu that allows Inspection of the DAG', async ({ page }) => {
// dedicated test file to make this isolated from the rest
const testFilename = 'explorer-context-menu-test.txt'
const testCid = 'bafkqaddjnzzxazldoqwxizltoq'
// first: create a test file via "Add by path"
const importButton = files.importButton(page)
await expect(importButton).toBeVisible()
await importButton.click()
await expect(files.addByPathOption(page)).toBeVisible()
await files.addByPathOption(page).click()
// wait for dialog inputs to be visible (dialog may have animation)
const pathInput = files.dialogInput(page, 'path')
await expect(pathInput).toBeVisible()
await pathInput.fill(`/ipfs/${testCid}`)
await files.dialogInput(page, 'name').fill(testFilename)
await page.keyboard.press('Enter')
// expect file with matching filename to be added to the file list
const fileRow = page.getByTestId('file-row').filter({ hasText: testFilename })
await expect(fileRow).toBeVisible()
// open context menu
const contextButton = files.contextMenuButton(page, testFilename)
await expect(contextButton).toBeVisible()
await contextButton.click()
const inspectMenuItem = files.contextMenuItem(page, 'Inspect')
await expect(inspectMenuItem).toBeVisible()
// click on Inspect option
await inspectMenuItem.click()
// confirm Explore screen was opened with correct CID
await page.waitForURL(`/#/explore/${testCid}`)
await expect(page.getByText('CID info')).toBeVisible()
})
test('should successfully import file via Add by path', async ({ page }) => {
// bafkqac3imvwgy3zao5xxe3de is the inlined CID for "hello world"
const testCid = 'bafkqac3imvwgy3zao5xxe3de'
const testPath = `/ipfs/${testCid}`
const testFilename = 'add-by-path-test.txt'
// Open Import menu and click "From IPFS"
const importButton = files.importButton(page)
await expect(importButton).toBeVisible()
await importButton.click()
await expect(files.addByPathOption(page)).toBeVisible()
await files.addByPathOption(page).click()
// Fill in the dialog
const pathInput = files.dialogInput(page, 'path')
await expect(pathInput).toBeVisible()
await pathInput.fill(testPath)
await files.dialogInput(page, 'name').fill(testFilename)
// Click Import button in the dialog
const importDialogButton = modal.container(page).getByRole('button', { name: 'Import' })
await expect(importDialogButton).toBeVisible()
await importDialogButton.click()
// Wait for dialog to close
await expect(pathInput).not.toBeVisible({ timeout: 10000 })
// Verify file appears in the file list with the correct CID
const fileRow = page.getByTestId('file-row').filter({ hasText: testFilename })
await expect(fileRow).toBeVisible({ timeout: 30000 })
await expect(fileRow.getByText(testCid).first()).toBeVisible()
})
// Regression test: trailing slash in path should be handled correctly
test('should successfully import path with trailing slash', async ({ page }) => {
const testCid = 'bafkqac3imvwgy3zao5xxe3de'
// Path with trailing slash - should still work
const testPath = `/ipfs/${testCid}/`
const testFilename = 'trailing-slash-test.txt'
await files.importButton(page).click()
await files.addByPathOption(page).click()
const pathInput = files.dialogInput(page, 'path')
await expect(pathInput).toBeVisible()
await pathInput.fill(testPath)
await files.dialogInput(page, 'name').fill(testFilename)
await page.keyboard.press('Enter')
// Wait for dialog to close
await expect(pathInput).not.toBeVisible({ timeout: 10000 })
// Verify file appears with the custom filename
const fileRow = page.getByTestId('file-row').filter({ hasText: testFilename })
await expect(fileRow).toBeVisible({ timeout: 30000 })
})
test('should show error notification when importing non-existent path', async ({ page }) => {
// bafkqac3imvwgy3zao5xxe3de is the inlined CID for "hello world" (a file, not a dir)
// Trying to access /404 inside it fails because you can't traverse into a file
const nonExistentPath = '/ipfs/bafkqac3imvwgy3zao5xxe3de/404'
// Open Import menu and click "From IPFS"
const importButton = files.importButton(page)
await expect(importButton).toBeVisible()
await importButton.click()
await expect(files.addByPathOption(page)).toBeVisible()
await files.addByPathOption(page).click()
// Fill in the dialog with the non-existent path
const pathInput = files.dialogInput(page, 'path')
await expect(pathInput).toBeVisible()
await pathInput.fill(nonExistentPath)
// Click Import button to submit
const importDialogButton = modal.container(page).getByRole('button', { name: 'Import' })
await expect(importDialogButton).toBeVisible()
await importDialogButton.click()
// Wait for the dialog to close (the path input should disappear)
await expect(pathInput).not.toBeVisible({ timeout: 10000 })
// Wait for any import notification to appear
const notification = page.locator('.fileImportStatus')
await expect(notification).toBeVisible({ timeout: 30000 })
// The notification should show "Failed to import" instead of "Imported 0 items"
const errorNotification = page.locator('.fileImportStatus').filter({ hasText: /Failed to import/i })
await expect(errorNotification).toBeVisible()
// Verify error styling is applied (red background)
const errorContainer = page.locator('.fileImportStatusError')
await expect(errorContainer).toBeVisible()
// Verify the path is shown in the error details (in the fileImportStatusName span)
await expect(notification.locator('.fileImportStatusName').filter({ hasText: nonExistentPath })).toBeVisible()
// Verify the actual error message is displayed (in the dark-red error div)
const errorDetail = notification.locator('.dark-red.f7')
await expect(errorDetail).toBeVisible()
// Verify it contains the expected error from IPFS
await expect(errorDetail).toContainText('cp: cannot get node from path')
})
// Regression test: ensure previous import errors are cleared when starting a new import.
// Without the fix in doFilesAddPath, the second (successful) import would show
// "Failed to import 2 items" because errors from previous imports accumulate.
test('should clear previous errors when starting new import', async ({ page }) => {
const validPath = '/ipfs/bafkqac3imvwgy3zao5xxe3de'
const invalidPath = '/ipfs/bafkqac3imvwgy3zao5xxe3de/404'
// Use unique filename to avoid conflicts when tests run in parallel
const uniqueFilename = `clear-errors-test-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`
// First, import an invalid path to create an error
await files.importButton(page).click()
await files.addByPathOption(page).click()
await files.dialogInput(page, 'path').fill(invalidPath)
await modal.container(page).getByRole('button', { name: 'Import' }).click()
// Wait for error notification
const notification = page.locator('.fileImportStatus')
await expect(notification.filter({ hasText: /Failed to import 1 item/i })).toBeVisible({ timeout: 30000 })
// Verify error styling IS present after failed import
const errorContainer = page.locator('.fileImportStatusError')
await expect(errorContainer).toBeVisible()
// Now import a valid path with a unique filename to avoid conflicts with other parallel tests
await files.importButton(page).click()
await expect(files.addByPathOption(page)).toBeVisible()
await files.addByPathOption(page).click()
const pathInput = files.dialogInput(page, 'path')
await expect(pathInput).toBeVisible()
await pathInput.fill(validPath)
await files.dialogInput(page, 'name').fill(uniqueFilename)
// Press Enter to submit (notification overlaps Import button at bottom of screen)
await page.keyboard.press('Enter')
// Wait for success notification title - should show "Imported 1 item", not "Failed to import"
const notificationTitle = notification.locator('.fileImportStatusButton span').first()
await expect(notificationTitle).toHaveText('Imported 1 item', { timeout: 30000 })
// Verify success styling - no error class on the header
await expect(errorContainer).not.toBeVisible()
// Expand the drawer to check items (click on the header button)
const drawerButton = notification.locator('.fileImportStatusButton')
// Ensure drawer is expanded (aria-expanded="true")
const isExpanded = await drawerButton.getAttribute('aria-expanded')
if (isExpanded !== 'true') {
await drawerButton.click()
}
// Verify BOTH items shown in drawer (failed + successful)
await expect(notification.locator('.fileImportStatusName').filter({ hasText: invalidPath })).toBeVisible()
await expect(notification.locator('.fileImportStatusName').filter({ hasText: uniqueFilename })).toBeVisible()
})
test('should show error page when navigating to non-existing path', async ({ page }) => {
// bafyaabakaieac is CIDv1 of an empty directory, so /404 inside it does not exist
const nonExistingPath = '/ipfs/bafyaabakaieac/404'
// enter the path in the explore form input and click Browse
await explore.cidInput(page).fill(nonExistingPath)
await expect(explore.browseButton(page)).toBeEnabled()
await explore.browseButton(page).click()
// expect error page to be displayed with the correct title
await expect(page.getByRole('heading', { name: 'Unable to load this path' })).toBeVisible()
// expect the path to be displayed in the error message area (below the heading)
await expect(page.locator('p.truncate').filter({ hasText: 'bafyaabakaieac/404' })).toBeVisible()
// expect the "Go to Files" button to be present and working
const goToFilesButton = page.getByRole('link', { name: 'Go to Files' })
await expect(goToFilesButton).toBeVisible()
await goToFilesButton.click()
// confirm navigation back to files root
await page.waitForURL('/#/files')
})
test.describe('Search filter', () => {
// inline CIDs for small text content (no network needed)
const orangeCid = 'bafkqaddjnzzxazldoqwxizltoq'
const appleCid = 'bafkqac3imvwgy3zao5xxe3de'
const orangeFile = 'search-orange.txt'
const appleFile = 'search-apple.txt'
/**
* Import a file via "Add by path" using an inline CID.
*/
async function importByPath (page, cid, filename) {
await files.importButton(page).click()
await expect(files.addByPathOption(page)).toBeVisible()
await files.addByPathOption(page).click()
const pathInput = files.dialogInput(page, 'path')
await expect(pathInput).toBeVisible()
await pathInput.fill(`/ipfs/${cid}`)
await files.dialogInput(page, 'name').fill(filename)
await page.keyboard.press('Enter')
// wait for dialog to close
await expect(pathInput).not.toBeVisible({ timeout: 10000 })
}
test.beforeEach(async ({ page }) => {
test.slow()
// import two files with distinct names
await importByPath(page, orangeCid, orangeFile)
await dismissImportNotification(page)
await importByPath(page, appleCid, appleFile)
await dismissImportNotification(page)
// verify both files are visible before each test
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible({ timeout: 30000 })
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible({ timeout: 30000 })
})
test('search is hidden by default', async ({ page }) => {
await expect(files.searchInput(page)).not.toBeVisible()
await files.searchToggle(page).click()
await expect(files.searchInput(page)).toBeVisible()
})
test('filter by name in list view', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('orange')
// only the orange file should be visible
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
// filtered count should show "1 /"
await expect(page.getByText(/^1\s*\/\s*\d+$/)).toBeVisible()
})
test('filter by CID in list view', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
// get the CID from the orange file row
const orangeRow = page.getByTestId('file-row').filter({ hasText: orangeFile })
const cidText = await orangeRow.locator('div.monospace').textContent()
// use last 10 chars of the CID to avoid shared prefix between inline CIDs
const trimmed = cidText.trim()
const cidFragment = trimmed.slice(-10)
await files.searchInput(page).fill(cidFragment)
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
})
test('filter by name in grid view', async ({ page }) => {
await selectViewMode(page, 'grid')
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('apple')
// only the apple file should be visible
await expect(files.gridFileByName(page, appleFile)).toBeVisible()
await expect(files.gridFileByName(page, orangeFile)).not.toBeVisible()
})
test('hiding search clears filter', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('orange')
// only orange visible
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
// hide the search filter
await toggleSearchFilter(page, false)
await expect(files.searchInput(page)).not.toBeVisible()
// both files should be visible again
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible()
})
test('pressing Escape clears the filter', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('orange')
// only the orange file should be visible
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
// press Escape while focused on the search input
await files.searchInput(page).press('Escape')
// filter should be cleared and both files visible again
await expect(files.searchInput(page)).toHaveValue('')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible()
})
test('pressing / opens search and auto-focuses input', async ({ page }) => {
await selectViewMode(page, 'list')
// search should be hidden initially
await expect(files.searchInput(page)).not.toBeVisible()
// press / to open search (focus must not be in an input)
await page.keyboard.press('/')
// search input should appear and be focused
await expect(files.searchInput(page)).toBeVisible()
await expect(files.searchInput(page)).toBeFocused()
// type a filter term directly (input is focused)
await page.keyboard.type('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
})
test('pressing / re-focuses input when search is visible but not focused', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
// move focus out of the search input
await files.searchInput(page).blur()
await expect(files.searchInput(page)).not.toBeFocused()
// press / should re-focus the input, not hide search
await page.keyboard.press('/')
await expect(files.searchInput(page)).toBeVisible()
await expect(files.searchInput(page)).toBeFocused()
// filter text should still be intact
await expect(files.searchInput(page)).toHaveValue('orange')
})
test('pressing / focuses search after navigating away and back', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
// confirm search input is visible
await expect(files.searchInput(page)).toBeVisible()
// navigate to Status page
await nav.status(page).click()
await page.waitForURL('/#/')
// navigate back to Files
await nav.files(page).click()
await page.waitForURL('/#/files')
// search input should still be visible (persisted in localStorage)
await expect(files.searchInput(page)).toBeVisible()
// but it should NOT be focused on page load
await expect(files.searchInput(page)).not.toBeFocused()
// press / to focus the search input
await page.keyboard.press('/')
await expect(files.searchInput(page)).toBeFocused()
})
test('no matches message', async ({ page }) => {
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('nonexistent-file-xyz')
await expect(page.getByText('No files match your search')).toBeVisible()
})
test('select all only selects filtered files in grid view', async ({ page }) => {
await selectViewMode(page, 'grid')
await toggleSearchFilter(page, true)
// filter to show only the orange file
await files.searchInput(page).fill('orange')
await expect(files.gridFileByName(page, orangeFile)).toBeVisible()
await expect(files.gridFileByName(page, appleFile)).not.toBeVisible()
// click "Select all entries" checkbox (use label text since the input is absolutely positioned)
const selectAllLabel = page.getByText('Select all entries')
await expect(selectAllLabel).toBeVisible()
await selectAllLabel.click()
// the selected actions bar should show "1" (only the filtered file)
const selectedActions = page.locator('.selectedActions')
await expect(selectedActions).toBeVisible()
await expect(selectedActions.getByText('Item selected')).toBeVisible()
})
test('select all only selects filtered files in list view', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
// filter to show only the orange file
await files.searchInput(page).fill('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
// click "Select all entries" checkbox via its parent label (input is absolutely positioned)
const selectAllCheckbox = page.getByRole('checkbox', { name: 'Select all entries' })
await expect(selectAllCheckbox).toBeVisible()
await selectAllCheckbox.dispatchEvent('click')
// the selected actions bar should show "1" (only the filtered file)
const selectedActions = page.locator('.selectedActions')
await expect(selectedActions).toBeVisible()
await expect(selectedActions.getByText('Item selected')).toBeVisible()
})
test('filter persists when switching between list and grid view', async ({ page }) => {
await selectViewMode(page, 'list')
await toggleSearchFilter(page, true)
await files.searchInput(page).fill('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
// switch to grid view -- filter should persist
await selectViewMode(page, 'grid')
await expect(files.searchInput(page)).toBeVisible()
await expect(files.searchInput(page)).toHaveValue('orange')
await expect(files.gridFileByName(page, orangeFile)).toBeVisible()
await expect(files.gridFileByName(page, appleFile)).not.toBeVisible()
// switch back to list view -- filter should still persist
await selectViewMode(page, 'list')
await expect(files.searchInput(page)).toHaveValue('orange')
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
})
})
})