@ditojs/admin 2.100.0 → 3.0.0
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/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3421 -2662
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +81 -45
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +142 -95
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- package/src/utils/schema.js +0 -1121
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
DitoInput.dito-text(
|
|
3
|
-
:id="
|
|
3
|
+
:id="componentPath"
|
|
4
4
|
ref="element"
|
|
5
5
|
v-model="inputValue"
|
|
6
6
|
:type="inputType"
|
|
@@ -54,6 +54,18 @@ export default DitoTypeComponent.register(
|
|
|
54
54
|
textField: true,
|
|
55
55
|
ignoreMissingValue: ({ schema }) => schema.type === 'password',
|
|
56
56
|
|
|
57
|
+
getTypeValidations(schema) {
|
|
58
|
+
const validationName = {
|
|
59
|
+
email: 'email',
|
|
60
|
+
url: 'url',
|
|
61
|
+
hostname: 'hostname',
|
|
62
|
+
domain: 'domain',
|
|
63
|
+
password: 'password',
|
|
64
|
+
creditcard: 'creditcard'
|
|
65
|
+
}[schema.type]
|
|
66
|
+
return validationName ? { [validationName]: true } : {}
|
|
67
|
+
},
|
|
68
|
+
|
|
57
69
|
computed: {
|
|
58
70
|
inputType() {
|
|
59
71
|
return (
|
|
@@ -81,20 +93,6 @@ export default DitoTypeComponent.register(
|
|
|
81
93
|
this.value = value
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
methods: {
|
|
87
|
-
getValidations() {
|
|
88
|
-
const rule = {
|
|
89
|
-
email: 'email',
|
|
90
|
-
url: 'url',
|
|
91
|
-
hostname: 'hostname',
|
|
92
|
-
domain: 'domain',
|
|
93
|
-
password: 'password',
|
|
94
|
-
creditcard: 'creditcard'
|
|
95
|
-
}[this.type]
|
|
96
|
-
return rule ? { [rule]: true } : {}
|
|
97
|
-
}
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
)
|
|
@@ -18,17 +18,22 @@
|
|
|
18
18
|
v-if="editPath && hasEditableForms"
|
|
19
19
|
)
|
|
20
20
|
//- Include a router-view for the optional DitoFormInlined
|
|
21
|
-
RouterView
|
|
21
|
+
RouterView(
|
|
22
|
+
v-slot="{ Component, route }"
|
|
23
|
+
)
|
|
24
|
+
component(
|
|
25
|
+
v-if="Component"
|
|
26
|
+
:is="Component"
|
|
27
|
+
:key="getRouteComponentKey(route, routeComponent.routeLevel + 1)"
|
|
28
|
+
)
|
|
22
29
|
</template>
|
|
23
30
|
|
|
24
31
|
<script>
|
|
25
32
|
import DitoTypeComponent from '../DitoTypeComponent.js'
|
|
26
33
|
import SourceMixin from '../mixins/SourceMixin.js'
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
resolveSchemaComponents
|
|
31
|
-
} from '../utils/schema.js'
|
|
34
|
+
import { resolveSchemaComponents } from '../utils/schema/setup.js'
|
|
35
|
+
import { hasFormSchema, getFormSchemas } from '../utils/schema/lookup.js'
|
|
36
|
+
import { getParentDataPath } from '../utils/data.js'
|
|
32
37
|
|
|
33
38
|
export default DitoTypeComponent.register(
|
|
34
39
|
['tree-list', 'tree-object'],
|
|
@@ -66,7 +71,7 @@ export default DitoTypeComponent.register(
|
|
|
66
71
|
// Remove `name` from `dataPath`, as it is added
|
|
67
72
|
// to `treeData` and `treeSchema`
|
|
68
73
|
return this.isListSource
|
|
69
|
-
?
|
|
74
|
+
? getParentDataPath(this.dataPath)
|
|
70
75
|
: this.dataPath
|
|
71
76
|
},
|
|
72
77
|
|
|
@@ -138,9 +138,8 @@
|
|
|
138
138
|
import DitoTypeComponent from '../DitoTypeComponent.js'
|
|
139
139
|
import DitoContext from '../DitoContext.js'
|
|
140
140
|
import SortableMixin from '../mixins/SortableMixin.js'
|
|
141
|
-
import parseFileSize from 'filesize-parser'
|
|
142
141
|
import { getSchemaAccessor } from '../utils/accessor.js'
|
|
143
|
-
import { formatFileSize } from '../utils/units.js'
|
|
142
|
+
import { formatFileSize, parseFileSize } from '../utils/units.js'
|
|
144
143
|
import { appendDataPath } from '../utils/data.js'
|
|
145
144
|
import { isArray, asArray } from '@ditojs/utils'
|
|
146
145
|
import VueUpload from 'vue-upload-component'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { computed, shallowRef } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
isObject,
|
|
4
|
+
isFunction,
|
|
5
|
+
isPromise,
|
|
6
|
+
normalizeDataPath,
|
|
7
|
+
getValueAtDataPath
|
|
8
|
+
} from '@ditojs/utils'
|
|
9
|
+
|
|
10
|
+
// DataSchemaResolver resolves a data schema, `{ data, dataPath }` as used by
|
|
11
|
+
// `schema.options` and by the `computed` types, to its value, lazily: Nothing
|
|
12
|
+
// is evaluated or loaded before `value` or `isLoading` is read.
|
|
13
|
+
//
|
|
14
|
+
// - `data` is the value itself, or a function that returns the value, a
|
|
15
|
+
// promise of it, or a function that loads it. In the last case, `data()`
|
|
16
|
+
// only tracks the dependencies and the returned function loads the value,
|
|
17
|
+
// e.g. `data: ({ item: { id } }) => async () => load(id)`, so that loading
|
|
18
|
+
// only starts again when these dependencies change.
|
|
19
|
+
// - `dataPath` is resolved relative to the data path of the context, in its
|
|
20
|
+
// root data, e.g. `'../fontFamily/fontShopSets'`.
|
|
21
|
+
// - Data schemas that aren't objects are values, e.g. `options: ['A', 'B']`.
|
|
22
|
+
//
|
|
23
|
+
// While a value is loading, `value` is `undefined` and `isLoading` is `true`.
|
|
24
|
+
// If loading fails, the error is logged and kept in `lastLoadError`. Results
|
|
25
|
+
// of outdated loads are discarded. Resolved values are returned as they are, so
|
|
26
|
+
// that their object identity is kept.
|
|
27
|
+
//
|
|
28
|
+
// `createContext()` returns the `DitoContext` that `data()` and the loading
|
|
29
|
+
// functions are called with. `onLoadStart(promise)` is called with the promise
|
|
30
|
+
// of each load, which resolves once the load finished, see `finishLoad()`.
|
|
31
|
+
|
|
32
|
+
export class DataSchemaResolver {
|
|
33
|
+
// The promise or loading function of the current load, see `startLoad()`:
|
|
34
|
+
currentPromiseOrLoader = null
|
|
35
|
+
// The promise or loading function of the last finished load, with its value
|
|
36
|
+
// or error, see `finishLoad()`:
|
|
37
|
+
lastFinishedLoad = shallowRef({
|
|
38
|
+
promiseOrLoader: null,
|
|
39
|
+
value: undefined,
|
|
40
|
+
error: null
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
constructor(dataSchema, { createContext, onLoadStart = null }) {
|
|
44
|
+
this.dataSchema = isObject(dataSchema) ? dataSchema : { data: dataSchema }
|
|
45
|
+
this.createContext = createContext
|
|
46
|
+
this.onLoadStart = onLoadStart
|
|
47
|
+
this.valueOrPromiseOrLoader = computed(() =>
|
|
48
|
+
this.getValueOrPromiseOrLoader()
|
|
49
|
+
)
|
|
50
|
+
this.valueOrUndefinedWhileLoading = computed(() =>
|
|
51
|
+
this.getValueOrStartLoad()
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get value() {
|
|
56
|
+
return this.valueOrUndefinedWhileLoading.value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get isLoading() {
|
|
60
|
+
// Read `value` first, so that its loading starts:
|
|
61
|
+
if (this.value !== undefined) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
const valueOrPromiseOrLoader = this.valueOrPromiseOrLoader.value
|
|
65
|
+
return (
|
|
66
|
+
isPromiseOrLoader(valueOrPromiseOrLoader) &&
|
|
67
|
+
this.lastFinishedLoad.value.promiseOrLoader !== valueOrPromiseOrLoader
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The error of the last load, if it failed and isn't outdated:
|
|
72
|
+
get lastLoadError() {
|
|
73
|
+
const { promiseOrLoader, error } = this.lastFinishedLoad.value
|
|
74
|
+
return promiseOrLoader === this.valueOrPromiseOrLoader.value ? error : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Returns the value of the data schema, a promise of it, or a function that
|
|
78
|
+
// loads it. Called by a computed property, so that it only runs again when
|
|
79
|
+
// its dependencies change.
|
|
80
|
+
getValueOrPromiseOrLoader() {
|
|
81
|
+
const { data, dataPath } = this.dataSchema
|
|
82
|
+
if (data !== undefined) {
|
|
83
|
+
return isFunction(data) ? data(this.createContext()) : data
|
|
84
|
+
} else if (dataPath) {
|
|
85
|
+
const context = this.createContext()
|
|
86
|
+
// Data that isn't there, e.g. after it was reset by code, has no value,
|
|
87
|
+
// the same as a missing value at an existing data path.
|
|
88
|
+
return getValueAtDataPath(
|
|
89
|
+
context.rootItem,
|
|
90
|
+
normalizeDataPath(`${context.dataPath}/${dataPath}`),
|
|
91
|
+
() => undefined
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Returns the value of the data schema, or its loaded value once the load
|
|
97
|
+
// of its promise or loading function finished. Until then, it starts the
|
|
98
|
+
// load if it isn't started yet, and returns `undefined`. Called by a
|
|
99
|
+
// computed property, see `value`.
|
|
100
|
+
getValueOrStartLoad() {
|
|
101
|
+
const valueOrPromiseOrLoader = this.valueOrPromiseOrLoader.value
|
|
102
|
+
if (!isPromiseOrLoader(valueOrPromiseOrLoader)) {
|
|
103
|
+
return valueOrPromiseOrLoader
|
|
104
|
+
}
|
|
105
|
+
const { promiseOrLoader, value } = this.lastFinishedLoad.value
|
|
106
|
+
if (promiseOrLoader === valueOrPromiseOrLoader) {
|
|
107
|
+
return value
|
|
108
|
+
}
|
|
109
|
+
if (this.currentPromiseOrLoader !== valueOrPromiseOrLoader) {
|
|
110
|
+
this.startLoad(valueOrPromiseOrLoader)
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
startLoad(promiseOrLoader) {
|
|
116
|
+
this.currentPromiseOrLoader = promiseOrLoader
|
|
117
|
+
const context = this.createContext()
|
|
118
|
+
// Keeps the result in `lastFinishedLoad`, unless the load is outdated:
|
|
119
|
+
const finishLoad = (value, error = null) => {
|
|
120
|
+
if (this.currentPromiseOrLoader === promiseOrLoader) {
|
|
121
|
+
this.lastFinishedLoad.value = { promiseOrLoader, value, error }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Call loading functions in a microtask, outside of the computed property
|
|
125
|
+
// that started the load, so that they don't add to its dependencies.
|
|
126
|
+
const promise = Promise.resolve()
|
|
127
|
+
.then(() =>
|
|
128
|
+
isFunction(promiseOrLoader) ? promiseOrLoader(context) : promiseOrLoader
|
|
129
|
+
)
|
|
130
|
+
.then(value => finishLoad(value))
|
|
131
|
+
.catch(error => {
|
|
132
|
+
console.error(error)
|
|
133
|
+
finishLoad(undefined, error)
|
|
134
|
+
})
|
|
135
|
+
this.onLoadStart?.(promise)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isPromiseOrLoader(valueOrPromiseOrLoader) {
|
|
140
|
+
return isFunction(valueOrPromiseOrLoader) || isPromise(valueOrPromiseOrLoader)
|
|
141
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
import { reactive, nextTick } from 'vue'
|
|
3
|
+
import { DataSchemaResolver } from './DataSchemaResolver.js'
|
|
4
|
+
|
|
5
|
+
function createResolver(dataSchema, context = {}) {
|
|
6
|
+
const pendingLoads = []
|
|
7
|
+
const resolver = new DataSchemaResolver(dataSchema, {
|
|
8
|
+
createContext: () => context,
|
|
9
|
+
onLoadStart: promise => pendingLoads.push(promise)
|
|
10
|
+
})
|
|
11
|
+
const waitForPendingLoads = () => Promise.all(pendingLoads)
|
|
12
|
+
return { resolver, pendingLoads, waitForPendingLoads }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('DataSchemaResolver', () => {
|
|
16
|
+
it('resolves data schemas that are values', () => {
|
|
17
|
+
const options = ['Small', 'Large']
|
|
18
|
+
const { resolver } = createResolver(options)
|
|
19
|
+
expect(resolver.value).toBe(options)
|
|
20
|
+
expect(resolver.isLoading).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('resolves `data` values and functions', () => {
|
|
24
|
+
const item = reactive({ title: 'Hello' })
|
|
25
|
+
const { resolver } = createResolver(
|
|
26
|
+
{ data: ({ item }) => item.title.toUpperCase() },
|
|
27
|
+
{ item }
|
|
28
|
+
)
|
|
29
|
+
expect(resolver.value).toBe('HELLO')
|
|
30
|
+
item.title = 'Bye'
|
|
31
|
+
expect(resolver.value).toBe('BYE')
|
|
32
|
+
expect(createResolver({ data: 'Static' }).resolver.value).toBe('Static')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('resolves `dataPath` relative to the data path of the context', () => {
|
|
36
|
+
const rootItem = reactive({ sizes: ['S', 'M'], item: {} })
|
|
37
|
+
const { resolver } = createResolver(
|
|
38
|
+
{ dataPath: '../../sizes' },
|
|
39
|
+
{ rootItem, dataPath: 'item/size' }
|
|
40
|
+
)
|
|
41
|
+
expect(resolver.value).toBe(rootItem.sizes)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('evaluates and loads nothing before it is read', async () => {
|
|
45
|
+
const data = vi.fn(() => async () => ['A'])
|
|
46
|
+
const { resolver, pendingLoads } = createResolver({ data })
|
|
47
|
+
await nextTick()
|
|
48
|
+
expect(data).not.toHaveBeenCalled()
|
|
49
|
+
expect(pendingLoads).toHaveLength(0)
|
|
50
|
+
expect(resolver.value).toBe(undefined)
|
|
51
|
+
expect(data).toHaveBeenCalledTimes(1)
|
|
52
|
+
expect(pendingLoads).toHaveLength(1)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('loads values asynchronously, keeping their object identity', async () => {
|
|
56
|
+
const options = [{ id: 1 }]
|
|
57
|
+
const { resolver, waitForPendingLoads } = createResolver({
|
|
58
|
+
data: async () => options
|
|
59
|
+
})
|
|
60
|
+
expect(resolver.value).toBe(undefined)
|
|
61
|
+
expect(resolver.isLoading).toBe(true)
|
|
62
|
+
await waitForPendingLoads()
|
|
63
|
+
expect(resolver.value).toBe(options)
|
|
64
|
+
expect(resolver.isLoading).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('loads again only when the dependencies of `data()` change', async () => {
|
|
68
|
+
const item = reactive({ category: 'news', title: 'Hello' })
|
|
69
|
+
const load = vi.fn(async category => [`${category}-1`])
|
|
70
|
+
const { resolver, waitForPendingLoads } = createResolver(
|
|
71
|
+
{
|
|
72
|
+
data: ({ item }) => {
|
|
73
|
+
const { category } = item
|
|
74
|
+
return async () => load(category)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{ item }
|
|
78
|
+
)
|
|
79
|
+
resolver.value
|
|
80
|
+
await waitForPendingLoads()
|
|
81
|
+
expect(resolver.value).toEqual(['news-1'])
|
|
82
|
+
item.title = 'Changed'
|
|
83
|
+
expect(resolver.value).toEqual(['news-1'])
|
|
84
|
+
item.category = 'sports'
|
|
85
|
+
expect(resolver.value).toBe(undefined)
|
|
86
|
+
await waitForPendingLoads()
|
|
87
|
+
expect(resolver.value).toEqual(['sports-1'])
|
|
88
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('discards the values of outdated loads', async () => {
|
|
92
|
+
const item = reactive({ category: 'news' })
|
|
93
|
+
const resolvers = {}
|
|
94
|
+
const { resolver, waitForPendingLoads } = createResolver(
|
|
95
|
+
{
|
|
96
|
+
data: ({ item }) => {
|
|
97
|
+
const { category } = item
|
|
98
|
+
return () =>
|
|
99
|
+
new Promise(resolve => {
|
|
100
|
+
resolvers[category] = () => resolve(category)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{ item }
|
|
105
|
+
)
|
|
106
|
+
resolver.value
|
|
107
|
+
item.category = 'sports'
|
|
108
|
+
resolver.value
|
|
109
|
+
await nextTick()
|
|
110
|
+
resolvers.sports()
|
|
111
|
+
resolvers.news()
|
|
112
|
+
await waitForPendingLoads()
|
|
113
|
+
expect(resolver.value).toBe('sports')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('keeps the error of the last load in `lastLoadError`', async () => {
|
|
117
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
118
|
+
const error = new Error('Failed')
|
|
119
|
+
const { resolver, waitForPendingLoads } = createResolver({
|
|
120
|
+
data: async () => {
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
resolver.value
|
|
125
|
+
await waitForPendingLoads()
|
|
126
|
+
expect(resolver.value).toBe(undefined)
|
|
127
|
+
expect(resolver.isLoading).toBe(false)
|
|
128
|
+
expect(resolver.lastLoadError).toBe(error)
|
|
129
|
+
expect(console.error).toHaveBeenCalledWith(error)
|
|
130
|
+
vi.restoreAllMocks()
|
|
131
|
+
})
|
|
132
|
+
})
|