import { expect, it, vi } from 'vitest' import { createFilePickerAdapter, encodePlatformPickerPath, type FilePickerTransport, } from '../../composables/createFilePickerAdapter' import type { FileDirectoryListing } from '../../types/fileBrowserTypes' const identity = (path = '') => encodePlatformPickerPath('raw', path) function setup() { const transport: FilePickerTransport = { listMounts: vi.fn(async () => [{ id: 'raw', label: 'Raw files' }]), browse: vi.fn(async ({ mount_id, path }) => ({ mount_id, path, entries: path ? [{ kind: 'file' as const, name: 'run.mzML.gz', path: `${path}/run.mzML.gz`, size_bytes: 7 }] : [{ kind: 'directory' as const, name: 'study', path: 'study' }], })), } return { transport, adapter: createFilePickerAdapter(transport, { rootLocation: { mount_id: 'raw', path: '' } }) } } it('shows a configured root directly, preserving nested ancestry and file formats', async () => { const { adapter, transport } = setup() const roots = await adapter.listRoot() expect(transport.listMounts).not.toHaveBeenCalled() expect(roots).toMatchObject([{ path: identity('study'), parentPath: null, parentName: 'raw' }]) expect(await adapter.listChildren(roots[0]!.path)).toMatchObject([ { path: identity('study/run.mzML.gz'), parentPath: identity('study'), format: 'mzML.gz', bytes: 7 }, ]) }) it('reuses directory listings across navigation and repeated searches within one instance', async () => { const { adapter, transport } = setup() await adapter.listRoot() await adapter.listChildren(identity('study')) expect(await adapter.search('run', { path: '', recursive: true })).toHaveLength(1) expect(await adapter.search('run', { path: identity('study'), recursive: false })).toHaveLength(1) expect(transport.browse).toHaveBeenCalledTimes(2) await createFilePickerAdapter(transport).listChildren(identity('study')) expect(transport.browse).toHaveBeenCalledTimes(3) }) it('refreshes root and descendants, forwarding refresh and cancellation to the transport', async () => { const { adapter, transport } = setup() await adapter.search('run', { path: '', recursive: true }) const signal = new AbortController().signal await adapter.listRoot({ refresh: true, signal }) expect(transport.browse).toHaveBeenLastCalledWith({ mount_id: 'raw', path: '' }, { refresh: true, signal }) await adapter.listChildren(identity('study')) expect(transport.browse).toHaveBeenCalledTimes(4) await adapter.search('run', { path: '', recursive: true }, { refresh: true, signal }) expect(transport.browse).toHaveBeenCalledTimes(6) expect(vi.mocked(transport.browse).mock.calls.at(-1)![1]).toEqual({ signal }) }) it('does not reuse an old pending request during refresh or cache its late response', async () => { const { adapter, transport } = setup() let finish!: (value: FileDirectoryListing) => void vi.mocked(transport.browse).mockReturnValueOnce(new Promise((resolve) => { finish = resolve })) const old = adapter.listChildren(identity('study')) const rejected = expect(old).rejects.toMatchObject({ name: 'AbortError' }) const current = await adapter.listChildren(identity('study'), { refresh: true }) finish({ mount_id: 'raw', path: 'study', entries: [] }) await rejected expect(await adapter.listChildren(identity('study'))).toEqual(current) expect(transport.browse).toHaveBeenCalledTimes(2) }) it('rejects late aborted responses even when the transport ignores the signal', async () => { const { adapter, transport } = setup() let finish!: (value: FileDirectoryListing) => void vi.mocked(transport.browse).mockReturnValueOnce(new Promise((resolve) => { finish = resolve })) const controller = new AbortController() const old = adapter.listRoot({ signal: controller.signal }) const rejected = expect(old).rejects.toMatchObject({ name: 'AbortError' }) controller.abort() finish({ mount_id: 'raw', path: '', entries: [] }) await rejected expect(await adapter.listRoot()).toHaveLength(1) expect(transport.browse).toHaveBeenCalledTimes(2) }) it('rejects already cancelled requests without serving cached listings', async () => { const { adapter, transport } = setup() await adapter.listRoot() const signal = AbortSignal.abort() await expect(adapter.listRoot({ signal })).rejects.toMatchObject({ name: 'AbortError' }) await expect(adapter.listChildren(identity(), { signal })).rejects.toMatchObject({ name: 'AbortError' }) expect(transport.browse).toHaveBeenCalledTimes(1) }) it('confines navigation, search and returned nodes to the configured root', async () => { const { transport } = setup() const adapter = createFilePickerAdapter(transport, { rootLocation: { mount_id: 'raw', path: 'study' } }) for (const path of [identity(), identity('study-other'), identity('study/../outside'), encodePlatformPickerPath('other', 'study')]) { await expect(adapter.listChildren(path)).rejects.toThrow('location') await expect(adapter.search('run', { path, recursive: true })).rejects.toThrow('location') } expect(transport.browse).not.toHaveBeenCalled() expect(await adapter.listRoot()).toMatchObject([{ parentPath: null }]) vi.mocked(transport.browse).mockResolvedValue({ mount_id: 'raw', path: 'study', entries: [ { kind: 'directory', name: 'outside', path: 'outside' }, ] }) await expect(adapter.listRoot({ refresh: true })).rejects.toThrow('entry') }) it('bounds recursive traversal and rejects truncated listings', async () => { const { adapter, transport } = setup() vi.mocked(transport.browse).mockImplementation(async ({ mount_id, path }) => ({ mount_id, path, entries: [{ kind: 'directory', name: 'next', path: path ? `${path}/next` : 'next' }], })) expect(await adapter.search('missing', { path: '', recursive: true })).toEqual({ nodes: [], truncated: true }) expect(transport.browse).toHaveBeenCalledTimes(500) vi.mocked(transport.browse).mockResolvedValue({ mount_id: 'raw', path: '', entries: [], truncated: true }) await expect(adapter.listRoot({ refresh: true })).rejects.toThrow('listing limit') }) it('invalidating a session prevents old responses from repopulating its cache', async () => { const { adapter, transport } = setup() let finish!: (value: FileDirectoryListing) => void vi.mocked(transport.browse).mockReturnValueOnce(new Promise((resolve) => { finish = resolve })) const old = adapter.listRoot() const rejected = expect(old).rejects.toMatchObject({ name: 'AbortError' }) adapter.invalidate!() const current = await adapter.listRoot() finish({ mount_id: 'raw', path: '', entries: [] }) await rejected expect(await adapter.listRoot()).toEqual(current) expect(transport.browse).toHaveBeenCalledTimes(2) }) it('keeps folders expandable when filtered child counts are zero', async () => { const { adapter, transport } = setup() vi.mocked(transport.browse).mockResolvedValueOnce({ mount_id: 'raw', path: '', entries: [{ kind: 'directory', name: 'study', path: 'study', child_count: 0 }], }) const [folder] = await adapter.listRoot() expect(folder?.kind).toBe('folder') expect(folder).not.toHaveProperty('hasChildren', false) }) it('should mark the adapter as mount-rooted when no root location is configured', () => { expect(createFilePickerAdapter(setup().transport).mounts).toBe(true) }) it('should not mark the adapter as mount-rooted when a root location is configured', () => { expect(setup().adapter.mounts).toBe(false) }) it('should carry the entry modification time onto the picker node', async () => { const { adapter, transport } = setup() vi.mocked(transport.browse).mockResolvedValueOnce({ mount_id: 'raw', path: '', entries: [{ kind: 'file', name: 'a.raw', path: 'a.raw', modified_at: '2026-09-24T14:32:00Z' }], }) expect((await adapter.listRoot())[0]!.modifiedAt).toBe('2026-09-24T14:32:00Z') }) it('names the mount holding a path and nothing for a path it cannot decode', () => { const adapter = createFilePickerAdapter(setup().transport) expect([adapter.mountOf!(identity('study/run')), adapter.mountOf!('not-json')]).toEqual([identity(), undefined]) }) function treeSetup() { const study = { kind: 'directory' as const, name: 'study', path: 'study' } const run = { kind: 'file' as const, name: 'run.raw', path: 'study/run.raw' } const transport: FilePickerTransport = { listMounts: vi.fn(async () => [{ id: 'raw', label: 'Raw files' }]), browse: vi.fn(async ({ mount_id, path }) => ({ mount_id, path, entries: [] })), tree: vi.fn(async ({ mount_id }) => ({ listings: [ { mount_id, path: '', entries: [study] }, { mount_id, path: 'study', entries: [run] }, ], truncated: false, })), search: vi.fn(async ({ path }) => ({ entries: [study, run].filter((entry) => !path || entry.path.startsWith(`${path}/`)), searched_directories: 2, truncated: false, })), } return { transport, adapter: createFilePickerAdapter(transport, { rootLocation: { mount_id: 'raw', path: '' } }) } } it('should serve a subfolder from the tree response when the transport has tree', async () => { const { adapter, transport } = treeSetup() await adapter.listRoot() await adapter.listChildren(identity('study')) expect(transport.tree).toHaveBeenCalledTimes(1) }) it('should not call browse when the transport has tree', async () => { const { adapter, transport } = treeSetup() await adapter.listRoot() expect(transport.browse).not.toHaveBeenCalled() }) it('should map tree listings to their own parent folders', async () => { const { adapter } = treeSetup() await adapter.listRoot() expect(await adapter.listChildren(identity('study'))).toMatchObject([ { path: identity('study/run.raw'), parentPath: identity('study'), kind: 'file' }, ]) }) it('should search on the server when the transport has search', async () => { const { adapter, transport } = treeSetup() await adapter.search('ru', { path: identity('study'), recursive: true }) expect(transport.search).toHaveBeenCalledWith({ mount_id: 'raw', path: 'study' }, 'ru', true, undefined) }) it('should give server search results their parent folder', async () => { const { adapter } = treeSetup() expect(await adapter.search('ru', { path: '', recursive: true })).toMatchObject([ { path: identity('study'), parentPath: null }, { path: identity('study/run.raw'), parentPath: identity('study') }, ]) }) it('should return partial server results marked truncated when the server stops early', async () => { const { adapter, transport } = treeSetup() vi.mocked(transport.search!).mockResolvedValueOnce({ entries: [{ kind: 'file', name: 'run.raw', path: 'study/run.raw' }], searched_directories: 5000, truncated: true, }) expect(await adapter.search('ru', { path: '', recursive: true })).toMatchObject({ nodes: [{ path: identity('study/run.raw') }], truncated: true, }) }) it('should reject a search entry whose name is only a suffix of its last path segment', async () => { const { adapter, transport } = treeSetup() vi.mocked(transport.search!).mockResolvedValueOnce({ entries: [{ kind: 'file', name: 'b', path: 'study/ab' }], searched_directories: 1, truncated: false, }) await expect(adapter.search('b', { path: '', recursive: true })).rejects.toThrow(/Invalid/) }) it('should reject a server search entry outside the searched folder', async () => { const { adapter, transport } = treeSetup() vi.mocked(transport.search!).mockResolvedValueOnce({ entries: [{ kind: 'file', name: 'x.raw', path: 'other/x.raw' }], searched_directories: 1, truncated: false, }) await expect(adapter.search('x', { path: identity('study'), recursive: true })).rejects.toThrow(/Invalid/) }) it('should reject a nested entry from a search limited to this folder', async () => { const { adapter, transport } = treeSetup() vi.mocked(transport.search!).mockResolvedValueOnce({ entries: [{ kind: 'file', name: 'x.raw', path: 'study/private/x.raw' }], searched_directories: 1, truncated: false, }) await expect(adapter.search('x', { path: identity('study'), recursive: false })).rejects.toThrow(/Invalid/) })