/** * Tests for useExperimentData. * * Covers the derived computeds plugins rely on when rendering plugin * data that was saved via the platform's /api/experiments/{id}/data * endpoint: * * - tree_data / treeData snake-or-camel case handling * - table_data / tableData * - summary_data validation (must have a `metadata` key) * - load / refresh / error paths * - lastLoadedAt tracks successful syncs */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createPinia, setActivePinia } from 'pinia' import axios, { type AxiosRequestConfig } from 'axios' import { useExperimentData } from '../../composables/useExperimentData' let fakeResponse: unknown = null let fakeError: Error | null = null let recordedUrls: string[] = [] describe('useExperimentData', () => { beforeEach(() => { setActivePinia(createPinia()) fakeResponse = {} fakeError = null recordedUrls = [] vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function ( this: unknown, url: string, _config?: AxiosRequestConfig, ) { recordedUrls.push(url) if (fakeError) throw fakeError return { data: fakeResponse } }) }) afterEach(() => { vi.restoreAllMocks() }) it('fetch resolves data and flips isLoading', async () => { fakeResponse = { tree_data: [{ id: 'a', label: 'A' }] } const hook = useExperimentData() expect(hook.isLoading.value).toBe(false) expect(hook.lastLoadedAt.value).toBeNull() const p = hook.fetch(42) expect(hook.isLoading.value).toBe(true) await p expect(hook.isLoading.value).toBe(false) expect(hook.data.value).toEqual({ tree_data: [{ id: 'a', label: 'A' }] }) expect(hook.lastLoadedAt.value).toBeInstanceOf(Date) expect(recordedUrls[0]).toBe('/experiments/42/data') }) it('exposes tree_data via treeData regardless of snake vs camel', async () => { fakeResponse = { tree_data: [{ id: '1' }] } const hook1 = useExperimentData() await hook1.fetch(1) expect(hook1.treeData.value).toEqual([{ id: '1' }]) fakeResponse = { treeData: [{ id: '2' }] } const hook2 = useExperimentData() await hook2.fetch(2) expect(hook2.treeData.value).toEqual([{ id: '2' }]) }) it('unwraps platform design-data responses and exposes sample helpers', async () => { const response = { experiment_id: 42, plugin_id: 'ms-designer', schema_version: '1.0', data: { tree_data: [{ id: 'design' }], samples: [ { sample_name: 'S1', sample_type: 'sample', conditions: { group: 'A' } }, { sample_name: 'Blank', sample_type: 'blank', conditions: { group: 'QC' } }, ], }, } fakeResponse = response const hook = useExperimentData() await hook.fetch(42) expect(hook.designData.value).toEqual(response.data) expect(hook.treeData.value).toEqual([{ id: 'design' }]) expect(hook.sampleNames.value).toEqual(['S1']) expect(hook.sampleOptions.value).toEqual([{ value: 'S1', label: 'S1' }]) }) it('tableData falls back to empty array when absent', async () => { fakeResponse = {} const hook = useExperimentData() await hook.fetch(1) expect(hook.tableData.value).toEqual([]) }) it('summaryData requires a metadata key', async () => { fakeResponse = { summary_data: { metadata: { n: 1 }, sections: [] } } const hook = useExperimentData() await hook.fetch(1) expect(hook.summaryData.value).toEqual({ metadata: { n: 1 }, sections: [], }) // Invalid summary shape -> returns null fakeResponse = { summary_data: { notmeta: true } } const hook2 = useExperimentData() await hook2.fetch(2) expect(hook2.summaryData.value).toBeNull() }) it('refresh reuses the last experimentId', async () => { fakeResponse = { tree_data: [] } const hook = useExperimentData() await hook.fetch(99) recordedUrls.length = 0 await hook.refresh() expect(recordedUrls).toEqual(['/experiments/99/data']) }) it('refresh before any fetch is a noop', async () => { const hook = useExperimentData() await hook.refresh() expect(recordedUrls).toEqual([]) }) it('sets error and clears data on network failure', async () => { fakeError = new Error('boom') const hook = useExperimentData() await hook.fetch(1) expect(hook.error.value).toBe('boom') expect(hook.data.value).toBeNull() expect(hook.isLoading.value).toBe(false) expect(hook.lastLoadedAt.value).toBeNull() }) it('apiBaseUrl option is honoured', async () => { fakeResponse = {} const hook = useExperimentData({ apiBaseUrl: '/api/plugin' }) await hook.fetch(5) // useApi attaches baseURL to the request config; the mocked axios // call records it in recordedUrls as the relative URL only, so we // can't observe baseURL directly here — assert the URL shape. expect(recordedUrls[0]).toBe('/experiments/5/data') }) })