@morscherlab/mint-sdk 1.0.56 → 1.0.57
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/__tests__/components/ArtifactSaveDialog.test.d.ts +1 -0
- package/dist/__tests__/components/ArtifactSelectorModal.test.d.ts +1 -0
- package/dist/__tests__/composables/useAnalysisArtifacts.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js +7 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -0
- package/dist/components/ArtifactSaveDialog.vue.d.ts +56 -0
- package/dist/components/ArtifactSelectorModal.vue.d.ts +41 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-BQwOeyiy.js → components-PC3SVsaa.js} +1743 -1144
- package/dist/components-PC3SVsaa.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +4 -3
- package/dist/composables/useAnalysisArtifacts.d.ts +59 -0
- package/dist/{composables-TpXyhRZZ.js → composables-OYTD0Y3r.js} +2 -2
- package/dist/{composables-TpXyhRZZ.js.map → composables-OYTD0Y3r.js.map} +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/styles.css +412 -0
- package/dist/types/analysisArtifactTypes.d.ts +100 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/{useJobsStatusTray-CVecN6Ao.js → useAnalysisArtifacts-Ob8UtVwl.js} +157 -2
- package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +516 -0
- package/src/__tests__/components/ArtifactSelectorModal.test.ts +178 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +667 -0
- package/src/components/ArtifactSaveDialog.story.vue +293 -0
- package/src/components/ArtifactSaveDialog.vue +383 -0
- package/src/components/ArtifactSelectorModal.story.vue +295 -0
- package/src/components/ArtifactSelectorModal.vue +326 -0
- package/src/components/index.ts +2 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useAnalysisArtifacts.ts +305 -0
- package/src/styles/components/artifact-save-dialog.css +17 -0
- package/src/styles/components/artifact-selector-modal.css +216 -0
- package/src/styles/index.css +2 -0
- package/src/types/analysisArtifactTypes.ts +119 -0
- package/src/types/index.ts +17 -0
- package/dist/components-BQwOeyiy.js.map +0 -1
- package/dist/useJobsStatusTray-CVecN6Ao.js.map +0 -1
- /package/dist/{ExperimentPopover-8A4Rhffp.js → ExperimentPopover-Ryusjrhv.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.57",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { mount, flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
import ArtifactSaveDialog from '../../components/ArtifactSaveDialog.vue'
|
|
4
|
+
import FileUploader from '../../components/FileUploader.vue'
|
|
5
|
+
import SegmentedControl from '../../components/SegmentedControl.vue'
|
|
6
|
+
import { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../../types/analysisArtifactTypes'
|
|
7
|
+
import type {
|
|
8
|
+
AnalysisArtifactDetail,
|
|
9
|
+
ArtifactSavePayload,
|
|
10
|
+
ArtifactSaveResult,
|
|
11
|
+
} from '../../types/analysisArtifactTypes'
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function makeDetail(overrides: Partial<AnalysisArtifactDetail> = {}): AnalysisArtifactDetail {
|
|
18
|
+
return {
|
|
19
|
+
id: 5,
|
|
20
|
+
experiment_id: 42,
|
|
21
|
+
plugin_id: 'plugin-a',
|
|
22
|
+
artifact_key: 'fit',
|
|
23
|
+
display_name: 'Dose Response Fit',
|
|
24
|
+
note: 'baseline run',
|
|
25
|
+
status: 'active',
|
|
26
|
+
result_keys: ['summary'],
|
|
27
|
+
result: { curves: [] },
|
|
28
|
+
tree: [],
|
|
29
|
+
summary: null,
|
|
30
|
+
...overrides,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFileDetail(overrides: Partial<AnalysisArtifactDetail> = {}): AnalysisArtifactDetail {
|
|
35
|
+
return makeDetail({
|
|
36
|
+
artifact_key: 'report',
|
|
37
|
+
display_name: 'QC Report',
|
|
38
|
+
result: {
|
|
39
|
+
schema_version: ANALYSIS_FILE_ARTIFACT_SCHEMA,
|
|
40
|
+
kind: 'report',
|
|
41
|
+
filename: 'qc-report.pdf',
|
|
42
|
+
file: {},
|
|
43
|
+
},
|
|
44
|
+
...overrides,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mountDialog(props: Record<string, unknown> = {}) {
|
|
49
|
+
const saveHandler = vi.fn<(payload: ArtifactSavePayload) => Promise<ArtifactSaveResult | void>>()
|
|
50
|
+
.mockResolvedValue(undefined)
|
|
51
|
+
const wrapper = mount(ArtifactSaveDialog, {
|
|
52
|
+
props: {
|
|
53
|
+
modelValue: true,
|
|
54
|
+
experimentId: 42,
|
|
55
|
+
saveHandler,
|
|
56
|
+
...props,
|
|
57
|
+
},
|
|
58
|
+
global: { stubs: { teleport: true } },
|
|
59
|
+
})
|
|
60
|
+
return { wrapper, saveHandler }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function submit(wrapper: ReturnType<typeof mountDialog>['wrapper']) {
|
|
64
|
+
await wrapper.get('.mint-artifact-save-dialog__submit').trigger('click')
|
|
65
|
+
await flushPromises()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const TEST_FILE = new File(['content'], 'plot.png', { type: 'image/png' })
|
|
69
|
+
|
|
70
|
+
async function chooseFile(wrapper: ReturnType<typeof mountDialog>['wrapper'], file = TEST_FILE) {
|
|
71
|
+
wrapper.findComponent(FileUploader).vm.$emit('upload', [file])
|
|
72
|
+
await flushPromises()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Tests
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
describe('ArtifactSaveDialog', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
|
|
82
|
+
window.history.replaceState({}, '', '/')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
// JSON create
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
describe('json create', () => {
|
|
90
|
+
it('should call the saveHandler once with a create payload', async () => {
|
|
91
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 } })
|
|
92
|
+
|
|
93
|
+
await submit(wrapper)
|
|
94
|
+
|
|
95
|
+
expect(saveHandler).toHaveBeenCalledOnce()
|
|
96
|
+
expect(saveHandler).toHaveBeenCalledWith({
|
|
97
|
+
kind: 'json',
|
|
98
|
+
mode: 'create',
|
|
99
|
+
experimentId: 42,
|
|
100
|
+
artifactKey: 'default',
|
|
101
|
+
result: { x: 1 },
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should use the typed artifact key and display name', async () => {
|
|
106
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 } })
|
|
107
|
+
|
|
108
|
+
await wrapper.get('.mint-artifact-save-dialog__key input').setValue('my-fit')
|
|
109
|
+
await wrapper.get('.mint-artifact-save-dialog__name input').setValue('My Fit')
|
|
110
|
+
await submit(wrapper)
|
|
111
|
+
|
|
112
|
+
const payload = saveHandler.mock.calls[0][0]
|
|
113
|
+
expect(payload.artifactKey).toBe('my-fit')
|
|
114
|
+
expect(payload).toMatchObject({ displayName: 'My Fit' })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should include a typed note as a string', async () => {
|
|
118
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 } })
|
|
119
|
+
|
|
120
|
+
await wrapper.get('.mint-artifact-save-dialog__note textarea').setValue('first pass')
|
|
121
|
+
await submit(wrapper)
|
|
122
|
+
|
|
123
|
+
expect(saveHandler.mock.calls[0][0].note).toBe('first pass')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should emit saved with the payload and close on success', async () => {
|
|
127
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 } })
|
|
128
|
+
saveHandler.mockResolvedValueOnce({ cleanupPending: false })
|
|
129
|
+
|
|
130
|
+
await submit(wrapper)
|
|
131
|
+
|
|
132
|
+
const saved = wrapper.emitted('saved')
|
|
133
|
+
expect(saved).toHaveLength(1)
|
|
134
|
+
expect((saved![0][0] as { payload: ArtifactSavePayload }).payload.kind).toBe('json')
|
|
135
|
+
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([false])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should disable submit when no result payload is provided', async () => {
|
|
139
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json' })
|
|
140
|
+
|
|
141
|
+
await submit(wrapper)
|
|
142
|
+
|
|
143
|
+
expect(saveHandler).not.toHaveBeenCalled()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
// Key validation
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('artifact key validation', () => {
|
|
152
|
+
it('should block submit on an invalid key', async () => {
|
|
153
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 } })
|
|
154
|
+
|
|
155
|
+
await wrapper.get('.mint-artifact-save-dialog__key input').setValue('Bad Key!')
|
|
156
|
+
await submit(wrapper)
|
|
157
|
+
|
|
158
|
+
expect(saveHandler).not.toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should fall back to the default key when the input is emptied', async () => {
|
|
162
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
163
|
+
mode: 'json',
|
|
164
|
+
result: { x: 1 },
|
|
165
|
+
defaultArtifactKey: 'fallback-key',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await wrapper.get('.mint-artifact-save-dialog__key input').setValue('')
|
|
169
|
+
await submit(wrapper)
|
|
170
|
+
|
|
171
|
+
expect(saveHandler.mock.calls[0][0].artifactKey).toBe('fallback-key')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// -------------------------------------------------------------------------
|
|
176
|
+
// File create
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe('file create', () => {
|
|
180
|
+
it('should block submit until a file is chosen', async () => {
|
|
181
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'file' })
|
|
182
|
+
|
|
183
|
+
await submit(wrapper)
|
|
184
|
+
|
|
185
|
+
expect(saveHandler).not.toHaveBeenCalled()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should send a file create payload with the chosen file', async () => {
|
|
189
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'file' })
|
|
190
|
+
|
|
191
|
+
await chooseFile(wrapper)
|
|
192
|
+
await submit(wrapper)
|
|
193
|
+
|
|
194
|
+
expect(saveHandler).toHaveBeenCalledOnce()
|
|
195
|
+
const payload = saveHandler.mock.calls[0][0]
|
|
196
|
+
expect(payload).toMatchObject({
|
|
197
|
+
kind: 'file',
|
|
198
|
+
mode: 'create',
|
|
199
|
+
experimentId: 42,
|
|
200
|
+
artifactKey: 'default',
|
|
201
|
+
fileKind: 'file',
|
|
202
|
+
})
|
|
203
|
+
expect((payload as { file: File }).file).toBe(TEST_FILE)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should use the defaultFileKind prop', async () => {
|
|
207
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'file', defaultFileKind: 'plot' })
|
|
208
|
+
|
|
209
|
+
await chooseFile(wrapper)
|
|
210
|
+
await submit(wrapper)
|
|
211
|
+
|
|
212
|
+
expect((saveHandler.mock.calls[0][0] as { fileKind: string }).fileKind).toBe('plot')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
// Both mode
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
describe('both mode', () => {
|
|
221
|
+
it('should default to the file variant and switch to json via the kind toggle', async () => {
|
|
222
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'both', result: { x: 1 } })
|
|
223
|
+
|
|
224
|
+
expect(wrapper.findComponent(SegmentedControl).exists()).toBe(true)
|
|
225
|
+
expect(wrapper.findComponent(FileUploader).exists()).toBe(true)
|
|
226
|
+
|
|
227
|
+
wrapper.findComponent(SegmentedControl).vm.$emit('update:modelValue', 'json')
|
|
228
|
+
await flushPromises()
|
|
229
|
+
await submit(wrapper)
|
|
230
|
+
|
|
231
|
+
expect(saveHandler.mock.calls[0][0].kind).toBe('json')
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// -------------------------------------------------------------------------
|
|
236
|
+
// Key collisions
|
|
237
|
+
// -------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
describe('key collisions', () => {
|
|
240
|
+
// The backend upserts a JSON artifact by key, but only sets display_name/note
|
|
241
|
+
// when provided — the warning must say so rather than promise a clean replace.
|
|
242
|
+
it('should warn and send upsert for a json save when the key exists', async () => {
|
|
243
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
244
|
+
mode: 'json',
|
|
245
|
+
result: { x: 1 },
|
|
246
|
+
existingArtifactKeys: ['default'],
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
expect(wrapper.get('.mint-artifact-save-dialog__warning').text()).toContain('kept unless')
|
|
250
|
+
await submit(wrapper)
|
|
251
|
+
|
|
252
|
+
expect(saveHandler.mock.calls[0][0].mode).toBe('upsert')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// save_analysis_file_artifact is create-only: a colliding key always raises
|
|
256
|
+
// ConflictException, and the dialog lacks the detail needed to replace safely.
|
|
257
|
+
it('should block a file save when the key exists', async () => {
|
|
258
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
259
|
+
mode: 'file',
|
|
260
|
+
existingArtifactKeys: ['default'],
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await chooseFile(wrapper)
|
|
264
|
+
await submit(wrapper)
|
|
265
|
+
|
|
266
|
+
expect(saveHandler).not.toHaveBeenCalled()
|
|
267
|
+
expect(wrapper.get('.mint-artifact-save-dialog__key').text()).toContain('already used')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should not warn when the key is unique', async () => {
|
|
271
|
+
const { wrapper } = mountDialog({
|
|
272
|
+
mode: 'json',
|
|
273
|
+
result: { x: 1 },
|
|
274
|
+
existingArtifactKeys: ['other'],
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
expect(wrapper.find('.mint-artifact-save-dialog__warning').exists()).toBe(false)
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
// Update mode (existingArtifact)
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
describe('update mode', () => {
|
|
286
|
+
it('should lock the artifact key to the existing artifact', async () => {
|
|
287
|
+
const { wrapper } = mountDialog({ result: { x: 1 }, existingArtifact: makeDetail() })
|
|
288
|
+
|
|
289
|
+
const keyInput = wrapper.get('.mint-artifact-save-dialog__key input')
|
|
290
|
+
expect((keyInput.element as HTMLInputElement).value).toBe('fit')
|
|
291
|
+
expect(keyInput.attributes('readonly')).toBeDefined()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should upsert a json artifact preserving its display name', async () => {
|
|
295
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
296
|
+
result: { x: 2 },
|
|
297
|
+
existingArtifact: makeDetail(),
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
await submit(wrapper)
|
|
301
|
+
|
|
302
|
+
expect(saveHandler.mock.calls[0][0]).toMatchObject({
|
|
303
|
+
kind: 'json',
|
|
304
|
+
mode: 'upsert',
|
|
305
|
+
artifactKey: 'fit',
|
|
306
|
+
displayName: 'Dose Response Fit',
|
|
307
|
+
result: { x: 2 },
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// save_analysis_artifact ignores note=None (preserves), so it cannot clear a
|
|
312
|
+
// note; offering the field here would silently discard the user's edit.
|
|
313
|
+
it('should hide the note field when updating a json artifact', async () => {
|
|
314
|
+
const { wrapper } = mountDialog({ result: { x: 2 }, existingArtifact: makeDetail() })
|
|
315
|
+
|
|
316
|
+
expect(wrapper.find('.mint-artifact-save-dialog__note').exists()).toBe(false)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should never send a note on a json upsert', async () => {
|
|
320
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
321
|
+
result: { x: 2 },
|
|
322
|
+
existingArtifact: makeDetail({ note: 'baseline run' }),
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
await submit(wrapper)
|
|
326
|
+
|
|
327
|
+
expect('note' in saveHandler.mock.calls[0][0]).toBe(false)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should keep the note field for file artifacts, which support tri-state notes', async () => {
|
|
331
|
+
const { wrapper } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
332
|
+
|
|
333
|
+
expect(wrapper.find('.mint-artifact-save-dialog__note').exists()).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should send update-file payload with the immutable file kind', async () => {
|
|
337
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
338
|
+
|
|
339
|
+
await chooseFile(wrapper)
|
|
340
|
+
await submit(wrapper)
|
|
341
|
+
|
|
342
|
+
expect(saveHandler.mock.calls[0][0]).toMatchObject({
|
|
343
|
+
kind: 'file',
|
|
344
|
+
mode: 'update',
|
|
345
|
+
artifactKey: 'report',
|
|
346
|
+
fileKind: 'report',
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// update_analysis_file_artifact REPLACES result metadata rather than merging,
|
|
351
|
+
// so omitting it would wipe whatever the artifact already carried.
|
|
352
|
+
it('should carry the existing result metadata through a file replacement', async () => {
|
|
353
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
354
|
+
existingArtifact: makeFileDetail({
|
|
355
|
+
result: {
|
|
356
|
+
schema_version: ANALYSIS_FILE_ARTIFACT_SCHEMA,
|
|
357
|
+
kind: 'report',
|
|
358
|
+
filename: 'qc-report.pdf',
|
|
359
|
+
file: {},
|
|
360
|
+
metadata: { instrument: 'QE-HF', replicate: 2, passed: true },
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
await chooseFile(wrapper)
|
|
366
|
+
await submit(wrapper)
|
|
367
|
+
|
|
368
|
+
expect(saveHandler.mock.calls[0][0]).toMatchObject({
|
|
369
|
+
metadata: { instrument: 'QE-HF', replicate: 2, passed: true },
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('should omit metadata when the artifact has none', async () => {
|
|
374
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
375
|
+
|
|
376
|
+
await chooseFile(wrapper)
|
|
377
|
+
await submit(wrapper)
|
|
378
|
+
|
|
379
|
+
expect('metadata' in saveHandler.mock.calls[0][0]).toBe(false)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('should let a metadata prop override the carried-through metadata', async () => {
|
|
383
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
384
|
+
existingArtifact: makeFileDetail({
|
|
385
|
+
result: {
|
|
386
|
+
schema_version: ANALYSIS_FILE_ARTIFACT_SCHEMA,
|
|
387
|
+
kind: 'report',
|
|
388
|
+
filename: 'qc-report.pdf',
|
|
389
|
+
file: {},
|
|
390
|
+
metadata: { stale: true },
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
metadata: { fresh: 'yes' },
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
await chooseFile(wrapper)
|
|
397
|
+
await submit(wrapper)
|
|
398
|
+
|
|
399
|
+
expect(saveHandler.mock.calls[0][0]).toMatchObject({ metadata: { fresh: 'yes' } })
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('should require a replacement file for file artifacts', async () => {
|
|
403
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
404
|
+
|
|
405
|
+
await submit(wrapper)
|
|
406
|
+
|
|
407
|
+
expect(saveHandler).not.toHaveBeenCalled()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should omit note when untouched', async () => {
|
|
411
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
412
|
+
|
|
413
|
+
await chooseFile(wrapper)
|
|
414
|
+
await submit(wrapper)
|
|
415
|
+
|
|
416
|
+
expect('note' in saveHandler.mock.calls[0][0]).toBe(false)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('should send null when a previously set note is cleared', async () => {
|
|
420
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
421
|
+
|
|
422
|
+
await wrapper.get('.mint-artifact-save-dialog__note textarea').setValue('')
|
|
423
|
+
await chooseFile(wrapper)
|
|
424
|
+
await submit(wrapper)
|
|
425
|
+
|
|
426
|
+
expect(saveHandler.mock.calls[0][0].note).toBeNull()
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should send the edited note as a string', async () => {
|
|
430
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
431
|
+
|
|
432
|
+
await wrapper.get('.mint-artifact-save-dialog__note textarea').setValue('re-measured')
|
|
433
|
+
await chooseFile(wrapper)
|
|
434
|
+
await submit(wrapper)
|
|
435
|
+
|
|
436
|
+
expect(saveHandler.mock.calls[0][0].note).toBe('re-measured')
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// -------------------------------------------------------------------------
|
|
441
|
+
// Form lifecycle
|
|
442
|
+
// -------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
describe('form lifecycle', () => {
|
|
445
|
+
it('should keep in-progress edits when the same artifact is re-fetched', async () => {
|
|
446
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
447
|
+
await wrapper.get('.mint-artifact-save-dialog__note textarea').setValue('mid-edit note')
|
|
448
|
+
|
|
449
|
+
// A host poll hands back an equal-but-new detail object.
|
|
450
|
+
await wrapper.setProps({ existingArtifact: makeFileDetail() })
|
|
451
|
+
|
|
452
|
+
expect((wrapper.get('.mint-artifact-save-dialog__note textarea').element as HTMLTextAreaElement).value)
|
|
453
|
+
.toBe('mid-edit note')
|
|
454
|
+
|
|
455
|
+
await chooseFile(wrapper)
|
|
456
|
+
await submit(wrapper)
|
|
457
|
+
expect(saveHandler.mock.calls[0][0].note).toBe('mid-edit note')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('should reset the form when a different artifact is supplied', async () => {
|
|
461
|
+
const { wrapper } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
462
|
+
await wrapper.get('.mint-artifact-save-dialog__note textarea').setValue('stale edit')
|
|
463
|
+
|
|
464
|
+
await wrapper.setProps({
|
|
465
|
+
existingArtifact: makeFileDetail({ id: 9, artifact_key: 'other-report', note: 'fresh' }),
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
expect((wrapper.get('.mint-artifact-save-dialog__key input').element as HTMLInputElement).value)
|
|
469
|
+
.toBe('other-report')
|
|
470
|
+
expect((wrapper.get('.mint-artifact-save-dialog__note textarea').element as HTMLTextAreaElement).value)
|
|
471
|
+
.toBe('fresh')
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// -------------------------------------------------------------------------
|
|
476
|
+
// Error handling
|
|
477
|
+
// -------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
describe('error handling', () => {
|
|
480
|
+
it('should stay open, render the error, and emit error when the handler rejects', async () => {
|
|
481
|
+
const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 } })
|
|
482
|
+
saveHandler.mockRejectedValueOnce(new Error('Backend unavailable'))
|
|
483
|
+
|
|
484
|
+
await submit(wrapper)
|
|
485
|
+
|
|
486
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
487
|
+
expect(wrapper.emitted('error')).toEqual([['Backend unavailable']])
|
|
488
|
+
expect(wrapper.get('.mint-artifact-save-dialog__error').text()).toContain('Backend unavailable')
|
|
489
|
+
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('should surface cleanupPending from the handler result in saved', async () => {
|
|
493
|
+
const { wrapper, saveHandler } = mountDialog({ existingArtifact: makeFileDetail() })
|
|
494
|
+
saveHandler.mockResolvedValueOnce({ cleanupPending: true })
|
|
495
|
+
|
|
496
|
+
await chooseFile(wrapper)
|
|
497
|
+
await submit(wrapper)
|
|
498
|
+
|
|
499
|
+
const saved = wrapper.emitted('saved')![0][0] as { cleanupPending?: boolean }
|
|
500
|
+
expect(saved.cleanupPending).toBe(true)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('should set an error when no experiment id can be resolved', async () => {
|
|
504
|
+
const { wrapper, saveHandler } = mountDialog({
|
|
505
|
+
mode: 'json',
|
|
506
|
+
result: { x: 1 },
|
|
507
|
+
experimentId: null,
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
await submit(wrapper)
|
|
511
|
+
|
|
512
|
+
expect(saveHandler).not.toHaveBeenCalled()
|
|
513
|
+
expect(wrapper.find('.mint-artifact-save-dialog__error').exists()).toBe(true)
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
})
|