@morscherlab/mint-sdk 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{EmptyState-DgBRoYAy.js → EmptyState-LMPS3keb.js} +3 -193
- package/dist/EmptyState-LMPS3keb.js.map +1 -0
- package/dist/{ExperimentPopover-BFw3QX8L.js → ExperimentPopover-D6_RGzTY.js} +113 -35
- package/dist/ExperimentPopover-D6_RGzTY.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BBWXC7KW.js → ExperimentSelectorModal-CDNsNudn.js} +9 -11
- package/dist/ExperimentSelectorModal-CDNsNudn.js.map +1 -0
- package/dist/{SettingsModal-Dpg-X1OM.js → SettingsModal-BGBimSIA.js} +5 -5
- package/dist/{SettingsModal-Dpg-X1OM.js.map → SettingsModal-BGBimSIA.js.map} +1 -1
- package/dist/__stories__/experiment-helpers.d.ts +3 -0
- package/dist/__tests__/stores/experiment.test.d.ts +1 -0
- package/dist/{analysisArtifactTypes-ejqR8MX3.js → analysisArtifactTypes-DQ0MjwHZ.js} +1 -1
- package/dist/{analysisArtifactTypes-ejqR8MX3.js.map → analysisArtifactTypes-DQ0MjwHZ.js.map} +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +5 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +1 -5
- package/dist/components/PluginWorkspaceView.props.d.ts +1 -4
- package/dist/components/PluginWorkspaceView.vue.d.ts +0 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
- package/dist/components/index.js +7 -7
- package/dist/{components-Ci_BTx00.js → components-BbOyjE2T.js} +24 -29
- package/dist/components-BbOyjE2T.js.map +1 -0
- package/dist/composables/experiment-utils.d.ts +10 -0
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.js +11 -10
- package/dist/composables/useAppExperiment.d.ts +29 -17
- package/dist/composables/usePlatformContext.d.ts +3 -3
- package/dist/{composables-C0VEJ73O.js → composables-CW8NRXLB.js} +5 -5
- package/dist/{composables-C0VEJ73O.js.map → composables-CW8NRXLB.js.map} +1 -1
- package/dist/experiment-Y9Iz7KYi.js +95 -0
- package/dist/experiment-Y9Iz7KYi.js.map +1 -0
- package/dist/{experiment-utils-CxkruZ3G.js → experiment-utils-7Yw-h0tD.js} +16 -2
- package/dist/{experiment-utils-CxkruZ3G.js.map → experiment-utils-7Yw-h0tD.js.map} +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/install.js +7 -7
- package/dist/{plotly.min-BR53hja4.js → plotly.min-c2Sez60c.js} +1 -1
- package/dist/{plotly.min-BR53hja4.js.map → plotly.min-c2Sez60c.js.map} +1 -1
- package/dist/stores/auth.d.ts +6 -6
- package/dist/stores/experiment.d.ts +166 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +3 -2
- package/dist/styles.css +188 -0
- package/dist/templates/index.js +4 -4
- package/dist/{templates-Cdj93S9C.js → templates-CJRFiWUX.js} +2 -2
- package/dist/{templates-Cdj93S9C.js.map → templates-CJRFiWUX.js.map} +1 -1
- package/dist/types/auth.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/platform.d.ts +1 -1
- package/dist/{auth-B87xM--s.js → useApi-CDBq6z23.js} +191 -2
- package/dist/useApi-CDBq6z23.js.map +1 -0
- package/dist/{useControlSchema-B_qgU4rW.js → useControlSchema-BEAgLyyx.js} +5 -4
- package/dist/{useControlSchema-B_qgU4rW.js.map → useControlSchema-BEAgLyyx.js.map} +1 -1
- package/dist/{useDropdownState-gVaMXtwW.js → useDropdownState-Be-ug_ci.js} +2 -2
- package/dist/{useDropdownState-gVaMXtwW.js.map → useDropdownState-Be-ug_ci.js.map} +1 -1
- package/dist/{useExperimentSelector-BuQZRgMt.js → useExperimentSelector-Dld4841S.js} +4 -3
- package/dist/{useExperimentSelector-BuQZRgMt.js.map → useExperimentSelector-Dld4841S.js.map} +1 -1
- package/dist/{useFormBuilder-D1G5J5Z5.js → useFormBuilder-CR9pBApW.js} +2 -2
- package/dist/{useFormBuilder-D1G5J5Z5.js.map → useFormBuilder-CR9pBApW.js.map} +1 -1
- package/dist/{useProtocolTemplates-DeaOmMaw.js → useProtocolTemplates-DXeIHMwV.js} +38 -42
- package/dist/useProtocolTemplates-DXeIHMwV.js.map +1 -0
- package/package.json +1 -1
- package/src/__stories__/experiment-helpers.ts +12 -9
- package/src/__tests__/components/AppTopBar.test.ts +10 -6
- package/src/__tests__/components/ExperimentPopover.test.ts +64 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +86 -41
- package/src/__tests__/components/PluginWorkspaceView.test.ts +103 -19
- package/src/__tests__/components/SettingsModal.test.ts +1 -1
- package/src/__tests__/composables/experiment-utils.test.ts +33 -0
- package/src/__tests__/composables/useAppExperiment.test.ts +331 -92
- package/src/__tests__/composables/useAuth.test.ts +2 -2
- package/src/__tests__/composables/usePlatformContext.test.ts +5 -1
- package/src/__tests__/stores/experiment.test.ts +300 -0
- package/src/components/AppTopBar.vue +0 -2
- package/src/components/ExperimentPopover.story.vue +59 -0
- package/src/components/ExperimentPopover.vue +96 -6
- package/src/components/ExperimentSelectorModal.story.vue +6 -16
- package/src/components/ExperimentSelectorModal.vue +8 -4
- package/src/components/PluginWorkspaceView.props.ts +1 -4
- package/src/components/PluginWorkspaceView.story.vue +12 -5
- package/src/components/PluginWorkspaceView.vue +0 -2
- package/src/composables/experiment-utils.ts +22 -0
- package/src/composables/index.ts +1 -2
- package/src/composables/useAppExperiment.ts +57 -67
- package/src/composables/useAuth.ts +1 -1
- package/src/stores/experiment.ts +120 -0
- package/src/stores/index.ts +1 -0
- package/src/styles/components/experiment-popover.css +105 -0
- package/src/types/auth.ts +1 -1
- package/src/types/platform.ts +1 -1
- package/dist/EmptyState-DgBRoYAy.js.map +0 -1
- package/dist/ExperimentPopover-BFw3QX8L.js.map +0 -1
- package/dist/ExperimentSelectorModal-BBWXC7KW.js.map +0 -1
- package/dist/auth-B87xM--s.js.map +0 -1
- package/dist/components-Ci_BTx00.js.map +0 -1
- package/dist/useProtocolTemplates-DeaOmMaw.js.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { ref, computed, nextTick } from 'vue'
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia'
|
|
4
|
+
import axios from 'axios'
|
|
3
5
|
|
|
4
6
|
// Capture side-effect callbacks so tests can invoke them directly
|
|
5
7
|
let capturedScopeDispose: (() => void) | null = null
|
|
@@ -22,6 +24,7 @@ vi.mock('vue', async () => {
|
|
|
22
24
|
|
|
23
25
|
import { useAppExperiment, APP_EXPERIMENT_KEY } from '../../composables/useAppExperiment'
|
|
24
26
|
import type { AppExperimentState } from '../../composables/useAppExperiment'
|
|
27
|
+
import { useExperimentStore } from '../../stores/experiment'
|
|
25
28
|
import type { ExperimentSummary } from '../../types'
|
|
26
29
|
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
@@ -45,6 +48,37 @@ function getState(): AppExperimentState {
|
|
|
45
48
|
return capturedProvideValue as AppExperimentState
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
interface PendingFetch {
|
|
52
|
+
promise: Promise<{ data: ExperimentSummary }>
|
|
53
|
+
resolveWith: (experiment: ExperimentSummary) => void
|
|
54
|
+
rejectWith: (error: Error) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read at call time, not at spy-install time: axios binds its prototype methods
|
|
58
|
+
// into each instance when the instance is created, so a spy installed per test
|
|
59
|
+
// never reaches the client the SDK already built.
|
|
60
|
+
let pendingFetch: PendingFetch | null = null
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Holds the platform request open so tests can observe the composable while the
|
|
64
|
+
* store is mid-resolve — the state an id-only deep link starts in.
|
|
65
|
+
*/
|
|
66
|
+
function stubExperimentFetch(): PendingFetch {
|
|
67
|
+
let settle!: (result: { data: ExperimentSummary } | Error) => void
|
|
68
|
+
const promise = new Promise<{ data: ExperimentSummary }>((resolve, reject) => {
|
|
69
|
+
settle = (result) => {
|
|
70
|
+
if (result instanceof Error) reject(result)
|
|
71
|
+
else resolve(result)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
pendingFetch = {
|
|
75
|
+
promise,
|
|
76
|
+
resolveWith: (experiment: ExperimentSummary) => settle({ data: experiment }),
|
|
77
|
+
rejectWith: (error: Error) => settle(error),
|
|
78
|
+
}
|
|
79
|
+
return pendingFetch
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
// ---------------------------------------------------------------------------
|
|
49
83
|
// Tests
|
|
50
84
|
// ---------------------------------------------------------------------------
|
|
@@ -54,6 +88,12 @@ describe('useAppExperiment', () => {
|
|
|
54
88
|
capturedScopeDispose = null
|
|
55
89
|
capturedProvideKey = null
|
|
56
90
|
capturedProvideValue = null
|
|
91
|
+
setActivePinia(createPinia())
|
|
92
|
+
pendingFetch = null
|
|
93
|
+
vi.spyOn(axios.Axios.prototype, 'get').mockImplementation((() => {
|
|
94
|
+
if (!pendingFetch) return Promise.reject(new Error('Unexpected platform request'))
|
|
95
|
+
return pendingFetch.promise
|
|
96
|
+
}) as never)
|
|
57
97
|
vi.useFakeTimers()
|
|
58
98
|
})
|
|
59
99
|
|
|
@@ -79,107 +119,138 @@ describe('useAppExperiment', () => {
|
|
|
79
119
|
})
|
|
80
120
|
|
|
81
121
|
// -------------------------------------------------------------------------
|
|
82
|
-
//
|
|
122
|
+
// Store-backed state
|
|
83
123
|
// -------------------------------------------------------------------------
|
|
84
124
|
|
|
85
|
-
describe('
|
|
86
|
-
it('should
|
|
87
|
-
const { experimentName, experimentCode, experimentId } = useAppExperiment(
|
|
88
|
-
experiment: makeExperiment({ id: 8, name: 'Initial run', experiment_code: 'EXP-008' }),
|
|
89
|
-
})
|
|
125
|
+
describe('store-backed experiment state', () => {
|
|
126
|
+
it('should mirror a record the platform already returned', () => {
|
|
127
|
+
const { experimentName, experimentCode, experimentId } = useAppExperiment()
|
|
90
128
|
|
|
91
|
-
|
|
92
|
-
expect(experimentCode.value).toBe('EXP-008')
|
|
93
|
-
expect(experimentId.value).toBe(8)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('should sync and clear a reactive experiment option', async () => {
|
|
97
|
-
const source = ref<ExperimentSummary | null>(
|
|
129
|
+
useExperimentStore().select(
|
|
98
130
|
makeExperiment({ id: 8, name: 'Initial run', experiment_code: 'EXP-008' }),
|
|
99
131
|
)
|
|
100
|
-
const { experimentName, experimentCode, experimentId } = useAppExperiment({
|
|
101
|
-
experiment: source,
|
|
102
|
-
})
|
|
103
132
|
|
|
104
133
|
expect(experimentName.value).toBe('Initial run')
|
|
105
134
|
expect(experimentCode.value).toBe('EXP-008')
|
|
106
135
|
expect(experimentId.value).toBe(8)
|
|
136
|
+
})
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
it('should leave experimentCode undefined for a record without a code', () => {
|
|
139
|
+
const { experimentCode } = useAppExperiment()
|
|
140
|
+
|
|
141
|
+
useExperimentStore().select(makeExperiment({ id: 7, name: 'Alpha Run' }))
|
|
142
|
+
|
|
143
|
+
expect(experimentCode.value).toBeUndefined()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should follow the store when the selection changes', () => {
|
|
147
|
+
const store = useExperimentStore()
|
|
148
|
+
const { experimentName, experimentCode, experimentId } = useAppExperiment()
|
|
149
|
+
|
|
150
|
+
store.select(makeExperiment({ id: 8, name: 'Initial run', experiment_code: 'EXP-008' }))
|
|
151
|
+
store.select(makeExperiment({ id: 9, name: 'Updated run', experiment_code: 'EXP-009' }))
|
|
110
152
|
|
|
111
153
|
expect(experimentName.value).toBe('Updated run')
|
|
112
154
|
expect(experimentCode.value).toBe('EXP-009')
|
|
113
155
|
expect(experimentId.value).toBe(9)
|
|
156
|
+
})
|
|
114
157
|
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
it('should expose the status from the store record', () => {
|
|
159
|
+
useAppExperiment()
|
|
160
|
+
|
|
161
|
+
useExperimentStore().select(makeExperiment({ status: 'completed' }))
|
|
162
|
+
|
|
163
|
+
expect(getState().experimentStatus.value).toBe('completed')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should reset when the store is cleared', () => {
|
|
167
|
+
const store = useExperimentStore()
|
|
168
|
+
const { experimentName, experimentCode, experimentStatus, experimentId } = useAppExperiment()
|
|
169
|
+
|
|
170
|
+
store.select(makeExperiment({ id: 99, name: 'To Be Cleared', experiment_code: 'EXP-099' }))
|
|
171
|
+
store.clear()
|
|
117
172
|
|
|
118
173
|
expect(experimentName.value).toBeUndefined()
|
|
119
174
|
expect(experimentCode.value).toBeUndefined()
|
|
175
|
+
expect(experimentStatus.value).toBeUndefined()
|
|
120
176
|
expect(experimentId.value).toBeNull()
|
|
121
177
|
})
|
|
122
178
|
|
|
123
|
-
it('should
|
|
124
|
-
const
|
|
125
|
-
const
|
|
179
|
+
it('should report isResolving while the store resolves an id', async () => {
|
|
180
|
+
const fetch = stubExperimentFetch()
|
|
181
|
+
const store = useExperimentStore()
|
|
182
|
+
const { isResolving } = useAppExperiment()
|
|
126
183
|
|
|
127
|
-
|
|
184
|
+
const pending = store.selectById(8)
|
|
185
|
+
expect(isResolving.value).toBe(true)
|
|
128
186
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
187
|
+
fetch.resolveWith(makeExperiment({ id: 8, name: 'Resolved run' }))
|
|
188
|
+
await pending
|
|
189
|
+
|
|
190
|
+
expect(isResolving.value).toBe(false)
|
|
132
191
|
})
|
|
133
192
|
|
|
134
|
-
it('should
|
|
135
|
-
const
|
|
136
|
-
const
|
|
193
|
+
it('should expose the bound id but no name while an id-only binding resolves', async () => {
|
|
194
|
+
const fetch = stubExperimentFetch()
|
|
195
|
+
const store = useExperimentStore()
|
|
196
|
+
const { experimentId, experimentName, experimentCode } = useAppExperiment()
|
|
137
197
|
|
|
138
|
-
|
|
198
|
+
const pending = store.selectById(8)
|
|
139
199
|
|
|
140
|
-
expect(
|
|
200
|
+
expect(experimentId.value).toBe(8)
|
|
201
|
+
expect(experimentName.value).toBeUndefined()
|
|
202
|
+
expect(experimentCode.value).toBeUndefined()
|
|
203
|
+
|
|
204
|
+
fetch.resolveWith(makeExperiment({ id: 8, name: 'Resolved run' }))
|
|
205
|
+
await pending
|
|
141
206
|
})
|
|
142
207
|
|
|
143
|
-
it('should
|
|
144
|
-
const
|
|
208
|
+
it('should take the name from the platform once an id-only binding resolves', async () => {
|
|
209
|
+
const fetch = stubExperimentFetch()
|
|
210
|
+
const store = useExperimentStore()
|
|
211
|
+
const { experimentName } = useAppExperiment()
|
|
145
212
|
|
|
146
|
-
|
|
147
|
-
|
|
213
|
+
const pending = store.selectById(8)
|
|
214
|
+
fetch.resolveWith(makeExperiment({ id: 8, name: 'Hexokinase knockout run' }))
|
|
215
|
+
await pending
|
|
148
216
|
|
|
149
|
-
expect(experimentName.value).toBe('
|
|
150
|
-
expect(experimentId.value).toBe(2)
|
|
217
|
+
expect(experimentName.value).toBe('Hexokinase knockout run')
|
|
151
218
|
})
|
|
152
|
-
})
|
|
153
219
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
220
|
+
it('should surface the store error when resolution fails', async () => {
|
|
221
|
+
const fetch = stubExperimentFetch()
|
|
222
|
+
const store = useExperimentStore()
|
|
223
|
+
const { error } = useAppExperiment()
|
|
157
224
|
|
|
158
|
-
|
|
159
|
-
|
|
225
|
+
const pending = store.selectById(8)
|
|
226
|
+
fetch.rejectWith(new Error('Experiment not found'))
|
|
227
|
+
await pending
|
|
160
228
|
|
|
161
|
-
expect(
|
|
162
|
-
expect(experimentId.value).toBeNull()
|
|
229
|
+
expect(error.value).toBe('Experiment not found')
|
|
163
230
|
})
|
|
164
231
|
|
|
165
|
-
it('should
|
|
166
|
-
const
|
|
232
|
+
it('should expose no name after a failed resolve, keeping the bound id', async () => {
|
|
233
|
+
const fetch = stubExperimentFetch()
|
|
234
|
+
const store = useExperimentStore()
|
|
235
|
+
const { experimentName, experimentCode, experimentId } = useAppExperiment()
|
|
167
236
|
|
|
168
|
-
|
|
169
|
-
|
|
237
|
+
const pending = store.selectById(8)
|
|
238
|
+
fetch.rejectWith(new Error('Experiment not found'))
|
|
239
|
+
await pending
|
|
170
240
|
|
|
171
|
-
expect(
|
|
241
|
+
expect(experimentName.value).toBeUndefined()
|
|
242
|
+
expect(experimentCode.value).toBeUndefined()
|
|
243
|
+
expect(experimentId.value).toBe(8)
|
|
172
244
|
})
|
|
173
245
|
})
|
|
174
246
|
|
|
175
|
-
describe('returned refs are
|
|
176
|
-
it('should expose experimentName as
|
|
247
|
+
describe('returned refs are read-only projections of the store', () => {
|
|
248
|
+
it('should expose experimentName as undefined with nothing selected', () => {
|
|
177
249
|
const { experimentName } = useAppExperiment()
|
|
178
|
-
// Readonly refs still have a .value property for reading
|
|
179
250
|
expect(experimentName.value).toBeUndefined()
|
|
180
251
|
})
|
|
181
252
|
|
|
182
|
-
it('should expose experimentId as
|
|
253
|
+
it('should expose experimentId as null with nothing selected', () => {
|
|
183
254
|
const { experimentId } = useAppExperiment()
|
|
184
255
|
expect(experimentId.value).toBeNull()
|
|
185
256
|
})
|
|
@@ -210,48 +281,75 @@ describe('useAppExperiment', () => {
|
|
|
210
281
|
})
|
|
211
282
|
|
|
212
283
|
// -------------------------------------------------------------------------
|
|
213
|
-
//
|
|
284
|
+
// onSelect — fired by the store adopting an experiment, not by the shell
|
|
214
285
|
// -------------------------------------------------------------------------
|
|
215
286
|
|
|
216
|
-
describe('
|
|
217
|
-
it('should call onSelect
|
|
287
|
+
describe('onSelect', () => {
|
|
288
|
+
it('should call onSelect when the store adopts an experiment', async () => {
|
|
218
289
|
const onSelect = vi.fn()
|
|
219
290
|
useAppExperiment({ onSelect })
|
|
220
|
-
const state = getState()
|
|
221
|
-
const experiment = makeExperiment()
|
|
222
291
|
|
|
223
|
-
|
|
292
|
+
useExperimentStore().select(makeExperiment())
|
|
293
|
+
await nextTick()
|
|
224
294
|
|
|
225
295
|
expect(onSelect).toHaveBeenCalledOnce()
|
|
226
|
-
expect(onSelect).toHaveBeenCalledWith(experiment)
|
|
227
296
|
})
|
|
228
297
|
|
|
229
|
-
it('should
|
|
230
|
-
const
|
|
231
|
-
|
|
298
|
+
it('should pass the platform record through unchanged', async () => {
|
|
299
|
+
const onSelect = vi.fn()
|
|
300
|
+
useAppExperiment({ onSelect })
|
|
232
301
|
const experiment = makeExperiment({ id: 5, name: 'Selected Exp' })
|
|
233
302
|
|
|
234
|
-
|
|
303
|
+
useExperimentStore().select(experiment)
|
|
304
|
+
await nextTick()
|
|
235
305
|
|
|
236
|
-
expect(
|
|
237
|
-
expect(experimentName.value).toBe('Selected Exp')
|
|
306
|
+
expect(onSelect).toHaveBeenCalledWith(experiment)
|
|
238
307
|
})
|
|
239
308
|
|
|
240
|
-
it('should
|
|
241
|
-
|
|
242
|
-
const
|
|
309
|
+
it('should call onSelect when an id-only binding resolves', async () => {
|
|
310
|
+
const fetch = stubExperimentFetch()
|
|
311
|
+
const onSelect = vi.fn()
|
|
312
|
+
const store = useExperimentStore()
|
|
313
|
+
useAppExperiment({ onSelect })
|
|
243
314
|
|
|
244
|
-
|
|
245
|
-
|
|
315
|
+
const pending = store.selectById(8)
|
|
316
|
+
fetch.resolveWith(makeExperiment({ id: 8, name: 'Resolved run' }))
|
|
317
|
+
await pending
|
|
318
|
+
await nextTick()
|
|
246
319
|
|
|
247
|
-
expect(
|
|
320
|
+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ name: 'Resolved run' }))
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('should not call onSelect when the store is cleared', async () => {
|
|
324
|
+
const onSelect = vi.fn()
|
|
325
|
+
const store = useExperimentStore()
|
|
326
|
+
useAppExperiment({ onSelect })
|
|
327
|
+
|
|
328
|
+
store.select(makeExperiment())
|
|
329
|
+
await nextTick()
|
|
330
|
+
onSelect.mockClear()
|
|
331
|
+
|
|
332
|
+
store.clear()
|
|
333
|
+
await nextTick()
|
|
334
|
+
|
|
335
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should not call onSelect for an experiment already selected at setup', async () => {
|
|
339
|
+
const onSelect = vi.fn()
|
|
340
|
+
useExperimentStore().select(makeExperiment())
|
|
341
|
+
|
|
342
|
+
useAppExperiment({ onSelect })
|
|
343
|
+
await nextTick()
|
|
344
|
+
|
|
345
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
248
346
|
})
|
|
249
347
|
|
|
250
|
-
it('should not throw when no onSelect callback is provided', () => {
|
|
348
|
+
it('should not throw when no onSelect callback is provided', async () => {
|
|
251
349
|
useAppExperiment()
|
|
252
|
-
const state = getState()
|
|
253
350
|
|
|
254
|
-
expect(() =>
|
|
351
|
+
expect(() => useExperimentStore().select(makeExperiment())).not.toThrow()
|
|
352
|
+
await nextTick()
|
|
255
353
|
})
|
|
256
354
|
})
|
|
257
355
|
|
|
@@ -391,17 +489,36 @@ describe('useAppExperiment', () => {
|
|
|
391
489
|
// -------------------------------------------------------------------------
|
|
392
490
|
|
|
393
491
|
describe('handleDetach', () => {
|
|
394
|
-
it('should clear
|
|
395
|
-
const
|
|
492
|
+
it('should clear the shared store', () => {
|
|
493
|
+
const store = useExperimentStore()
|
|
494
|
+
const { experimentId, experimentName } = useAppExperiment()
|
|
396
495
|
const state = getState()
|
|
397
496
|
|
|
398
|
-
|
|
497
|
+
store.select(makeExperiment({ id: 3, name: 'Detachable' }))
|
|
399
498
|
state.handleDetach()
|
|
400
499
|
|
|
500
|
+
expect(store.current).toBeNull()
|
|
501
|
+
expect(store.currentId).toBeNull()
|
|
401
502
|
expect(experimentId.value).toBeNull()
|
|
402
503
|
expect(experimentName.value).toBeUndefined()
|
|
403
504
|
})
|
|
404
505
|
|
|
506
|
+
it('should clear a binding whose resolution failed', async () => {
|
|
507
|
+
const fetch = stubExperimentFetch()
|
|
508
|
+
const store = useExperimentStore()
|
|
509
|
+
const { experimentId, error } = useAppExperiment()
|
|
510
|
+
const state = getState()
|
|
511
|
+
|
|
512
|
+
const pending = store.selectById(8)
|
|
513
|
+
fetch.rejectWith(new Error('Experiment not found'))
|
|
514
|
+
await pending
|
|
515
|
+
|
|
516
|
+
state.handleDetach()
|
|
517
|
+
|
|
518
|
+
expect(experimentId.value).toBeNull()
|
|
519
|
+
expect(error.value).toBeNull()
|
|
520
|
+
})
|
|
521
|
+
|
|
405
522
|
it('should call onDetach callback', () => {
|
|
406
523
|
const onDetach = vi.fn()
|
|
407
524
|
useAppExperiment({ onDetach })
|
|
@@ -421,7 +538,7 @@ describe('useAppExperiment', () => {
|
|
|
421
538
|
|
|
422
539
|
it('should call onDetach after clearing state', () => {
|
|
423
540
|
const calls: string[] = []
|
|
424
|
-
const {
|
|
541
|
+
const { experimentId } = useAppExperiment({
|
|
425
542
|
onDetach: () => {
|
|
426
543
|
// By the time onDetach fires, experimentId should already be null
|
|
427
544
|
calls.push(experimentId.value === null ? 'cleared' : 'not-cleared')
|
|
@@ -429,7 +546,7 @@ describe('useAppExperiment', () => {
|
|
|
429
546
|
})
|
|
430
547
|
const state = getState()
|
|
431
548
|
|
|
432
|
-
|
|
549
|
+
useExperimentStore().select(makeExperiment())
|
|
433
550
|
state.handleDetach()
|
|
434
551
|
|
|
435
552
|
expect(calls).toEqual(['cleared'])
|
|
@@ -462,19 +579,44 @@ describe('useAppExperiment', () => {
|
|
|
462
579
|
expect(getState().showDetach.value).toBe(false)
|
|
463
580
|
})
|
|
464
581
|
|
|
465
|
-
it('should be true after an experiment is
|
|
466
|
-
|
|
582
|
+
it('should be true after an experiment is selected', () => {
|
|
583
|
+
useAppExperiment()
|
|
467
584
|
|
|
468
|
-
|
|
585
|
+
useExperimentStore().select(makeExperiment())
|
|
469
586
|
|
|
470
587
|
expect(getState().showDetach.value).toBe(true)
|
|
471
588
|
})
|
|
472
589
|
|
|
473
|
-
it('should
|
|
474
|
-
const
|
|
590
|
+
it('should be true while an id-only binding is still resolving', async () => {
|
|
591
|
+
const fetch = stubExperimentFetch()
|
|
592
|
+
const store = useExperimentStore()
|
|
593
|
+
useAppExperiment()
|
|
594
|
+
|
|
595
|
+
const pending = store.selectById(8)
|
|
596
|
+
expect(getState().showDetach.value).toBe(true)
|
|
475
597
|
|
|
476
|
-
|
|
477
|
-
|
|
598
|
+
fetch.resolveWith(makeExperiment({ id: 8 }))
|
|
599
|
+
await pending
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('should stay true after a failed resolve so the user can detach', async () => {
|
|
603
|
+
const fetch = stubExperimentFetch()
|
|
604
|
+
const store = useExperimentStore()
|
|
605
|
+
useAppExperiment()
|
|
606
|
+
|
|
607
|
+
const pending = store.selectById(8)
|
|
608
|
+
fetch.rejectWith(new Error('Experiment not found'))
|
|
609
|
+
await pending
|
|
610
|
+
|
|
611
|
+
expect(getState().showDetach.value).toBe(true)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('should revert to false after the store is cleared', () => {
|
|
615
|
+
const store = useExperimentStore()
|
|
616
|
+
useAppExperiment()
|
|
617
|
+
|
|
618
|
+
store.select(makeExperiment())
|
|
619
|
+
store.clear()
|
|
478
620
|
|
|
479
621
|
expect(getState().showDetach.value).toBe(false)
|
|
480
622
|
})
|
|
@@ -558,10 +700,10 @@ describe('useAppExperiment', () => {
|
|
|
558
700
|
const saveDisabled = ref(false)
|
|
559
701
|
const saveDisabledMessage = ref<string | undefined>('Ready to save')
|
|
560
702
|
const onSave = vi.fn().mockResolvedValue('Saved')
|
|
561
|
-
|
|
703
|
+
useAppExperiment({ onSave, saveDisabled, saveDisabledMessage })
|
|
562
704
|
const state = getState()
|
|
563
705
|
|
|
564
|
-
|
|
706
|
+
useExperimentStore().select(makeExperiment({
|
|
565
707
|
id: 12,
|
|
566
708
|
name: 'Dose response',
|
|
567
709
|
status: 'ongoing',
|
|
@@ -572,12 +714,14 @@ describe('useAppExperiment', () => {
|
|
|
572
714
|
experimentName: 'Dose response',
|
|
573
715
|
experimentCode: 'EXP-012',
|
|
574
716
|
experimentStatus: 'ongoing',
|
|
717
|
+
isResolving: false,
|
|
575
718
|
showSave: true,
|
|
576
719
|
showDetach: true,
|
|
577
720
|
saveDisabled: false,
|
|
578
721
|
saveDisabledMessage: 'Ready to save',
|
|
579
722
|
saveLoading: false,
|
|
580
723
|
})
|
|
724
|
+
expect(state.popover.value.error).toBeUndefined()
|
|
581
725
|
|
|
582
726
|
saveDisabled.value = true
|
|
583
727
|
saveDisabledMessage.value = 'Locked'
|
|
@@ -590,6 +734,44 @@ describe('useAppExperiment', () => {
|
|
|
590
734
|
expect(state.popover.value.saveSuccessMessage).toBe('Saved')
|
|
591
735
|
})
|
|
592
736
|
|
|
737
|
+
it('marks the popover binding as resolving with no name to show', async () => {
|
|
738
|
+
const fetch = stubExperimentFetch()
|
|
739
|
+
const store = useExperimentStore()
|
|
740
|
+
useAppExperiment()
|
|
741
|
+
const state = getState()
|
|
742
|
+
|
|
743
|
+
const pending = store.selectById(12)
|
|
744
|
+
|
|
745
|
+
expect(state.popover.value).toMatchObject({
|
|
746
|
+
experimentName: undefined,
|
|
747
|
+
experimentCode: undefined,
|
|
748
|
+
isResolving: true,
|
|
749
|
+
showDetach: true,
|
|
750
|
+
})
|
|
751
|
+
expect(state.popover.value.error).toBeUndefined()
|
|
752
|
+
|
|
753
|
+
fetch.resolveWith(makeExperiment({ id: 12 }))
|
|
754
|
+
await pending
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('puts the resolution failure on the popover binding and keeps detach', async () => {
|
|
758
|
+
const fetch = stubExperimentFetch()
|
|
759
|
+
const store = useExperimentStore()
|
|
760
|
+
useAppExperiment()
|
|
761
|
+
const state = getState()
|
|
762
|
+
|
|
763
|
+
const pending = store.selectById(12)
|
|
764
|
+
fetch.rejectWith(new Error('Experiment not found'))
|
|
765
|
+
await pending
|
|
766
|
+
|
|
767
|
+
expect(state.popover.value).toMatchObject({
|
|
768
|
+
experimentName: undefined,
|
|
769
|
+
isResolving: false,
|
|
770
|
+
error: 'Experiment not found',
|
|
771
|
+
showDetach: true,
|
|
772
|
+
})
|
|
773
|
+
})
|
|
774
|
+
|
|
593
775
|
it('exposes live ExperimentSelectorModal props from state and return value', () => {
|
|
594
776
|
const appExperiment = useAppExperiment()
|
|
595
777
|
const state = getState()
|
|
@@ -600,7 +782,7 @@ describe('useAppExperiment', () => {
|
|
|
600
782
|
})
|
|
601
783
|
expect(appExperiment.selectorModal.value).toEqual(state.selectorModal.value)
|
|
602
784
|
|
|
603
|
-
|
|
785
|
+
useExperimentStore().select(makeExperiment({ id: 77 }))
|
|
604
786
|
state.openModal()
|
|
605
787
|
|
|
606
788
|
expect(state.selectorModal.value).toEqual({
|
|
@@ -609,6 +791,19 @@ describe('useAppExperiment', () => {
|
|
|
609
791
|
})
|
|
610
792
|
expect(appExperiment.popover.value.experimentName).toBe('Test Experiment')
|
|
611
793
|
})
|
|
794
|
+
|
|
795
|
+
it('keeps the selector bound to the id after a failed resolve', async () => {
|
|
796
|
+
const fetch = stubExperimentFetch()
|
|
797
|
+
const store = useExperimentStore()
|
|
798
|
+
useAppExperiment()
|
|
799
|
+
const state = getState()
|
|
800
|
+
|
|
801
|
+
const pending = store.selectById(77)
|
|
802
|
+
fetch.rejectWith(new Error('Experiment not found'))
|
|
803
|
+
await pending
|
|
804
|
+
|
|
805
|
+
expect(state.selectorModal.value.currentExperimentId).toBe(77)
|
|
806
|
+
})
|
|
612
807
|
})
|
|
613
808
|
|
|
614
809
|
// -------------------------------------------------------------------------
|
|
@@ -658,3 +853,47 @@ describe('useAppExperiment', () => {
|
|
|
658
853
|
})
|
|
659
854
|
})
|
|
660
855
|
})
|
|
856
|
+
|
|
857
|
+
describe('useAppExperiment browser storage isolation', () => {
|
|
858
|
+
function installLegacySelection() {
|
|
859
|
+
const storage: Record<string, string> = { 'mint-current-experiment-id': '42' }
|
|
860
|
+
vi.stubGlobal('localStorage', {
|
|
861
|
+
getItem: vi.fn((key: string) => storage[key] ?? null),
|
|
862
|
+
setItem: vi.fn((key: string, value: string) => { storage[key] = value }),
|
|
863
|
+
removeItem: vi.fn((key: string) => { delete storage[key] }),
|
|
864
|
+
clear: vi.fn(() => { for (const key of Object.keys(storage)) delete storage[key] }),
|
|
865
|
+
} as unknown as Storage)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
beforeEach(() => {
|
|
869
|
+
setActivePinia(createPinia())
|
|
870
|
+
pendingFetch = null
|
|
871
|
+
installLegacySelection()
|
|
872
|
+
vi.spyOn(axios.Axios.prototype, 'get').mockRejectedValue(new Error('Unexpected platform request'))
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
afterEach(() => {
|
|
876
|
+
vi.restoreAllMocks()
|
|
877
|
+
vi.unstubAllGlobals()
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
it('should ignore a legacy persisted id when nothing is selected', () => {
|
|
881
|
+
const { experimentId } = useAppExperiment()
|
|
882
|
+
|
|
883
|
+
expect(experimentId.value).toBeNull()
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
it('should not request an experiment during shell setup', () => {
|
|
887
|
+
useAppExperiment()
|
|
888
|
+
|
|
889
|
+
expect(axios.Axios.prototype.get).not.toHaveBeenCalled()
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
it('should leave an already-bound experiment alone', () => {
|
|
893
|
+
useExperimentStore().select(makeExperiment({ id: 7, name: 'Already chosen' }))
|
|
894
|
+
|
|
895
|
+
const { experimentName } = useAppExperiment()
|
|
896
|
+
|
|
897
|
+
expect(experimentName.value).toBe('Already chosen')
|
|
898
|
+
})
|
|
899
|
+
})
|
|
@@ -56,7 +56,7 @@ describe('useAuth', () => {
|
|
|
56
56
|
data: { access_token: 'test-token', expires_in: 3600, token_type: 'bearer' },
|
|
57
57
|
})
|
|
58
58
|
mockedAxios.get.mockResolvedValueOnce({
|
|
59
|
-
data: { id:
|
|
59
|
+
data: { id: 1, username: 'test', shortname: null, email: null, role: 'user', is_active: true },
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
const { login } = useAuth()
|
|
@@ -162,7 +162,7 @@ describe('useAuth', () => {
|
|
|
162
162
|
throw new Error(`Unexpected POST ${url}`)
|
|
163
163
|
})
|
|
164
164
|
mockedAxios.get.mockResolvedValue({
|
|
165
|
-
data: { id:
|
|
165
|
+
data: { id: 1, username: 'test', shortname: null, email: null, role: 'user', is_active: true },
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
const authA = useAuth()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
const unmountedCallbacks: Array<() => void> = []
|
|
4
4
|
|
|
@@ -39,6 +39,10 @@ function sendThemeMessage(origin: string, theme: 'light' | 'dark' | 'system'): v
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
describe('usePlatformContext', () => {
|
|
42
|
+
it('uses integer IDs for injected users', () => {
|
|
43
|
+
expectTypeOf<NonNullable<PlatformContext['user']>['id']>().toEqualTypeOf<number>()
|
|
44
|
+
})
|
|
45
|
+
|
|
42
46
|
beforeEach(() => {
|
|
43
47
|
unmountedCallbacks.length = 0
|
|
44
48
|
messageHandler = null
|