import { describe, it, expect, vi, beforeEach } from 'vitest' // --------------------------------------------------------------------------- // Mock useApi — intercepts all HTTP calls // --------------------------------------------------------------------------- const mockGet = vi.fn() const mockPut = vi.fn() const mockDelete = vi.fn() const mockUseApi = vi.fn((_options?: unknown) => ({ get: mockGet, put: mockPut, delete: mockDelete, })) vi.mock('../../composables/useApi', () => ({ useApi: (options: unknown) => mockUseApi(options), })) import { useExperimentSave } from '../../composables/useExperimentSave' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function setup(options?: Parameters[0]) { return useExperimentSave(options) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('useExperimentSave', () => { beforeEach(() => { vi.clearAllMocks() mockGet.mockResolvedValue({}) mockPut.mockResolvedValue({}) mockDelete.mockResolvedValue({}) delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ window.history.replaceState({}, '', '/') }) // ------------------------------------------------------------------------- // saveDesign // ------------------------------------------------------------------------- describe('saveDesign', () => { it('should call PUT /experiments/{id}/data with correct body', async () => { const { saveDesign } = setup({ pluginId: 'my-plugin', schemaVersion: '2.0' }) await saveDesign(42, { layout: 'grid' }) expect(mockPut).toHaveBeenCalledOnce() expect(mockPut).toHaveBeenCalledWith('/experiments/42/data', { plugin_id: 'my-plugin', data: { layout: 'grid' }, schema_version: '2.0', }) }) it('should use default schema_version "1.0" when not provided', async () => { const { saveDesign } = setup({ pluginId: 'p1' }) await saveDesign(1, { x: 1 }) const body = mockPut.mock.calls[0][1] expect(body.schema_version).toBe('1.0') }) it('should return true on success', async () => { const { saveDesign } = setup({ pluginId: 'p1' }) const ok = await saveDesign(1, { x: 1 }) expect(ok).toBe(true) }) it('should return false on error', async () => { mockPut.mockRejectedValueOnce(new Error('Network error')) const { saveDesign } = setup({ pluginId: 'p1' }) const ok = await saveDesign(1, { x: 1 }) expect(ok).toBe(false) }) it('should set isSaving during the call', async () => { let resolvePut!: (v: unknown) => void mockPut.mockImplementationOnce(() => new Promise((r) => { resolvePut = r })) const { saveDesign, isSaving } = setup({ pluginId: 'p1' }) expect(isSaving.value).toBe(false) const promise = saveDesign(1, { x: 1 }) expect(isSaving.value).toBe(true) resolvePut({}) await promise expect(isSaving.value).toBe(false) }) it('should set error on failure', async () => { mockPut.mockRejectedValueOnce(new Error('Bad request')) const { saveDesign, error } = setup({ pluginId: 'p1' }) await saveDesign(1, { x: 1 }) expect(error.value).toBe('Bad request') }) it('should clear previous error on success', async () => { mockPut.mockRejectedValueOnce(new Error('First fail')) const { saveDesign, error } = setup({ pluginId: 'p1' }) await saveDesign(1, { x: 1 }) expect(error.value).toBe('First fail') mockPut.mockResolvedValueOnce({}) await saveDesign(1, { x: 1 }) expect(error.value).toBeNull() }) it('should update lastSavedAt on success', async () => { const { saveDesign, lastSavedAt } = setup({ pluginId: 'p1' }) expect(lastSavedAt.value).toBeNull() await saveDesign(1, { x: 1 }) expect(lastSavedAt.value).toBeInstanceOf(Date) }) it('should not update lastSavedAt on failure', async () => { mockPut.mockRejectedValueOnce(new Error('fail')) const { saveDesign, lastSavedAt } = setup({ pluginId: 'p1' }) await saveDesign(1, { x: 1 }) expect(lastSavedAt.value).toBeNull() }) it('should require pluginId', async () => { const { saveDesign } = setup() const ok = await saveDesign(1, { x: 1 }) expect(ok).toBe(false) }) it('should default pluginId and apiBaseUrl from injected platform context', async () => { ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = { isIntegrated: true, theme: 'light', platformApiUrl: '/api', plugin: { id: 'platform-plugin', name: 'Platform Plugin', version: '1.0.0', route_prefix: '/platform-plugin', api_prefix: '/api/platform-plugin', }, } const { saveDesign } = setup() await saveDesign(1, { x: 1 }) expect(mockUseApi).toHaveBeenCalledWith({ baseUrl: '/api' }) expect(mockPut).toHaveBeenCalledWith('/experiments/1/data', { plugin_id: 'platform-plugin', data: { x: 1 }, schema_version: '1.0', }) }) }) // ------------------------------------------------------------------------- // saveTemplatePreset // ------------------------------------------------------------------------- describe('saveTemplatePreset', () => { it('should create a preset collection and save it as design data', async () => { const { saveTemplatePreset } = setup({ pluginId: 'p1' }) const ok = await saveTemplatePreset(42, 'wellplate', { samples: ['Control', 'Treatment'], compounds: { 'Drug A': [10, 1] }, }) expect(ok).toBe(true) expect(mockPut).toHaveBeenCalledOnce() const body = mockPut.mock.calls[0][1] expect(mockPut.mock.calls[0][0]).toBe('/experiments/42/data') expect(body.plugin_id).toBe('p1') expect(body.data.metadata.preset).toBe('wellplate-screen') expect(Object.keys(body.data.templates)).toEqual(['plate-map', 'sample-sheet', 'dose-response']) expect(body.data.templates['plate-map'].data.samples[1].id).toBe('treatment') }) it('should resolve preset aliases', async () => { const { saveTemplatePreset } = setup({ pluginId: 'p1' }) await saveTemplatePreset(42, 'gene expression', { samples: ['Control'], targets: ['ACTB'], }) const body = mockPut.mock.calls[0][1] expect(body.data.metadata.preset).toBe('qpcr-expression') expect(Object.keys(body.data.templates)).toEqual(['sample-sheet', 'qpcr-plate']) }) it('should return false when pluginId is missing', async () => { const { saveTemplatePreset, error } = setup() const ok = await saveTemplatePreset(42, 'lcms', { samples: ['S001'] }) expect(ok).toBe(false) expect(error.value).toBe('pluginId is required for saveDesign') expect(mockPut).not.toHaveBeenCalled() }) }) // ------------------------------------------------------------------------- // current experiment shortcuts // ------------------------------------------------------------------------- describe('current experiment shortcuts', () => { it('should expose current experiment id from platform injection', async () => { ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = { isIntegrated: true, theme: 'light', plugin: { id: 'p1', name: 'Plugin', version: '1.0.0', route_prefix: '/plugin', api_prefix: '/api/plugin', }, currentExperiment: { id: 77 }, } const { currentExperimentId, hasCurrentExperiment, requireCurrentExperimentId } = setup() expect(currentExperimentId.value).toBe(77) expect(hasCurrentExperiment.value).toBe(true) expect(requireCurrentExperimentId()).toBe(77) }) it('should expose current experiment id from URL query and path', () => { window.history.replaceState({}, '', '/analysis?experimentId=42') expect(setup().currentExperimentId.value).toBe(42) window.history.replaceState({}, '', '/experiments/43/analysis') expect(setup().currentExperimentId.value).toBe(43) }) it('should save design for the current experiment', async () => { window.history.replaceState({}, '', '/analysis?experimentId=42') const { saveCurrentDesign } = setup({ pluginId: 'p1' }) const ok = await saveCurrentDesign({ layout: 'grid' }) expect(ok).toBe(true) expect(mockPut).toHaveBeenCalledWith('/experiments/42/data', { plugin_id: 'p1', data: { layout: 'grid' }, schema_version: '1.0', }) }) it('should save a template preset for the current experiment', async () => { window.history.replaceState({}, '', '/experiments/42/template') const { saveCurrentTemplatePreset } = setup({ pluginId: 'p1' }) const ok = await saveCurrentTemplatePreset('wellplate', { samples: ['Control', 'Treatment'], compounds: { 'Drug A': [10, 1] }, }) expect(ok).toBe(true) expect(mockPut.mock.calls[0][0]).toBe('/experiments/42/data') expect(mockPut.mock.calls[0][1].data.metadata.preset).toBe('wellplate-screen') }) it('should save analysis for the current experiment', async () => { window.history.replaceState({}, '', '/analysis?experiment_id=44') const { saveCurrentAnalysis } = setup({ pluginId: 'p1' }) const ok = await saveCurrentAnalysis({ score: 0.95 }) expect(ok).toBe(true) expect(mockPut).toHaveBeenCalledWith('/experiments/44/results/p1', { result: { score: 0.95 }, }) }) it('should load and delete current experiment data', async () => { window.history.replaceState({}, '', '/experiments/45/template') mockGet.mockResolvedValueOnce({ design: { plate: 'A' } }) const { loadCurrentDesign, deleteCurrentAnalysis, } = setup({ pluginId: 'p1' }) await expect(loadCurrentDesign()).resolves.toEqual({ design: { plate: 'A' } }) await expect(deleteCurrentAnalysis()).resolves.toBe(true) expect(mockGet).toHaveBeenCalledWith('/experiments/45/data') expect(mockDelete).toHaveBeenCalledWith('/experiments/45/results/p1') }) it('should return false when current experiment is missing', async () => { const { saveCurrentTemplatePreset, error, requireCurrentExperimentId } = setup({ pluginId: 'p1' }) expect(() => requireCurrentExperimentId()).toThrow('[MINT SDK] No current experiment is selected.') const ok = await saveCurrentTemplatePreset('lcms', { samples: ['S001'] }) expect(ok).toBe(false) expect(error.value).toBe('No current experiment is selected for saveTemplatePreset') expect(mockPut).not.toHaveBeenCalled() }) }) // ------------------------------------------------------------------------- // saveAnalysis // ------------------------------------------------------------------------- describe('saveAnalysis', () => { it('should call PUT /experiments/{id}/results/{pluginId}', async () => { const { saveAnalysis } = setup({ pluginId: 'my-plugin' }) await saveAnalysis(42, { score: 0.95 }) expect(mockPut).toHaveBeenCalledOnce() expect(mockPut).toHaveBeenCalledWith( '/experiments/42/results/my-plugin', { result: { score: 0.95 } }, ) }) it('should return true on success', async () => { const { saveAnalysis } = setup({ pluginId: 'p1' }) const ok = await saveAnalysis(1, { score: 1 }) expect(ok).toBe(true) }) it('should return false on error', async () => { mockPut.mockRejectedValueOnce(new Error('fail')) const { saveAnalysis } = setup({ pluginId: 'p1' }) const ok = await saveAnalysis(1, { score: 1 }) expect(ok).toBe(false) }) it('should require pluginId', async () => { const { saveAnalysis } = setup() const ok = await saveAnalysis(1, { score: 1 }) expect(ok).toBe(false) }) }) // ------------------------------------------------------------------------- // save (combined) // ------------------------------------------------------------------------- describe('save', () => { it('should call both endpoints when design and analysis provided', async () => { const { save } = setup({ pluginId: 'p1', schemaVersion: '2.0' }) const ok = await save(1, { design: { layout: 'grid' }, analysis: { score: 0.9 }, }) expect(ok).toBe(true) expect(mockPut).toHaveBeenCalledTimes(2) }) it('should call only design endpoint when analysis is omitted', async () => { const { save } = setup({ pluginId: 'p1' }) await save(1, { design: { x: 1 } }) expect(mockPut).toHaveBeenCalledTimes(1) expect(mockPut.mock.calls[0][0]).toBe('/experiments/1/data') }) it('should call only analysis endpoint when design is omitted', async () => { const { save } = setup({ pluginId: 'p1' }) await save(1, { analysis: { score: 1 } }) expect(mockPut).toHaveBeenCalledTimes(1) expect(mockPut.mock.calls[0][0]).toBe('/experiments/1/results/p1') }) it('should return false if either call fails', async () => { mockPut .mockResolvedValueOnce({}) // design succeeds .mockRejectedValueOnce(new Error()) // analysis fails const { save } = setup({ pluginId: 'p1' }) const ok = await save(1, { design: { x: 1 }, analysis: { y: 2 }, }) expect(ok).toBe(false) }) }) // ------------------------------------------------------------------------- // loadDesign // ------------------------------------------------------------------------- describe('loadDesign', () => { it('should call GET /experiments/{id}/data', async () => { mockGet.mockResolvedValueOnce({ data: { key: 'value' } }) const { loadDesign } = setup() await loadDesign(42) expect(mockGet).toHaveBeenCalledOnce() expect(mockGet).toHaveBeenCalledWith('/experiments/42/data') }) it('should return the data on success', async () => { mockGet.mockResolvedValueOnce({ data: { key: 'value' } }) const { loadDesign } = setup() const result = await loadDesign(1) expect(result).toEqual({ data: { key: 'value' } }) }) it('should return null on error', async () => { mockGet.mockRejectedValueOnce(new Error('not found')) const { loadDesign, error } = setup() const result = await loadDesign(999) expect(result).toBeNull() expect(error.value).toBe('not found') }) it('should set isLoading and lastLoadedAt during the call', async () => { let resolveGet!: (v: unknown) => void mockGet.mockImplementationOnce(() => new Promise((r) => { resolveGet = r })) const { loadDesign, isLoading, lastLoadedAt } = setup() expect(isLoading.value).toBe(false) const promise = loadDesign(42) expect(isLoading.value).toBe(true) resolveGet({ data: { key: 'value' } }) await promise expect(isLoading.value).toBe(false) expect(lastLoadedAt.value).toBeInstanceOf(Date) }) }) // ------------------------------------------------------------------------- // loadAnalysis // ------------------------------------------------------------------------- describe('loadAnalysis', () => { it('should call GET /experiments/{id}/results/{pluginId}', async () => { mockGet.mockResolvedValueOnce({ result: { score: 1 } }) const { loadAnalysis } = setup({ pluginId: 'p1' }) await loadAnalysis(42) expect(mockGet).toHaveBeenCalledOnce() expect(mockGet).toHaveBeenCalledWith('/experiments/42/results/p1') }) it('should return the result on success', async () => { mockGet.mockResolvedValueOnce({ result: { score: 0.5 } }) const { loadAnalysis } = setup({ pluginId: 'p1' }) const result = await loadAnalysis(1) expect(result).toEqual({ result: { score: 0.5 } }) }) it('should return null on error', async () => { mockGet.mockRejectedValueOnce(new Error('not found')) const { loadAnalysis } = setup({ pluginId: 'p1' }) const result = await loadAnalysis(999) expect(result).toBeNull() }) it('should return null when pluginId not set', async () => { const { loadAnalysis, error } = setup() const result = await loadAnalysis(1) expect(result).toBeNull() expect(error.value).toBe('pluginId is required for loadAnalysis') }) }) // ------------------------------------------------------------------------- // deleteDesign // ------------------------------------------------------------------------- describe('deleteDesign', () => { it('should call DELETE /experiments/{id}/data', async () => { const { deleteDesign } = setup() await deleteDesign(42) expect(mockDelete).toHaveBeenCalledOnce() expect(mockDelete).toHaveBeenCalledWith('/experiments/42/data') }) it('should return true on success', async () => { const { deleteDesign } = setup() const ok = await deleteDesign(1) expect(ok).toBe(true) }) it('should return false on error', async () => { mockDelete.mockRejectedValueOnce(new Error('fail')) const { deleteDesign } = setup() const ok = await deleteDesign(1) expect(ok).toBe(false) }) }) // ------------------------------------------------------------------------- // deleteAnalysis // ------------------------------------------------------------------------- describe('deleteAnalysis', () => { it('should call DELETE /experiments/{id}/results/{pluginId}', async () => { const { deleteAnalysis } = setup({ pluginId: 'p1' }) await deleteAnalysis(42) expect(mockDelete).toHaveBeenCalledOnce() expect(mockDelete).toHaveBeenCalledWith('/experiments/42/results/p1') }) it('should return false when pluginId not set', async () => { const { deleteAnalysis, error } = setup() const ok = await deleteAnalysis(1) expect(ok).toBe(false) expect(error.value).toBe('pluginId is required for deleteAnalysis') }) }) })