import { mount, flushPromises } from '@vue/test-utils' import { describe, it, expect, vi, beforeEach } from 'vitest' const mockGet = vi.fn() const mockPost = vi.fn() const mockPatch = vi.fn() vi.mock('../../composables/useApi', () => ({ useApi: () => ({ get: mockGet, post: mockPost, patch: mockPatch }), })) import ArtifactSelectorModal from '../../components/ArtifactSelectorModal.vue' import type { AnalysisArtifactSummary } from '../../types/analysisArtifactTypes' function makeArtifact( overrides: Partial = {}, ): AnalysisArtifactSummary { return { id: 1, experiment_id: 42, plugin_id: 'plugin-a', artifact_key: 'fit', display_name: 'Dose Response Fit', status: 'active', result_keys: ['summary'], created_at: '2026-07-01T10:00:00Z', updated_at: '2026-07-02T10:00:00Z', ...overrides, } } const ARTIFACTS = [ makeArtifact({ id: 1 }), makeArtifact({ id: 2, plugin_id: 'plugin-b', artifact_key: 'peaks', display_name: 'Peak Table' }), ] async function mountModal(props: Record = {}) { const wrapper = mount(ArtifactSelectorModal, { props: { modelValue: true, experimentId: 42, ...props }, global: { stubs: { teleport: true } }, }) await flushPromises() return wrapper } describe('ArtifactSelectorModal', () => { beforeEach(() => { vi.clearAllMocks() // Fresh rows per call: the list is stored by reference and spliced on mutation. mockGet.mockImplementation(async () => ({ artifacts: ARTIFACTS.map((a) => ({ ...a })) })) mockPost.mockResolvedValue(makeArtifact({ status: 'archived' })) delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ window.history.replaceState({}, '', '/') }) it('should emit the summary and close on select', async () => { const wrapper = await mountModal() await wrapper.get('.mint-resource-picker__row').trigger('click') await flushPromises() expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ id: 1 }) expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([false]) }) it('should emit selectDetail alongside select when fetchDetailOnSelect is set', async () => { const wrapper = await mountModal({ fetchDetailOnSelect: true }) mockGet.mockResolvedValueOnce({ ...makeArtifact(), result: { x: 1 }, tree: [], summary: null }) await wrapper.get('.mint-resource-picker__row').trigger('click') await flushPromises() expect(wrapper.emitted('selectDetail')).toHaveLength(1) expect(wrapper.emitted('select')).toHaveLength(1) }) // A failed detail fetch must not look like a successful pick. it('should stay open and not emit select when the detail fetch fails', async () => { const wrapper = await mountModal({ fetchDetailOnSelect: true }) mockGet.mockRejectedValueOnce(new Error('Detail unavailable')) await wrapper.get('.mint-resource-picker__row').trigger('click') await flushPromises() expect(wrapper.emitted('selectDetail')).toBeUndefined() expect(wrapper.emitted('select')).toBeUndefined() expect(wrapper.emitted('update:modelValue')).toBeUndefined() expect(wrapper.get('.mint-resource-picker__error').text()).toContain('Detail unavailable') }) it('should archive a row without selecting it', async () => { const wrapper = await mountModal() await wrapper.get('.mint-artifact-selector__action').trigger('click') await flushPromises() expect(mockPost).toHaveBeenCalledWith('/experiments/42/analysis-artifacts/1/archive') expect(wrapper.emitted('archive')).toHaveLength(1) expect(wrapper.emitted('select')).toBeUndefined() }) // The row we clicked still says "active"; listeners must get the backend's version. it('should emit the post-mutation summary on archive', async () => { const wrapper = await mountModal() await wrapper.get('.mint-artifact-selector__action').trigger('click') await flushPromises() expect(wrapper.emitted('archive')?.[0]?.[0]).toMatchObject({ id: 1, status: 'archived' }) }) it('should not emit archive when the mutation fails', async () => { mockPost.mockRejectedValueOnce(new Error('Conflict')) const wrapper = await mountModal() await wrapper.get('.mint-artifact-selector__action').trigger('click') await flushPromises() expect(wrapper.emitted('archive')).toBeUndefined() }) // The container's Enter handler must not hijack a focused row button. it('should leave Enter alone when it targets a row action button', async () => { const wrapper = await mountModal() await wrapper.get('.mint-artifact-selector').trigger('keydown', { key: 'ArrowDown' }) await wrapper.get('.mint-artifact-selector__action').trigger('keydown', { key: 'Enter' }) await flushPromises() expect(wrapper.emitted('select')).toBeUndefined() expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) it('should select the arrow-highlighted row on Enter', async () => { const wrapper = await mountModal() await wrapper.get('.mint-artifact-selector').trigger('keydown', { key: 'ArrowDown' }) await wrapper.get('.mint-artifact-selector').trigger('keydown', { key: 'Enter' }) await flushPromises() expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ id: 1 }) }) it('should hide row actions without the experiments.edit permission', async () => { ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = { user: { id: '1', username: 'viewer', role: 'viewer', permissions: ['experiments.view'] }, } const wrapper = await mountModal() expect(wrapper.find('.mint-artifact-selector__action').exists()).toBe(false) }) it('should show row actions with the experiments.edit permission', async () => { ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = { user: { id: '1', username: 'member', role: 'member', permissions: ['experiments.view', 'experiments.edit'], }, } const wrapper = await mountModal() expect(wrapper.find('.mint-artifact-selector__action').exists()).toBe(true) }) it('should say Archived on the second line of an archived row', async () => { mockGet.mockImplementation(async () => ({ artifacts: [makeArtifact({ status: 'archived' })] })) const wrapper = await mountModal({ includeArchived: true }) expect(wrapper.get('.mint-resource-picker__meta').text()).toContain('Archived') }) it('should label the action Restore on an archived row', async () => { mockGet.mockImplementation(async () => ({ artifacts: [makeArtifact({ status: 'archived' })] })) const wrapper = await mountModal({ includeArchived: true }) expect(wrapper.get('.mint-artifact-selector__action').text()).toBe('Restore') }) it('should scope rows to a reactive pluginId prop', async () => { const wrapper = await mountModal({ pluginId: 'plugin-a' }) expect(wrapper.findAll('.mint-resource-picker__row')).toHaveLength(1) await wrapper.setProps({ pluginId: 'plugin-b' }) await flushPromises() const rows = wrapper.findAll('.mint-resource-picker__row') expect(rows).toHaveLength(1) expect(rows[0].text()).toContain('Peak Table') }) })