@morscherlab/mint-sdk 1.2.0-beta.9 → 1.2.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/README.md +30 -0
- package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
- package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
- package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
- package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
- package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
- package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
- package/dist/Skeleton-jhF9wUkU.js.map +1 -0
- package/dist/__tests__/apiErrors.test.d.ts +1 -0
- package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
- package/dist/__tests__/components/FormField.test.d.ts +1 -0
- package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
- package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
- package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
- package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
- package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
- package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
- package/dist/apiErrors.d.ts +26 -0
- package/dist/components/AppSidebar.vue.d.ts +7 -7
- package/dist/components/BaseInput.vue.d.ts +1 -1
- package/dist/components/BasePill.vue.d.ts +3 -0
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseTabs.vue.d.ts +1 -4
- package/dist/components/BaseTextarea.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +1 -1
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +1 -1
- package/dist/components/FileBrowserModal.vue.d.ts +99 -0
- package/dist/components/FilePicker.vue.d.ts +32 -0
- package/dist/components/FileUploader.vue.d.ts +8 -2
- package/dist/components/FormField.vue.d.ts +8 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
- package/dist/components/MobileSupportGate.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlotlyChart.vue.d.ts +59 -0
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SecretInput.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/TimePicker.vue.d.ts +1 -1
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +1 -1
- package/dist/components/fileBrowserPresentation.d.ts +10 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +8 -8
- package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
- package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
- package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
- package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
- package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
- package/dist/{components-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
- package/dist/components-Cp8DcNrU.js.map +1 -0
- package/dist/composables/filePicker/localFiles.d.ts +3 -0
- package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
- package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
- package/dist/composables/filePicker/validation.d.ts +5 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +10 -10
- package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
- package/dist/composables/useApi.d.ts +14 -7
- package/dist/composables/useFileBrowser.d.ts +55 -0
- package/dist/composables/useFilePicker.d.ts +189 -0
- package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
- package/dist/composables/useToast.d.ts +22 -1
- package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
- package/dist/composables-D_4qqOlp.js.map +1 -0
- package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
- package/dist/errors-BLuqMee5.js.map +1 -0
- package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
- package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -15
- package/dist/install.js +2 -2
- package/dist/stores/index.js +2 -2
- package/dist/styles.css +2957 -1708
- package/dist/templates/index.js +2 -2
- package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
- package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
- package/dist/types/components.d.ts +21 -0
- package/dist/types/fileBrowserTypes.d.ts +77 -0
- package/dist/types/filePicker.d.ts +77 -0
- package/dist/types/form-builder.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +2 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
- package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
- package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
- package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
- package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
- package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
- package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
- package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
- package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
- package/dist/validation-CTxuzLIX.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/apiErrors.test.ts +81 -0
- package/src/__tests__/components/AppSidebar.test.ts +84 -94
- package/src/__tests__/components/AppToastContainer.test.ts +55 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
- package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
- package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
- package/src/__tests__/components/FilePicker.state.test.ts +287 -0
- package/src/__tests__/components/FilePicker.test.ts +487 -0
- package/src/__tests__/components/FormBuilder.test.ts +26 -0
- package/src/__tests__/components/FormField.test.ts +26 -0
- package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
- package/src/__tests__/components/NumberInput.test.ts +105 -0
- package/src/__tests__/components/PlotlyChart.test.ts +305 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
- package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
- package/src/__tests__/components/SegmentedControl.test.ts +19 -0
- package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
- package/src/__tests__/composables/localFiles.test.ts +57 -0
- package/src/__tests__/composables/useApi.test.ts +92 -9
- package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
- package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
- package/src/__tests__/composables/usePluginClient.test.ts +3 -0
- package/src/__tests__/generatedUi.test.ts +111 -0
- package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
- package/src/apiErrors.ts +85 -0
- package/src/components/AppLayout.story.vue +1 -1
- package/src/components/AppSidebar.story.vue +7 -10
- package/src/components/AppSidebar.vue +178 -58
- package/src/components/AppToastContainer.vue +65 -23
- package/src/components/BasePill.story.vue +2 -0
- package/src/components/BasePill.vue +4 -0
- package/src/components/BaseTabs.story.vue +0 -42
- package/src/components/BaseTabs.vue +3 -6
- package/src/components/ExperimentPopover.vue +10 -2
- package/src/components/FileBrowserModal.story.vue +203 -0
- package/src/components/FileBrowserModal.vue +384 -0
- package/src/components/FilePicker.story.vue +44 -0
- package/src/components/FilePicker.vue +284 -0
- package/src/components/FileUploader.vue +42 -15
- package/src/components/FormField.vue +10 -3
- package/src/components/GeneratedPathInput.vue +11 -7
- package/src/components/GeneratedPlotlyResult.vue +34 -69
- package/src/components/GeneratedResultRenderer.vue +0 -5
- package/src/components/NumberInput.story.vue +32 -13
- package/src/components/NumberInput.vue +116 -55
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- package/src/components/PluginWorkspaceView.props.ts +3 -3
- package/src/components/SegmentedControl.vue +2 -2
- package/src/components/fileBrowserPresentation.ts +39 -0
- package/src/components/index.ts +5 -0
- package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
- package/src/components/internal/FileBrowserListingInternal.vue +197 -0
- package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
- package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
- package/src/components/internal/FilePickerTreeInternal.vue +238 -0
- package/src/components/internal/FormFieldRendererInternal.vue +17 -0
- package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
- package/src/composables/filePicker/localFiles.ts +65 -0
- package/src/composables/filePicker/usePickerNavigation.ts +220 -0
- package/src/composables/filePicker/usePickerSelection.ts +270 -0
- package/src/composables/filePicker/validation.ts +41 -0
- package/src/composables/index.ts +7 -0
- package/src/composables/pluginWorkspaceModel.ts +0 -3
- package/src/composables/useApi.ts +57 -13
- package/src/composables/useFileBrowser.ts +314 -0
- package/src/composables/useFilePicker.ts +366 -0
- package/src/composables/useGeneratedAnalysis.ts +10 -17
- package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
- package/src/composables/usePluginClient.ts +3 -0
- package/src/composables/usePluginWorkspace.ts +1 -2
- package/src/composables/useToast.ts +21 -5
- package/src/generated/schema.ts +58 -18
- package/src/index.ts +3 -0
- package/src/styles/components/app-sidebar.css +282 -121
- package/src/styles/components/file-browser-modal.css +489 -0
- package/src/styles/components/file-picker.css +502 -0
- package/src/styles/components/form-builder.css +1 -1
- package/src/styles/components/form-field.css +28 -0
- package/src/styles/components/icon-button.css +9 -0
- package/src/styles/components/number-input.css +73 -91
- package/src/styles/components/pill.css +49 -51
- package/src/styles/components/segmented-control.css +16 -11
- package/src/styles/components/tabs.css +18 -34
- package/src/styles/components/toast.css +136 -27
- package/src/styles/index.css +3 -0
- package/src/styles/variables.css +25 -5
- package/src/types/components.ts +23 -0
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/form-builder.ts +2 -0
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +16 -0
- package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
- package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
- package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
- package/dist/components-CgAUzF8F.js.map +0 -1
- package/dist/composables-B_pia3hO.js.map +0 -1
- package/dist/errors-_W0-LbA6.js.map +0 -1
- package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import NumberInput from '../../components/NumberInput.vue'
|
|
4
|
+
|
|
5
|
+
function lastEmitted(wrapper: ReturnType<typeof mount>) {
|
|
6
|
+
return wrapper.emitted('update:modelValue')?.at(-1)?.[0]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('NumberInput', () => {
|
|
10
|
+
it('should never render a range input, with or without min/max', () => {
|
|
11
|
+
for (const props of [{}, { min: 0 }, { max: 10 }, { min: 0, max: 10 }]) {
|
|
12
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5, ...props } })
|
|
13
|
+
expect(wrapper.find('input[type="range"]').exists()).toBe(false)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should draw the range fill only when both min and max are set', () => {
|
|
18
|
+
expect(mount(NumberInput, { props: { modelValue: 5, min: 0 } }).find('.mint-number-input__rule').exists()).toBe(false)
|
|
19
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
|
|
20
|
+
expect(wrapper.find('.mint-number-input__rule').exists()).toBe(true)
|
|
21
|
+
expect(wrapper.find('.mint-number-input__scrub').attributes('style')).toContain('--mint-number-input-fill: 50%')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should expose the scrub area as a slider when min and max are set', () => {
|
|
25
|
+
const scrub = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }).find('.mint-number-input__scrub')
|
|
26
|
+
expect(scrub.attributes('role')).toBe('slider')
|
|
27
|
+
expect(scrub.attributes('aria-valuenow')).toBe('5')
|
|
28
|
+
expect(scrub.attributes('tabindex')).toBe('0')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should always render both steppers', () => {
|
|
32
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5 } })
|
|
33
|
+
expect(wrapper.findAll('.mint-number-input__stepper')).toHaveLength(2)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should disable the steppers at min and max', () => {
|
|
37
|
+
const atMin = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 10 } })
|
|
38
|
+
expect(atMin.find('.mint-number-input__stepper--down').attributes('disabled')).toBeDefined()
|
|
39
|
+
const atMax = mount(NumberInput, { props: { modelValue: 10, min: 0, max: 10 } })
|
|
40
|
+
expect(atMax.find('.mint-number-input__stepper--up').attributes('disabled')).toBeDefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should emit the step-snapped value when dragging at 25% of the scrub width', async () => {
|
|
44
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 50, step: 0.5 } })
|
|
45
|
+
const scrub = wrapper.find('.mint-number-input__scrub')
|
|
46
|
+
scrub.element.getBoundingClientRect = () => ({ left: 100, width: 200 } as DOMRect)
|
|
47
|
+
await scrub.trigger('pointerdown', { clientX: 150, pointerId: 1 })
|
|
48
|
+
expect(lastEmitted(wrapper)).toBe(12.5)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should not scrub when the pointer goes down on the input itself', async () => {
|
|
52
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 50 } })
|
|
53
|
+
await wrapper.find('input').trigger('pointerdown', { clientX: 150, pointerId: 1 })
|
|
54
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should step with the arrow keys', async () => {
|
|
58
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5, step: 2 } })
|
|
59
|
+
await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
|
|
60
|
+
expect(lastEmitted(wrapper)).toBe(7)
|
|
61
|
+
await wrapper.find('input').trigger('keydown', { key: 'ArrowDown' })
|
|
62
|
+
expect(lastEmitted(wrapper)).toBe(3)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should step by ten steps with PageUp and PageDown', async () => {
|
|
66
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 50, step: 1, min: 0, max: 100 } })
|
|
67
|
+
await wrapper.find('input').trigger('keydown', { key: 'PageUp' })
|
|
68
|
+
expect(lastEmitted(wrapper)).toBe(60)
|
|
69
|
+
await wrapper.find('input').trigger('keydown', { key: 'PageDown' })
|
|
70
|
+
expect(lastEmitted(wrapper)).toBe(40)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should jump to min and max with Home and End on the slider surface', async () => {
|
|
74
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
|
|
75
|
+
const scrub = wrapper.find('.mint-number-input__scrub')
|
|
76
|
+
await scrub.trigger('keydown', { key: 'Home' })
|
|
77
|
+
expect(lastEmitted(wrapper)).toBe(0)
|
|
78
|
+
await scrub.trigger('keydown', { key: 'End' })
|
|
79
|
+
expect(lastEmitted(wrapper)).toBe(10)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should leave Home and End to the caret inside the text input', async () => {
|
|
83
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
|
|
84
|
+
await wrapper.find('input').trigger('keydown', { key: 'Home' })
|
|
85
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should emit once per arrow press in slider mode', async () => {
|
|
89
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
|
|
90
|
+
await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
|
|
91
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should keep a fractional value precision when stepping by an integer', async () => {
|
|
95
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 2.5, step: 1 } })
|
|
96
|
+
await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
|
|
97
|
+
expect(lastEmitted(wrapper)).toBe(3.5)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should trim float noise when stepping by a decimal', async () => {
|
|
101
|
+
const wrapper = mount(NumberInput, { props: { modelValue: 0.1, step: 0.2 } })
|
|
102
|
+
await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
|
|
103
|
+
expect(lastEmitted(wrapper)).toBe(0.3)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import PlotlyChart from '../../components/PlotlyChart.vue'
|
|
6
|
+
|
|
7
|
+
const plotly = vi.hoisted(() => ({
|
|
8
|
+
Plots: { resize: vi.fn() },
|
|
9
|
+
purge: vi.fn(),
|
|
10
|
+
react: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('plotly.js-dist-min', () => ({ default: plotly }))
|
|
14
|
+
enableAutoUnmount(afterEach)
|
|
15
|
+
|
|
16
|
+
describe('PlotlyChart', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks()
|
|
19
|
+
plotly.react.mockReset().mockResolvedValue(undefined)
|
|
20
|
+
plotly.Plots.resize.mockReset().mockResolvedValue(undefined)
|
|
21
|
+
document.documentElement.classList.remove('dark')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.unstubAllGlobals()
|
|
26
|
+
document.documentElement.classList.remove('dark')
|
|
27
|
+
document.documentElement.removeAttribute('style')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should merge MINT defaults with native Plotly props', async () => {
|
|
31
|
+
const trace = { type: 'scatter' as const, x: [1], y: [2] }
|
|
32
|
+
mount(PlotlyChart, {
|
|
33
|
+
props: {
|
|
34
|
+
data: [trace],
|
|
35
|
+
layout: { title: { text: 'QC' } },
|
|
36
|
+
config: { toImageButtonOptions: { filename: 'qc-export' } },
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
await flushPromises()
|
|
40
|
+
|
|
41
|
+
expect(plotly.react).toHaveBeenCalledWith(
|
|
42
|
+
expect.any(HTMLElement),
|
|
43
|
+
[trace],
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
autosize: true,
|
|
46
|
+
title: { text: 'QC' },
|
|
47
|
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
|
48
|
+
plot_bgcolor: 'rgba(0,0,0,0)',
|
|
49
|
+
}),
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
responsive: true,
|
|
52
|
+
displaylogo: false,
|
|
53
|
+
toImageButtonOptions: { filename: 'qc-export' },
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should render the shared chart empty state when explicitly empty', async () => {
|
|
59
|
+
const wrapper = mount(PlotlyChart, {
|
|
60
|
+
props: { data: [], empty: true, emptyMessage: 'No peaks found' },
|
|
61
|
+
})
|
|
62
|
+
await flushPromises()
|
|
63
|
+
|
|
64
|
+
expect(wrapper.text()).toContain('No peaks found')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should show a safe error when Plotly rejects rendering', async () => {
|
|
68
|
+
plotly.react.mockRejectedValue(new Error('<script>unsafe details</script>'))
|
|
69
|
+
const wrapper = mount(PlotlyChart, { props: { data: [] } })
|
|
70
|
+
await flushPromises()
|
|
71
|
+
|
|
72
|
+
expect(wrapper.get('[role="alert"]').text()).toBe('Unable to display this chart.')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should rerender when immutable native props change', async () => {
|
|
76
|
+
const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
|
|
77
|
+
await flushPromises()
|
|
78
|
+
|
|
79
|
+
await wrapper.setProps({ data: [{ x: [3], y: [4] }] })
|
|
80
|
+
await flushPromises()
|
|
81
|
+
|
|
82
|
+
expect(plotly.react).toHaveBeenCalledTimes(2)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should rerender when the MINT dark theme changes', async () => {
|
|
86
|
+
let themeChanged: MutationCallback | undefined
|
|
87
|
+
vi.stubGlobal('MutationObserver', class {
|
|
88
|
+
constructor(callback: MutationCallback) {
|
|
89
|
+
themeChanged = callback
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
observe(): void {}
|
|
93
|
+
disconnect(): void {}
|
|
94
|
+
takeRecords(): MutationRecord[] { return [] }
|
|
95
|
+
})
|
|
96
|
+
mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
|
|
97
|
+
await flushPromises()
|
|
98
|
+
|
|
99
|
+
document.documentElement.classList.add('dark')
|
|
100
|
+
themeChanged?.([], {} as MutationObserver)
|
|
101
|
+
await flushPromises()
|
|
102
|
+
|
|
103
|
+
expect(plotly.react).toHaveBeenCalledTimes(2)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should resize the Plotly canvas when its container changes size', async () => {
|
|
107
|
+
let resize: ResizeObserverCallback | undefined
|
|
108
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
109
|
+
constructor(callback: ResizeObserverCallback) {
|
|
110
|
+
resize = callback
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
observe(): void {}
|
|
114
|
+
disconnect(): void {}
|
|
115
|
+
})
|
|
116
|
+
mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
|
|
117
|
+
await flushPromises()
|
|
118
|
+
|
|
119
|
+
resize?.([], {} as ResizeObserver)
|
|
120
|
+
|
|
121
|
+
expect(plotly.Plots.resize).toHaveBeenCalledTimes(1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should observe the Plotly canvas after leaving the empty state', async () => {
|
|
125
|
+
const observe = vi.fn()
|
|
126
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
127
|
+
observe = observe
|
|
128
|
+
disconnect(): void {}
|
|
129
|
+
})
|
|
130
|
+
const wrapper = mount(PlotlyChart, {
|
|
131
|
+
props: { data: [], empty: true },
|
|
132
|
+
})
|
|
133
|
+
await flushPromises()
|
|
134
|
+
|
|
135
|
+
await wrapper.setProps({ empty: false })
|
|
136
|
+
await flushPromises()
|
|
137
|
+
|
|
138
|
+
expect(observe).toHaveBeenCalledWith(expect.any(HTMLElement))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should render once when data arrives as loading completes', async () => {
|
|
142
|
+
const wrapper = mount(PlotlyChart, {
|
|
143
|
+
props: { data: [], loading: true },
|
|
144
|
+
})
|
|
145
|
+
await flushPromises()
|
|
146
|
+
|
|
147
|
+
await wrapper.setProps({ data: [{ x: [1], y: [2] }], loading: false })
|
|
148
|
+
await flushPromises()
|
|
149
|
+
|
|
150
|
+
expect(plotly.react).toHaveBeenCalledTimes(1)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should serialize renders and use the latest props after an in-flight render', async () => {
|
|
154
|
+
let finish!: () => void
|
|
155
|
+
plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
|
|
156
|
+
const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
|
|
157
|
+
await flushPromises()
|
|
158
|
+
await wrapper.setProps({ data: [{ x: [2] }] })
|
|
159
|
+
await wrapper.setProps({ data: [{ x: [3] }] })
|
|
160
|
+
await flushPromises()
|
|
161
|
+
expect(plotly.react).toHaveBeenCalledTimes(1)
|
|
162
|
+
|
|
163
|
+
finish()
|
|
164
|
+
await flushPromises()
|
|
165
|
+
expect(plotly.react).toHaveBeenCalledTimes(2)
|
|
166
|
+
expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [3] }])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should skip rendering when unmounted before the initial DOM update', async () => {
|
|
170
|
+
const wrapper = mount(PlotlyChart, { props: { data: [] } })
|
|
171
|
+
wrapper.unmount()
|
|
172
|
+
await flushPromises()
|
|
173
|
+
expect(plotly.react).not.toHaveBeenCalled()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should recover from a stale failure without overwriting the newer render', async () => {
|
|
177
|
+
let reject!: (error: Error) => void
|
|
178
|
+
plotly.react.mockImplementationOnce(() => new Promise<void>((_resolve, fail) => { reject = fail }))
|
|
179
|
+
const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
|
|
180
|
+
await flushPromises()
|
|
181
|
+
await wrapper.setProps({ data: [{ x: [2] }] })
|
|
182
|
+
reject(new Error('Old render failed'))
|
|
183
|
+
await flushPromises()
|
|
184
|
+
expect(plotly.react).toHaveBeenCalledTimes(2)
|
|
185
|
+
expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [2] }])
|
|
186
|
+
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should purge an in-flight canvas removed by loading and then render its replacement', async () => {
|
|
190
|
+
let finish!: () => void
|
|
191
|
+
plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
|
|
192
|
+
const wrapper = mount(PlotlyChart, { props: { data: [] } })
|
|
193
|
+
await flushPromises()
|
|
194
|
+
const element = wrapper.get('.mint-plotly-chart__plot').element
|
|
195
|
+
await wrapper.setProps({ loading: true })
|
|
196
|
+
await wrapper.setProps({ loading: false, data: [{ x: [3] }] })
|
|
197
|
+
finish()
|
|
198
|
+
await flushPromises()
|
|
199
|
+
expect(plotly.purge).toHaveBeenCalledWith(element)
|
|
200
|
+
expect(plotly.react).toHaveBeenCalledTimes(2)
|
|
201
|
+
expect(plotly.react.mock.lastCall?.[0]).toBe(wrapper.get('.mint-plotly-chart__plot').element)
|
|
202
|
+
expect(plotly.react.mock.lastCall?.[0]).not.toBe(element)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should clean up a render that completes after unmount', async () => {
|
|
206
|
+
let finish!: () => void
|
|
207
|
+
plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
|
|
208
|
+
const wrapper = mount(PlotlyChart, { props: { data: [] } })
|
|
209
|
+
await flushPromises()
|
|
210
|
+
const element = wrapper.get('.mint-plotly-chart__plot').element
|
|
211
|
+
wrapper.unmount()
|
|
212
|
+
plotly.purge.mockClear()
|
|
213
|
+
finish()
|
|
214
|
+
await flushPromises()
|
|
215
|
+
expect(plotly.purge).toHaveBeenCalledWith(element)
|
|
216
|
+
expect(plotly.react).toHaveBeenCalledTimes(1)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should purge the removed canvas and render new data after loading or empty states', async () => {
|
|
220
|
+
const disconnect = vi.fn()
|
|
221
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
222
|
+
observe = vi.fn()
|
|
223
|
+
unobserve = vi.fn()
|
|
224
|
+
disconnect = disconnect
|
|
225
|
+
})
|
|
226
|
+
const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
|
|
227
|
+
await flushPromises()
|
|
228
|
+
for (const state of ['loading', 'empty'] as const) {
|
|
229
|
+
const element = wrapper.get('.mint-plotly-chart__plot').element
|
|
230
|
+
await wrapper.setProps({ [state]: true })
|
|
231
|
+
await flushPromises()
|
|
232
|
+
expect(plotly.purge).toHaveBeenCalledWith(element)
|
|
233
|
+
expect(disconnect).toHaveBeenCalled()
|
|
234
|
+
await wrapper.setProps({ [state]: false, data: [{ x: [2] }] })
|
|
235
|
+
await flushPromises()
|
|
236
|
+
expect(wrapper.get('.mint-plotly-chart__plot').element).not.toBe(element)
|
|
237
|
+
}
|
|
238
|
+
expect(plotly.react).toHaveBeenCalledTimes(3)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should use MINT colors and protect reactive native data and nested layout from Plotly mutations', async () => {
|
|
242
|
+
document.documentElement.style.setProperty('--text-primary', '#f8fafc')
|
|
243
|
+
document.documentElement.style.setProperty('--border-color', '#334155')
|
|
244
|
+
const data = reactive([{ x: [1], y: [2] }])
|
|
245
|
+
const layout = reactive({ title: { text: 'Original' }, xaxis: { title: { text: 'Sample' } } })
|
|
246
|
+
plotly.react.mockImplementationOnce((_element, traces, options) => {
|
|
247
|
+
traces[0].x.push(9)
|
|
248
|
+
options.title.text = 'Changed'
|
|
249
|
+
options.xaxis.title.text = 'Changed'
|
|
250
|
+
return Promise.resolve()
|
|
251
|
+
})
|
|
252
|
+
mount(PlotlyChart, { attachTo: document.body, props: { data, layout } })
|
|
253
|
+
await flushPromises()
|
|
254
|
+
expect(plotly.react.mock.lastCall?.[2].font.color).toBe('#f8fafc')
|
|
255
|
+
expect(plotly.react.mock.lastCall?.[2].xaxis.gridcolor).toBe('#334155')
|
|
256
|
+
expect(data[0].x).toEqual([1])
|
|
257
|
+
expect(layout.title.text).toBe('Original')
|
|
258
|
+
expect(layout.xaxis.title.text).toBe('Sample')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should accept nested reactive arrays, typed arrays, dates, and native export callbacks', async () => {
|
|
262
|
+
const x = reactive([new Date('2026-01-01')])
|
|
263
|
+
const y = new Float64Array([2])
|
|
264
|
+
const click = vi.fn()
|
|
265
|
+
mount(PlotlyChart, {
|
|
266
|
+
props: {
|
|
267
|
+
data: [{ x, y }],
|
|
268
|
+
config: { modeBarButtonsToAdd: [{ name: 'Export', title: 'Export', icon: 'camera', click }] },
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
await flushPromises()
|
|
272
|
+
expect(plotly.react).toHaveBeenCalledOnce()
|
|
273
|
+
const [, traces, , config] = plotly.react.mock.lastCall!
|
|
274
|
+
expect(traces[0].x).toEqual(x)
|
|
275
|
+
expect(traces[0].x).not.toBe(x)
|
|
276
|
+
expect(traces[0].x[0]).toBeInstanceOf(Date)
|
|
277
|
+
expect(traces[0].y).toEqual(y)
|
|
278
|
+
expect(traces[0].y).not.toBe(y)
|
|
279
|
+
expect(config.modeBarButtonsToAdd[0].click).toBe(click)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should ignore resize rejection when the canvas becomes hidden', async () => {
|
|
283
|
+
let resize!: ResizeObserverCallback
|
|
284
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
285
|
+
constructor(callback: ResizeObserverCallback) { resize = callback }
|
|
286
|
+
observe(): void {}
|
|
287
|
+
disconnect(): void {}
|
|
288
|
+
})
|
|
289
|
+
plotly.Plots.resize.mockRejectedValueOnce(new Error('Resize must be passed a displayed plot div'))
|
|
290
|
+
const wrapper = mount(PlotlyChart, { props: { data: [] } })
|
|
291
|
+
await flushPromises()
|
|
292
|
+
resize([], {} as ResizeObserver)
|
|
293
|
+
await flushPromises()
|
|
294
|
+
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should purge the Plotly canvas when unmounted', async () => {
|
|
298
|
+
const wrapper = mount(PlotlyChart, { props: { data: [] } })
|
|
299
|
+
await flushPromises()
|
|
300
|
+
|
|
301
|
+
wrapper.unmount()
|
|
302
|
+
|
|
303
|
+
expect(plotly.purge).toHaveBeenCalledTimes(1)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
@@ -114,9 +114,8 @@ describe('PluginWorkspaceView internal split', () => {
|
|
|
114
114
|
activeView: 'data',
|
|
115
115
|
title: 'Tools',
|
|
116
116
|
subtitle: 'Current file',
|
|
117
|
-
variant: 'analysis',
|
|
118
117
|
floating: false,
|
|
119
|
-
|
|
118
|
+
density: 'compact',
|
|
120
119
|
})
|
|
121
120
|
expect(wrapper.get('[data-test="topbar-action"]').text()).toBe('Run')
|
|
122
121
|
expect(wrapper.get('[data-test="import-action"]').text()).toBe('Upload')
|
|
@@ -182,9 +182,8 @@ describe('PluginWorkspaceView', () => {
|
|
|
182
182
|
activeView: 'data',
|
|
183
183
|
title: 'Tools',
|
|
184
184
|
subtitle: 'Current file',
|
|
185
|
-
variant: 'analysis',
|
|
186
185
|
floating: false,
|
|
187
|
-
|
|
186
|
+
density: 'compact',
|
|
188
187
|
})
|
|
189
188
|
expect(wrapper.get('[data-test="topbar-action"]').text()).toBe('Run')
|
|
190
189
|
expect(wrapper.get('[data-test="import-action"]').text()).toBe('Upload')
|
|
@@ -3,6 +3,25 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import SegmentedControl from '../../components/SegmentedControl.vue'
|
|
4
4
|
|
|
5
5
|
describe('SegmentedControl', () => {
|
|
6
|
+
it('defaults to card while allowing an explicit simple variant', async () => {
|
|
7
|
+
const wrapper = mount(SegmentedControl, {
|
|
8
|
+
props: {
|
|
9
|
+
modelValue: 'table',
|
|
10
|
+
options: [{ value: 'table', label: 'Table', description: 'View rows' }],
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect(wrapper.classes()).toContain('mint-segmented-control--card')
|
|
15
|
+
expect(wrapper.get('button').classes()).toContain('mint-segmented-control__option--card')
|
|
16
|
+
expect(wrapper.get('.mint-segmented-control__description').text()).toBe('View rows')
|
|
17
|
+
|
|
18
|
+
await wrapper.setProps({ variant: 'simple' })
|
|
19
|
+
|
|
20
|
+
expect(wrapper.classes()).toContain('mint-segmented-control--simple')
|
|
21
|
+
expect(wrapper.get('button').classes()).toContain('mint-segmented-control__option--simple')
|
|
22
|
+
expect(wrapper.find('.mint-segmented-control__description').exists()).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
6
25
|
it('accepts string and number shorthand options', async () => {
|
|
7
26
|
const wrapper = mount(SegmentedControl, {
|
|
8
27
|
props: {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
// jsdom has no layout, so this pins the stylesheet contract instead: a bare
|
|
6
|
+
// <svg> passed to IconButton's slot must be sized by the icon box, otherwise it
|
|
7
|
+
// collapses to zero width as a flex item (FilePicker source toggles, beta.14).
|
|
8
|
+
describe('IconButton icon sizing', () => {
|
|
9
|
+
it('should size a bare slot svg to the icon box when the caller passes no dimensions', () => {
|
|
10
|
+
const css = readFileSync(
|
|
11
|
+
resolve(__dirname, '../../styles/components/icon-button.css'),
|
|
12
|
+
'utf8',
|
|
13
|
+
)
|
|
14
|
+
expect(css).toMatch(
|
|
15
|
+
/\.mint-icon-button__icon--sm > svg,\s*\.mint-icon-button__icon--md > svg,\s*\.mint-icon-button__icon--lg > svg \{\s*width: 100%;\s*height: 100%;\s*\}/,
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { localFilesAdapter } from '../../composables/filePicker/localFiles'
|
|
3
|
+
|
|
4
|
+
describe('localFilesAdapter', () => {
|
|
5
|
+
it('preserves order, duplicate files, nested statistics and scoped search', async () => {
|
|
6
|
+
const files = [
|
|
7
|
+
new File(['abc'], 'run.raw'),
|
|
8
|
+
new File(['12'], 'run.raw'),
|
|
9
|
+
new File(['1234'], 'sample.mzML.gz'),
|
|
10
|
+
new File(['x'], 'other.raw'),
|
|
11
|
+
]
|
|
12
|
+
const adapter = localFilesAdapter(files, [
|
|
13
|
+
'study/batch/run.raw', 'study/batch/run.raw', 'study/sample.mzML.gz', 'study2/other.raw',
|
|
14
|
+
])
|
|
15
|
+
expect(await adapter.listRoot()).toMatchObject([
|
|
16
|
+
{ path: 'study', fileCount: 3, bytes: 9, formats: { raw: 2, 'mzML.gz': 1 } },
|
|
17
|
+
{ path: 'study2', fileCount: 1, bytes: 1, formats: { raw: 1 } },
|
|
18
|
+
])
|
|
19
|
+
expect(await adapter.listChildren('study')).toMatchObject([
|
|
20
|
+
{ path: 'study/batch', fileCount: 2, bytes: 5, formats: { raw: 2 } },
|
|
21
|
+
{ path: 'study/sample.mzML.gz', file: files[2] },
|
|
22
|
+
])
|
|
23
|
+
expect(await adapter.listChildren('study/batch')).toMatchObject([
|
|
24
|
+
{ path: 'study/batch/run.raw', file: files[0] },
|
|
25
|
+
{ path: 'study/batch/run.raw (2)', file: files[1] },
|
|
26
|
+
])
|
|
27
|
+
expect(await adapter.search('RUN', { path: 'study', recursive: true })).toHaveLength(2)
|
|
28
|
+
expect(await adapter.search('RUN', { path: 'study', recursive: false })).toEqual([])
|
|
29
|
+
expect(await adapter.listChildren('missing')).toEqual([])
|
|
30
|
+
const roots = await adapter.listRoot()
|
|
31
|
+
roots.pop()
|
|
32
|
+
expect(await adapter.listRoot()).toHaveLength(2)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('builds and lists many sibling folders without repeatedly scanning all nodes', async () => {
|
|
36
|
+
const files = Array.from({ length: 200 }, (_, i) => new File(['x'], `${i}.raw`))
|
|
37
|
+
const paths = files.map((file, i) => `folder-${i}/${file.name}`)
|
|
38
|
+
const originalValues = Map.prototype.values
|
|
39
|
+
let visited = 0
|
|
40
|
+
const values = vi.spyOn(Map.prototype, 'values').mockImplementation(function* (this: Map<unknown, unknown>) {
|
|
41
|
+
for (const value of originalValues.call(this)) {
|
|
42
|
+
visited++
|
|
43
|
+
yield value
|
|
44
|
+
}
|
|
45
|
+
} as typeof Map.prototype.values)
|
|
46
|
+
try {
|
|
47
|
+
const adapter = localFilesAdapter(files, paths)
|
|
48
|
+
await adapter.listRoot()
|
|
49
|
+
for (let i = 0; i < files.length; i++) {
|
|
50
|
+
expect(await adapter.listChildren(`folder-${i}`)).toHaveLength(1)
|
|
51
|
+
}
|
|
52
|
+
expect(visited).toBeLessThanOrEqual(files.length * 8)
|
|
53
|
+
} finally {
|
|
54
|
+
values.mockRestore()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|