import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, expect, it, vi } from 'vitest' import { ref } from 'vue' import FilePicker from '../../components/FilePicker.vue' import FileUploader from '../../components/FileUploader.vue' import { createFilePickerAdapter, encodePlatformPickerPath } from '../../composables/createFilePickerAdapter' import type { PickerAdapter, PickerNode, PickerSelection } from '../../types/filePicker' enableAutoUnmount(afterEach) const folder: PickerNode = { kind: 'folder', path: 'opaque-root', name: 'Root' } const child: PickerNode = { kind: 'folder', path: 'unrelated-child', name: 'Child', parentPath: folder.path, } const file: PickerNode = { kind: 'file', path: 'independent-file', name: 'run.raw', format: 'raw', bytes: 7, parentPath: child.path, } function adapter(): PickerAdapter { return { listRoot: vi.fn(async () => [folder]), listChildren: vi.fn(async (path) => (path === folder.path ? [child] : [file])), search: vi.fn(async () => []), } } async function setup(data = adapter(), extra: Record = {}) { const wrapper = mount(FilePicker, { props: { open: true, adapter: data, view: 'tree', ...extra }, global: { stubs: { teleport: true } }, }) await flushPromises() return wrapper } function row(wrapper: Awaited>, path: string) { return wrapper.findAll('[role="treeitem"]').find((item) => item.attributes('data-path') === path)! } function confirm(wrapper: Awaited>) { return wrapper.findAll('.mint-picker__actions button').at(-1)! } it('re-reads the current directory when reopening a cached SDK adapter', async () => { let name = 'old.raw' const browse = vi.fn(async () => ({ mount_id: 'raw', path: '', entries: [{ kind: 'file' as const, path: name, name, size_bytes: 7 }], })) const data = createFilePickerAdapter({ listMounts: async () => [], browse }) const wrapper = await setup(data, { initialPath: encodePlatformPickerPath('raw') }) expect(wrapper.text()).toContain('old.raw') await wrapper.setProps({ open: false }) name = 'new.raw' await wrapper.setProps({ open: true }) await flushPromises() expect(browse).toHaveBeenCalledTimes(2) expect(wrapper.text()).toContain('new.raw') expect(wrapper.text()).not.toContain('old.raw') }) it('root breadcrumb returns directly to mounts and navigation keeps a tabbable row', async () => { const wrapper = await setup() await row(wrapper, folder.path).trigger('click') await row(wrapper, folder.path).trigger('dblclick') await flushPromises() expect(row(wrapper, child.path).attributes('tabindex')).toBe('0') await row(wrapper, child.path).trigger('dblclick') await flushPromises() await wrapper.get('[aria-label="Breadcrumb"] button').trigger('click') await flushPromises() expect(row(wrapper, folder.path)).toBeDefined() }) it('restored nested files inherit their folder and disappear when that folder is deselected', async () => { const wrapper = await setup() await row(wrapper, folder.path).trigger('click') await flushPromises() await confirm(wrapper).trigger('click') const result = wrapper.emitted('select')![0]![0] as PickerSelection await wrapper.setProps({ open: false, initialSelection: result }) await wrapper.setProps({ open: true }) await flushPromises() await row(wrapper, folder.path).trigger('click', { ctrlKey: true }) await flushPromises() expect(confirm(wrapper).attributes('disabled')).toBeDefined() }) it('refresh removes deleted descendants from a selected folder', async () => { const data = adapter() const wrapper = await setup(data) await row(wrapper, folder.path).trigger('click') await flushPromises() expect(confirm(wrapper).attributes('disabled')).toBeUndefined() vi.mocked(data.listChildren).mockImplementation(async (path) => path === folder.path ? [child] : [], ) await wrapper.get('[aria-label="Refresh location"]').trigger('click') await flushPromises() expect(confirm(wrapper).attributes('disabled')).toBeDefined() expect(wrapper.get('.mint-picker__receipt').text()).not.toContain('7 B') }) it('marks only an explicit refresh so adapters can bypass their caches', async () => { const data = adapter() const wrapper = await setup(data) await row(wrapper, folder.path).trigger('dblclick') await flushPromises() expect(vi.mocked(data.listRoot).mock.calls[0]![0]).not.toHaveProperty('refresh') expect(vi.mocked(data.listChildren).mock.calls[0]![1]).not.toHaveProperty('refresh') await wrapper.get('[aria-label="Refresh location"]').trigger('click') await flushPromises() expect(vi.mocked(data.listChildren).mock.calls.at(-1)![1]).toMatchObject({ refresh: true }) await wrapper.get('[aria-label="Breadcrumb"] button').trigger('click') await flushPromises() expect(vi.mocked(data.listRoot).mock.calls.at(-1)![0]).not.toHaveProperty('refresh') await wrapper.get('[aria-label="Refresh location"]').trigger('click') await flushPromises() expect(vi.mocked(data.listRoot).mock.calls.at(-1)![0]).toMatchObject({ refresh: true }) }) it('late server responses cannot overwrite browser source metadata', async () => { const data = adapter() let release!: (nodes: PickerNode[]) => void vi.mocked(data.listRoot).mockReturnValueOnce( new Promise((resolve) => { release = resolve }), ) const wrapper = await setup(data, { sources: ['server', 'localFile'] }) await wrapper.findAll('[aria-label="Data source"] button').at(-1)!.trigger('click') await flushPromises() release([folder]) await flushPromises() expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(0) expect(wrapper.text()).toContain('No selection') }) it('browser source confirms original File objects without reading or uploading them', async () => { const wrapper = await setup(adapter(), { sources: ['localFile'], selectionMode: 'multi-file' }) const local = new File(['content'], 'run.raw') const read = vi.spyOn(local, 'arrayBuffer') const input = wrapper.get('input[type="file"]') Object.defineProperty(input.element, 'files', { value: [local], configurable: true }) await input.trigger('change') await flushPromises() await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')?.[0]?.[0]).toEqual({ source: 'localFile', files: [local] }) expect(read).not.toHaveBeenCalled() }) it('restores dropped folder paths after remounting with the confirmed local selection', async () => { const options = { sources: ['localFile'], selectionMode: 'folder' } const wrapper = await setup(adapter(), options) const file = new File(['data'], 'run.raw') wrapper.getComponent(FileUploader).vm.$emit('upload', [file], 'batch', ['batch/run.raw']) await flushPromises() await confirm(wrapper).trigger('click') const result = wrapper.emitted('select')![0]![0] as PickerSelection expect(result).toEqual({ source: 'localFile', files: [file], relativePaths: ['batch/run.raw'] }) expect(result.files[0]).toBe(file) wrapper.unmount() const restored = await setup(adapter(), { ...options, initialSelection: result }) expect(confirm(restored).attributes('disabled')).toBeUndefined() await confirm(restored).trigger('click') const confirmed = restored.emitted('select')![0]![0] as PickerSelection expect(confirmed).toEqual(result) expect(confirmed.files[0]).toBe(file) }) it('restores a nested selected folder display name without exposing its opaque identity', async () => { const wrapper = await setup() await row(wrapper, folder.path).trigger('dblclick') await flushPromises() await row(wrapper, child.path).trigger('click') await flushPromises() await wrapper.get('[aria-label="Breadcrumb"] button').trigger('click') await flushPromises() await confirm(wrapper).trigger('click') await wrapper.setProps({ open: false }) await wrapper.setProps({ open: true }) await flushPromises() expect(wrapper.get('.mint-picker__preview-name').text()).toBe('Child') }) it('blocks confirmation while refresh is replacing stale folder metadata', async () => { const data = adapter() const wrapper = await setup(data) await row(wrapper, folder.path).trigger('click') await flushPromises() let release!: (nodes: PickerNode[]) => void vi.mocked(data.listRoot).mockReturnValueOnce( new Promise((resolve) => { release = resolve }), ) await wrapper.get('[aria-label="Refresh location"]').trigger('click') expect(confirm(wrapper).attributes('disabled')).toBeDefined() await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')).toBeUndefined() release([folder]) await flushPromises() expect(confirm(wrapper).attributes('disabled')).toBeUndefined() }) it('does not confirm a folder after the host switches the same picker to single-file mode', async () => { const wrapper = await setup() await row(wrapper, folder.path).trigger('click') await flushPromises() await wrapper.setProps({ selectionMode: 'single-file' }) await flushPromises() expect(confirm(wrapper).attributes('disabled')).toBeDefined() await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')).toBeUndefined() }) it('system filter hides denied folders and skips their contents during folder selection', async () => { const allowed: PickerNode = { ...file, path: 'allowed', name: 'allowed.raw', parentPath: folder.path } const data = adapter() vi.mocked(data.listChildren).mockImplementation(async (path) => path === folder.path ? [child, allowed] : [file], ) const filter = vi.fn((node: PickerNode) => node.path !== child.path) const wrapper = await setup(data, { systemFilter: filter }) await row(wrapper, folder.path).trigger('click') await flushPromises() expect(data.listChildren).not.toHaveBeenCalledWith(child.path, expect.anything()) await row(wrapper, folder.path).trigger('dblclick') await flushPromises() expect(row(wrapper, child.path)).toBeUndefined() await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ files: [allowed], excluded: [] }) expect(filter).toHaveBeenCalledWith(expect.objectContaining({ path: child.path }), 'server') }) it('system filter applies to search results and restored or already selected files', async () => { const data = adapter() vi.mocked(data.listRoot).mockResolvedValue([file]) vi.mocked(data.search).mockResolvedValue([file]) const wrapper = await setup(data, { selectionMode: 'multi-file' }) await row(wrapper, file.path).trigger('click') await flushPromises() const deny = (node: PickerNode) => node.path !== file.path await wrapper.setProps({ systemFilter: deny }) expect(row(wrapper, file.path)).toBeUndefined() expect(confirm(wrapper).attributes('disabled')).toBeDefined() await wrapper.get('input[aria-label="Search folders and files"]').setValue('run') await new Promise((resolve) => setTimeout(resolve, 250)) await flushPromises() expect(data.search).toHaveBeenCalled() expect(row(wrapper, file.path)).toBeUndefined() await wrapper.setProps({ open: false, initialSelection: { source: 'server', folders: [], files: [file], excluded: [], } }) await wrapper.setProps({ open: true }) await flushPromises() expect(confirm(wrapper).attributes('disabled')).toBeDefined() await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')).toBeUndefined() }) it('system filter receives local source and prevents denied browser files from being confirmed', async () => { const filter = vi.fn((node: PickerNode) => node.name !== 'private.raw') const wrapper = await setup(adapter(), { sources: ['localFile'], selectionMode: 'multi-file', systemFilter: filter, }) const allowed = new File(['public'], 'public.raw') const denied = new File(['private'], 'private.raw') const input = wrapper.get('input[type="file"]') Object.defineProperty(input.element, 'files', { value: [allowed, denied], configurable: true }) await input.trigger('change') await flushPromises() expect(wrapper.get('.mint-picker__receipt').text()).toContain('1 file') expect(wrapper.text()).not.toContain('private.raw') await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')?.[0]?.[0]).toEqual({ source: 'localFile', files: [allowed] }) expect(filter).toHaveBeenCalledWith(expect.objectContaining({ name: denied.name }), 'localFile') }) it('reactive permission changes restore visible rows and resolve newly allowed selected subfolders', async () => { const allowed = ref(false) const data = adapter() const wrapper = await setup(data, { systemFilter: (node: PickerNode) => node.path !== child.path || allowed.value, }) await row(wrapper, folder.path).trigger('click') await flushPromises() await row(wrapper, folder.path).trigger('dblclick') await flushPromises() expect(row(wrapper, child.path)).toBeUndefined() expect(confirm(wrapper).attributes('disabled')).toBeDefined() allowed.value = true await flushPromises() expect(row(wrapper, child.path)).toBeDefined() expect(confirm(wrapper).attributes('disabled')).toBeUndefined() await confirm(wrapper).trigger('click') expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ files: [file] }) allowed.value = false await flushPromises() expect(row(wrapper, child.path)).toBeUndefined() expect(confirm(wrapper).attributes('disabled')).toBeDefined() })