Skip to content

Commit 7639888

Browse files
committed
test(files): add e2e tests for search filter
cover search toggle visibility, filtering by name and CID in both list and grid views, hiding search to clear filter, and no-matches message.
1 parent 4b1384b commit 7639888

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
lines changed

test/e2e/files.test.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test, expect } from './setup/coverage.js'
22
import { fixtureData } from './fixtures/index.js'
33
import { files, explore, dismissImportNotification } from './setup/locators.js'
4+
import { selectViewMode, toggleSearchFilter } from '../helpers/grid'
45
import all from 'it-all'
56
import filesize from 'filesize'
67
import * as kuboRpcModule from 'kubo-rpc-client'
@@ -297,4 +298,118 @@ test.describe('Files screen', () => {
297298
// confirm navigation back to files root
298299
await page.waitForURL('/#/files')
299300
})
301+
302+
test.describe('Search filter', () => {
303+
// inline CIDs for small text content (no network needed)
304+
const orangeCid = 'bafkqaddjnzzxazldoqwxizltoq'
305+
const appleCid = 'bafkqac3imvwgy3zao5xxe3de'
306+
const orangeFile = 'search-orange.txt'
307+
const appleFile = 'search-apple.txt'
308+
309+
/**
310+
* Import a file via "Add by path" using an inline CID.
311+
*/
312+
async function importByPath (page, cid, filename) {
313+
await files.importButton(page).click()
314+
await expect(files.addByPathOption(page)).toBeVisible()
315+
await files.addByPathOption(page).click()
316+
317+
const pathInput = files.dialogInput(page, 'path')
318+
await expect(pathInput).toBeVisible()
319+
await pathInput.fill(`/ipfs/${cid}`)
320+
await files.dialogInput(page, 'name').fill(filename)
321+
await page.keyboard.press('Enter')
322+
323+
// wait for dialog to close
324+
await expect(pathInput).not.toBeVisible({ timeout: 10000 })
325+
}
326+
327+
test.beforeEach(async ({ page }) => {
328+
test.slow()
329+
330+
// import two files with distinct names
331+
await importByPath(page, orangeCid, orangeFile)
332+
await dismissImportNotification(page)
333+
await importByPath(page, appleCid, appleFile)
334+
await dismissImportNotification(page)
335+
336+
// verify both files are visible before each test
337+
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible({ timeout: 30000 })
338+
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible({ timeout: 30000 })
339+
})
340+
341+
test('search is hidden by default', async ({ page }) => {
342+
await expect(files.searchInput(page)).not.toBeVisible()
343+
344+
await files.searchToggle(page).click()
345+
await expect(files.searchInput(page)).toBeVisible()
346+
})
347+
348+
test('filter by name in list view', async ({ page }) => {
349+
await selectViewMode(page, 'list')
350+
await toggleSearchFilter(page, true)
351+
352+
await files.searchInput(page).fill('orange')
353+
354+
// only the orange file should be visible
355+
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
356+
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
357+
358+
// filtered count should show "1 /"
359+
await expect(page.getByText(/^1\s*\/\s*\d+$/)).toBeVisible()
360+
})
361+
362+
test('filter by CID in list view', async ({ page }) => {
363+
await selectViewMode(page, 'list')
364+
await toggleSearchFilter(page, true)
365+
366+
// get the CID from the orange file row
367+
const orangeRow = page.getByTestId('file-row').filter({ hasText: orangeFile })
368+
const cidText = await orangeRow.locator('div.monospace').textContent()
369+
// use last 10 chars of the CID to avoid shared prefix between inline CIDs
370+
const trimmed = cidText.trim()
371+
const cidFragment = trimmed.slice(-10)
372+
373+
await files.searchInput(page).fill(cidFragment)
374+
375+
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
376+
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
377+
})
378+
379+
test('filter by name in grid view', async ({ page }) => {
380+
await selectViewMode(page, 'grid')
381+
await toggleSearchFilter(page, true)
382+
383+
await files.searchInput(page).fill('apple')
384+
385+
// only the apple file should be visible
386+
await expect(files.gridFileByName(page, appleFile)).toBeVisible()
387+
await expect(files.gridFileByName(page, orangeFile)).not.toBeVisible()
388+
})
389+
390+
test('hiding search clears filter', async ({ page }) => {
391+
await selectViewMode(page, 'list')
392+
await toggleSearchFilter(page, true)
393+
394+
await files.searchInput(page).fill('orange')
395+
// only orange visible
396+
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).not.toBeVisible()
397+
398+
// hide the search filter
399+
await toggleSearchFilter(page, false)
400+
401+
await expect(files.searchInput(page)).not.toBeVisible()
402+
// both files should be visible again
403+
await expect(page.getByTestId('file-row').filter({ hasText: orangeFile })).toBeVisible()
404+
await expect(page.getByTestId('file-row').filter({ hasText: appleFile })).toBeVisible()
405+
})
406+
407+
test('no matches message', async ({ page }) => {
408+
await toggleSearchFilter(page, true)
409+
410+
await files.searchInput(page).fill('nonexistent-file-xyz')
411+
412+
await expect(page.getByText('No files match your search')).toBeVisible()
413+
})
414+
})
300415
})

test/e2e/setup/locators.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export const files = {
4040
contextMenuButton: (page, name) => page.locator(`button[aria-label="View more options for ${name}"]`),
4141
contextMenuItem: (page, text) => page.locator('button[role="menuitem"]').filter({ hasText: text }),
4242

43+
// Search filter
44+
searchToggle: (page) => page.getByRole('button', { name: 'Click to show search filter' }),
45+
searchInput: (page) => page.locator('input[aria-label="Filter by name or CID…"]'),
46+
searchClearButton: (page) => page.getByRole('button', { name: 'Clear search' }),
47+
4348
// Dialogs
4449
dialog: (page) => page.locator('div[role="dialog"]'),
4550
dialogInput: (page, name) => page.locator(`div[role="dialog"] input[name="${name}"]`),

test/helpers/grid.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const navigateToFilesPage = async (page) => {
4444
try {
4545
localStorage.removeItem('files.viewMode')
4646
localStorage.removeItem('files.sorting')
47+
localStorage.removeItem('files.showSearch')
4748
} catch (e) {
4849
// Ignore if localStorage is not available
4950
}
@@ -95,8 +96,27 @@ const addTestFiles = async (page, directoryName, numFiles = 5) => {
9596
await page.waitForSelector('[title*="test-file-"]')
9697
}
9798

99+
/**
100+
* Toggle search filter visibility
101+
* @param {import('@playwright/test').Page} page - The Playwright page object
102+
* @param {boolean} show - Whether to show (true) or hide (false) the search filter
103+
*/
104+
const toggleSearchFilter = async (page, show) => {
105+
const searchInput = page.locator('input[aria-label="Filter by name or CID…"]')
106+
const isVisible = await searchInput.isVisible().catch(() => false)
107+
108+
if (show && !isVisible) {
109+
await page.getByRole('button', { name: 'Click to show search filter' }).click()
110+
await searchInput.waitFor({ state: 'visible', timeout: 5000 })
111+
} else if (!show && isVisible) {
112+
await page.getByRole('button', { name: 'Click to show search filter' }).click()
113+
await searchInput.waitFor({ state: 'hidden', timeout: 5000 })
114+
}
115+
}
116+
98117
export {
99118
navigateToFilesPage,
100119
addTestFiles,
101-
selectViewMode
120+
selectViewMode,
121+
toggleSearchFilter
102122
}

0 commit comments

Comments
 (0)