@blueprint-ts/core 4.0.0-beta.9 → 4.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/CHANGELOG.md +11 -0
- package/docs/services/pagination/index.md +1 -1
- package/docs/services/pagination/updating-rows.md +16 -0
- package/package.json +1 -1
- package/release-tool.json +1 -1
- package/src/pagination/BasePaginator.ts +19 -0
- package/src/requests/BaseRequest.ts +2 -1
- package/src/vue/forms/validation/rules/EmailRule.ts +1 -1
- package/tests/service/bulkRequests/BulkRequestSender.test.ts +76 -0
- package/tests/service/bulkRequests/BulkRequestWrapper.test.ts +51 -0
- package/tests/service/pagination/BasePaginator.test.ts +100 -0
- package/tests/service/pagination/InfiniteScroller.test.ts +101 -0
- package/tests/service/pagination/PageAwarePaginator.test.ts +133 -0
- package/tests/service/pagination/StatePaginator.test.ts +76 -0
- package/tests/service/pagination/VueViewDrivers.test.ts +46 -0
- package/tests/service/pagination/dtos/StatePaginationDataDto.test.ts +14 -0
- package/tests/service/persistenceDrivers/PersistenceDrivers.test.ts +56 -0
- package/tests/service/requests/BaseRequest.test.ts +199 -0
- package/tests/service/requests/BodiesAndFactories.test.ts +28 -0
- package/tests/service/requests/Enums.test.ts +19 -0
- package/tests/service/requests/ErrorHandler.test.ts +45 -1
- package/tests/service/requests/RequestErrorRouter.test.ts +44 -0
- package/tests/service/requests/Responses.test.ts +83 -0
- package/tests/service/requests/exceptions/Exceptions.test.ts +43 -0
- package/tests/service/requests/fetch/FetchDriver.test.ts +76 -0
- package/tests/service/requests/fetch/FetchResponse.test.ts +21 -0
- package/tests/service/support/DeferredPromise.test.ts +40 -0
- package/tests/service/support/helpers.test.ts +37 -0
- package/tests/vue/composables/useConfirmDialog.test.ts +77 -0
- package/tests/vue/composables/useGlobalCheckbox.test.ts +126 -0
- package/tests/vue/composables/useIsEmpty.test.ts +18 -0
- package/tests/vue/composables/useIsOpen.test.ts +25 -0
- package/tests/vue/composables/useIsOpenFromVar.test.ts +22 -0
- package/tests/vue/composables/useModelWrapper.test.ts +30 -0
- package/tests/vue/composables/useOnOpen.test.ts +26 -0
- package/tests/vue/forms/PropertyAwareArray.test.ts +30 -0
- package/tests/vue/forms/validation/ValidationRules.test.ts +79 -0
- package/tests/vue/requests/VueRequestLoaders.test.ts +48 -0
- package/tests/vue/router/routeResourceBinding/RouteResourceUtils.test.ts +70 -0
- package/tests/vue/state/State.test.ts +151 -0
- package/vitest.config.ts +10 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { FetchResponse } from '../../../../src/requests/drivers/fetch/FetchResponse'
|
|
3
|
+
|
|
4
|
+
describe('FetchResponse', () => {
|
|
5
|
+
it('exposes response data', async () => {
|
|
6
|
+
const response = new Response('{"name":"Ada"}', {
|
|
7
|
+
status: 201,
|
|
8
|
+
headers: { 'X-Test': 'yes' },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const fetchResponse = new FetchResponse(response)
|
|
12
|
+
|
|
13
|
+
expect(fetchResponse.getStatusCode()).toBe(201)
|
|
14
|
+
expect(fetchResponse.getHeaders()).toEqual({
|
|
15
|
+
'content-type': 'text/plain;charset=UTF-8',
|
|
16
|
+
'x-test': 'yes',
|
|
17
|
+
})
|
|
18
|
+
expect(fetchResponse.getRawResponse()).toBe(response)
|
|
19
|
+
await expect(fetchResponse.json()).resolves.toEqual({ name: 'Ada' })
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DeferredPromise } from '../../../src/support/DeferredPromise'
|
|
3
|
+
|
|
4
|
+
describe('DeferredPromise', () => {
|
|
5
|
+
it('resolves and updates state', async () => {
|
|
6
|
+
const deferred = new DeferredPromise<string>()
|
|
7
|
+
|
|
8
|
+
expect(deferred.state).toBe('pending')
|
|
9
|
+
|
|
10
|
+
const resultPromise = deferred.then((value) => `value:${value}`)
|
|
11
|
+
deferred.resolve('ok')
|
|
12
|
+
|
|
13
|
+
await expect(resultPromise).resolves.toBe('value:ok')
|
|
14
|
+
expect(deferred.state).toBe('fulfilled')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('rejects and updates state', async () => {
|
|
18
|
+
const deferred = new DeferredPromise<string>()
|
|
19
|
+
|
|
20
|
+
const resultPromise = deferred.catch((error) => `error:${String(error)}`)
|
|
21
|
+
deferred.reject('boom')
|
|
22
|
+
|
|
23
|
+
await expect(resultPromise).resolves.toBe('error:boom')
|
|
24
|
+
expect(deferred.state).toBe('rejected')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('supports finally', async () => {
|
|
28
|
+
const deferred = new DeferredPromise<string>()
|
|
29
|
+
let ran = false
|
|
30
|
+
|
|
31
|
+
const promise = deferred.finally(() => {
|
|
32
|
+
ran = true
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
deferred.resolve('ok')
|
|
36
|
+
await promise
|
|
37
|
+
|
|
38
|
+
expect(ran).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { getCookie, getDisplayablePages, isAtBottom, isObject } from '../../../src/support/helpers'
|
|
3
|
+
|
|
4
|
+
describe('support helpers', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
document.cookie = ''
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('reads cookies by name and trims spaces', () => {
|
|
10
|
+
document.cookie = 'first=one'
|
|
11
|
+
document.cookie = 'second=two'
|
|
12
|
+
|
|
13
|
+
expect(getCookie('first')).toBe('one')
|
|
14
|
+
expect(getCookie('second')).toBe('two')
|
|
15
|
+
expect(getCookie('missing')).toBe('')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('detects objects and excludes arrays/null', () => {
|
|
19
|
+
expect(isObject({})).toBe(true)
|
|
20
|
+
expect(isObject({ a: 1 })).toBe(true)
|
|
21
|
+
expect(isObject([])).toBe(false)
|
|
22
|
+
expect(isObject(null)).toBe(false)
|
|
23
|
+
expect(isObject(undefined)).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('calculates displayable pages with bounds', () => {
|
|
27
|
+
expect(getDisplayablePages(3, 1, 4)).toEqual([1, 2, 3])
|
|
28
|
+
expect(getDisplayablePages(10, 1, 4)).toEqual([1, 2, 3, 4])
|
|
29
|
+
expect(getDisplayablePages(10, 5, 4)).toEqual([4, 5, 6, 7])
|
|
30
|
+
expect(getDisplayablePages(10, 10, 4)).toEqual([7, 8, 9, 10])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('detects near-bottom scroll positions', () => {
|
|
34
|
+
expect(isAtBottom(100, 97, 3)).toBe(true)
|
|
35
|
+
expect(isAtBottom(100, 90, 3)).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, defineComponent, h } from 'vue'
|
|
3
|
+
import useConfirmDialog, { ConfirmDialogSeverity, type ConfirmDialogOptions } from '../../../src/vue/composables/useConfirmDialog'
|
|
4
|
+
|
|
5
|
+
const options: ConfirmDialogOptions = {
|
|
6
|
+
getMessage: () => 'Are you sure?',
|
|
7
|
+
getSeverity: () => ConfirmDialogSeverity.INFO,
|
|
8
|
+
getTitle: () => 'Confirm',
|
|
9
|
+
getOkText: () => 'OK',
|
|
10
|
+
getCancelText: () => 'Cancel',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('useConfirmDialog', () => {
|
|
14
|
+
it('throws when called outside setup', async () => {
|
|
15
|
+
const Dialog = defineComponent({ setup: () => () => h('div') })
|
|
16
|
+
const { openConfirmDialog } = useConfirmDialog(Dialog, '#dialog')
|
|
17
|
+
|
|
18
|
+
await expect(openConfirmDialog(options)).rejects.toThrow('useConfirmDialog must be called inside a setup function')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('opens dialog and returns exposed result', async () => {
|
|
22
|
+
const dialogTarget = document.createElement('div')
|
|
23
|
+
dialogTarget.id = 'dialog'
|
|
24
|
+
document.body.appendChild(dialogTarget)
|
|
25
|
+
|
|
26
|
+
const Dialog = defineComponent({
|
|
27
|
+
setup(_, { expose }) {
|
|
28
|
+
expose({ open: Promise.resolve(true) })
|
|
29
|
+
return () => h('div')
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
let openConfirmDialog: (opts: ConfirmDialogOptions) => Promise<boolean>
|
|
34
|
+
|
|
35
|
+
const App = defineComponent({
|
|
36
|
+
setup() {
|
|
37
|
+
;({ openConfirmDialog } = useConfirmDialog(Dialog, '#dialog'))
|
|
38
|
+
return () => h('div')
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const root = document.createElement('div')
|
|
43
|
+
document.body.appendChild(root)
|
|
44
|
+
|
|
45
|
+
createApp(App).mount(root)
|
|
46
|
+
|
|
47
|
+
await expect(openConfirmDialog(options)).resolves.toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('throws when dialog does not expose open', async () => {
|
|
51
|
+
const dialogTarget = document.createElement('div')
|
|
52
|
+
dialogTarget.id = 'dialog-2'
|
|
53
|
+
document.body.appendChild(dialogTarget)
|
|
54
|
+
|
|
55
|
+
const Dialog = defineComponent({
|
|
56
|
+
setup() {
|
|
57
|
+
return () => h('div')
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let openConfirmDialog: (opts: ConfirmDialogOptions) => Promise<boolean>
|
|
62
|
+
|
|
63
|
+
const App = defineComponent({
|
|
64
|
+
setup() {
|
|
65
|
+
;({ openConfirmDialog } = useConfirmDialog(Dialog, '#dialog-2'))
|
|
66
|
+
return () => h('div')
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const root = document.createElement('div')
|
|
71
|
+
document.body.appendChild(root)
|
|
72
|
+
|
|
73
|
+
createApp(App).mount(root)
|
|
74
|
+
|
|
75
|
+
await expect(openConfirmDialog(options)).rejects.toThrow('Provided component does not expose an "open" method')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createApp, defineComponent, h, type Component } from 'vue'
|
|
3
|
+
import useGlobalCheckbox from '../../../src/vue/composables/useGlobalCheckbox'
|
|
4
|
+
|
|
5
|
+
const setupComposable = <T>(dialog: Component, options: { getAll: () => Promise<T[]>; getPage: () => T[]; totalCount: () => number }, targetId: string) => {
|
|
6
|
+
const dialogTarget = document.createElement('div')
|
|
7
|
+
dialogTarget.id = targetId
|
|
8
|
+
document.body.appendChild(dialogTarget)
|
|
9
|
+
|
|
10
|
+
let api: ReturnType<typeof useGlobalCheckbox<T>> | undefined
|
|
11
|
+
|
|
12
|
+
const App = defineComponent({
|
|
13
|
+
setup() {
|
|
14
|
+
api = useGlobalCheckbox<T>(dialog, options, `#${targetId}`)
|
|
15
|
+
return () => h('div')
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const root = document.createElement('div')
|
|
20
|
+
document.body.appendChild(root)
|
|
21
|
+
createApp(App).mount(root)
|
|
22
|
+
|
|
23
|
+
return api!
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const createEvent = () => {
|
|
27
|
+
const input = document.createElement('input')
|
|
28
|
+
const event = new Event('change') as Event & { target: HTMLInputElement; preventDefault: () => void }
|
|
29
|
+
Object.defineProperty(event, 'target', { value: input })
|
|
30
|
+
event.preventDefault = vi.fn()
|
|
31
|
+
return event
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('useGlobalCheckbox', () => {
|
|
35
|
+
it('selects current page when all elements are on page', async () => {
|
|
36
|
+
const Dialog = defineComponent({
|
|
37
|
+
setup(_, { expose }) {
|
|
38
|
+
expose({ open: Promise.resolve(true) })
|
|
39
|
+
return () => h('div')
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const options = {
|
|
44
|
+
getAll: vi.fn().mockResolvedValue([1, 2]),
|
|
45
|
+
getPage: vi.fn().mockReturnValue([1, 2]),
|
|
46
|
+
totalCount: vi.fn().mockReturnValue(2),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const api = setupComposable<number>(Dialog, options, 'dialog-all')
|
|
50
|
+
|
|
51
|
+
await api.handleGlobalCheckboxChange(createEvent())
|
|
52
|
+
|
|
53
|
+
expect(api.selectedRows.value).toEqual([1, 2])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('opens dialog to select all when not all elements are on page', async () => {
|
|
57
|
+
const Dialog = defineComponent({
|
|
58
|
+
setup(_, { expose }) {
|
|
59
|
+
expose({ open: Promise.resolve(true) })
|
|
60
|
+
return () => h('div')
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const options = {
|
|
65
|
+
getAll: vi.fn().mockResolvedValue([1, 2, 3]),
|
|
66
|
+
getPage: vi.fn().mockReturnValue([1]),
|
|
67
|
+
totalCount: vi.fn().mockReturnValue(3),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const api = setupComposable<number>(Dialog, options, 'dialog-select-all')
|
|
71
|
+
const event = createEvent()
|
|
72
|
+
|
|
73
|
+
await api.handleGlobalCheckboxChange(event)
|
|
74
|
+
|
|
75
|
+
expect(event.preventDefault).toHaveBeenCalled()
|
|
76
|
+
expect(api.selectedRows.value).toEqual([1, 2, 3])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('clears selection on indeterminate cancel', async () => {
|
|
80
|
+
const Dialog = defineComponent({
|
|
81
|
+
setup(_, { expose }) {
|
|
82
|
+
expose({ open: Promise.resolve(false) })
|
|
83
|
+
return () => h('div')
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const options = {
|
|
88
|
+
getAll: vi.fn().mockResolvedValue([1, 2, 3]),
|
|
89
|
+
getPage: vi.fn().mockReturnValue([1]),
|
|
90
|
+
totalCount: vi.fn().mockReturnValue(3),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const api = setupComposable<number>(Dialog, options, 'dialog-cancel')
|
|
94
|
+
api.selectedRows.value = [1]
|
|
95
|
+
|
|
96
|
+
await api.handleGlobalCheckboxChange(createEvent())
|
|
97
|
+
|
|
98
|
+
expect(api.selectedRows.value).toEqual([])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('clears selection when already checked', async () => {
|
|
102
|
+
const Dialog = defineComponent({
|
|
103
|
+
setup(_, { expose }) {
|
|
104
|
+
expose({ open: Promise.resolve(true) })
|
|
105
|
+
return () => h('div')
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const options = {
|
|
110
|
+
getAll: vi.fn().mockResolvedValue([1, 2]),
|
|
111
|
+
getPage: vi.fn().mockReturnValue([1, 2]),
|
|
112
|
+
totalCount: vi.fn().mockReturnValue(2),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const api = setupComposable<number>(Dialog, options, 'dialog-checked')
|
|
116
|
+
api.selectedRows.value = [1, 2]
|
|
117
|
+
|
|
118
|
+
await api.handleGlobalCheckboxChange(createEvent())
|
|
119
|
+
|
|
120
|
+
expect(api.selectedRows.value).toEqual([])
|
|
121
|
+
|
|
122
|
+
api.selectedRows.value = [1]
|
|
123
|
+
api.checked.value = false
|
|
124
|
+
expect(api.selectedRows.value).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import useIsEmpty from '../../../src/vue/composables/useIsEmpty'
|
|
3
|
+
|
|
4
|
+
describe('useIsEmpty', () => {
|
|
5
|
+
it('detects empty values', () => {
|
|
6
|
+
const { isEmpty, isNotEmpty } = useIsEmpty()
|
|
7
|
+
|
|
8
|
+
expect(isEmpty(undefined)).toBe(true)
|
|
9
|
+
expect(isEmpty(null)).toBe(true)
|
|
10
|
+
expect(isEmpty('')).toBe(true)
|
|
11
|
+
expect(isEmpty([])).toBe(true)
|
|
12
|
+
expect(isEmpty({})).toBe(true)
|
|
13
|
+
|
|
14
|
+
expect(isNotEmpty('a')).toBe(true)
|
|
15
|
+
expect(isNotEmpty([1])).toBe(true)
|
|
16
|
+
expect(isNotEmpty({ a: 1 })).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import useIsOpen from '../../../src/vue/composables/useIsOpen'
|
|
3
|
+
|
|
4
|
+
describe('useIsOpen', () => {
|
|
5
|
+
it('tracks open state and increments key after delay', () => {
|
|
6
|
+
vi.useFakeTimers()
|
|
7
|
+
|
|
8
|
+
const callback = vi.fn()
|
|
9
|
+
const { isOpen, isOpenKey } = useIsOpen(callback, 50)
|
|
10
|
+
|
|
11
|
+
isOpen.value = true
|
|
12
|
+
expect(isOpen.value).toBe(true)
|
|
13
|
+
expect(callback).toHaveBeenCalledWith(true)
|
|
14
|
+
|
|
15
|
+
isOpen.value = false
|
|
16
|
+
expect(isOpen.value).toBe(false)
|
|
17
|
+
expect(callback).toHaveBeenCalledWith(false)
|
|
18
|
+
|
|
19
|
+
expect(isOpenKey.value).toBe(0)
|
|
20
|
+
vi.advanceTimersByTime(50)
|
|
21
|
+
expect(isOpenKey.value).toBe(1)
|
|
22
|
+
|
|
23
|
+
vi.useRealTimers()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import useIsOpenFromVar from '../../../src/vue/composables/useIsOpenFromVar'
|
|
3
|
+
|
|
4
|
+
describe('useIsOpenFromVar', () => {
|
|
5
|
+
it('syncs open state with variable and resets after delay', () => {
|
|
6
|
+
vi.useFakeTimers()
|
|
7
|
+
|
|
8
|
+
const { fromVar, isOpenFromVar, isOpenFromVarKey } = useIsOpenFromVar<string>('default', 50)
|
|
9
|
+
|
|
10
|
+
fromVar.value = 'value'
|
|
11
|
+
expect(isOpenFromVar.value).toBe(true)
|
|
12
|
+
expect(fromVar.value).toBe('value')
|
|
13
|
+
|
|
14
|
+
isOpenFromVar.value = false
|
|
15
|
+
vi.advanceTimersByTime(50)
|
|
16
|
+
|
|
17
|
+
expect(fromVar.value).toBe('default')
|
|
18
|
+
expect(isOpenFromVarKey.value).toBe(1)
|
|
19
|
+
|
|
20
|
+
vi.useRealTimers()
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import useModelWrapper from '../../../src/vue/composables/useModelWrapper'
|
|
3
|
+
|
|
4
|
+
describe('useModelWrapper', () => {
|
|
5
|
+
it('wraps modelValue and emits updates', () => {
|
|
6
|
+
const props = { modelValue: 1 }
|
|
7
|
+
const emit = vi.fn()
|
|
8
|
+
const callback = vi.fn()
|
|
9
|
+
|
|
10
|
+
const model = useModelWrapper<number, typeof emit>(props, emit, { callback })
|
|
11
|
+
|
|
12
|
+
expect(model.value).toBe(1)
|
|
13
|
+
|
|
14
|
+
model.value = 2
|
|
15
|
+
|
|
16
|
+
expect(emit).toHaveBeenCalledWith('update:modelValue', 2)
|
|
17
|
+
expect(callback).toHaveBeenCalledWith(2)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('supports custom model name', () => {
|
|
21
|
+
const props = { custom: 'a' }
|
|
22
|
+
const emit = vi.fn()
|
|
23
|
+
|
|
24
|
+
const model = useModelWrapper<string, typeof emit>(props, emit, { name: 'custom' })
|
|
25
|
+
|
|
26
|
+
model.value = 'b'
|
|
27
|
+
|
|
28
|
+
expect(emit).toHaveBeenCalledWith('update:custom', 'b')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ref, nextTick } from 'vue'
|
|
3
|
+
import useOnOpen from '../../../src/vue/composables/useOnOpen'
|
|
4
|
+
|
|
5
|
+
describe('useOnOpen', () => {
|
|
6
|
+
it('invokes open and close callbacks', async () => {
|
|
7
|
+
const isOpen = ref(false)
|
|
8
|
+
const { onOpen, onClose } = useOnOpen(isOpen)
|
|
9
|
+
|
|
10
|
+
const openCb = vi.fn()
|
|
11
|
+
const closeCb = vi.fn()
|
|
12
|
+
|
|
13
|
+
onOpen(openCb)
|
|
14
|
+
onClose(closeCb)
|
|
15
|
+
|
|
16
|
+
isOpen.value = true
|
|
17
|
+
await nextTick()
|
|
18
|
+
await nextTick()
|
|
19
|
+
expect(openCb).toHaveBeenCalledTimes(1)
|
|
20
|
+
|
|
21
|
+
isOpen.value = false
|
|
22
|
+
await nextTick()
|
|
23
|
+
await nextTick()
|
|
24
|
+
expect(closeCb).toHaveBeenCalledTimes(1)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { PropertyAwareArray } from '../../../src/vue/forms/PropertyAwareArray'
|
|
3
|
+
|
|
4
|
+
describe('PropertyAwareArray', () => {
|
|
5
|
+
it('creates from array and preserves type on map/filter/slice/concat', () => {
|
|
6
|
+
const arr = new PropertyAwareArray<number>([1, 2, 3])
|
|
7
|
+
|
|
8
|
+
const mapped = arr.map((value) => value * 2)
|
|
9
|
+
const filtered = arr.filter((value) => value > 1)
|
|
10
|
+
const sliced = arr.slice(1)
|
|
11
|
+
const concatenated = arr.concat([4])
|
|
12
|
+
|
|
13
|
+
expect(mapped).toBeInstanceOf(PropertyAwareArray)
|
|
14
|
+
expect(filtered).toBeInstanceOf(PropertyAwareArray)
|
|
15
|
+
expect(sliced).toBeInstanceOf(PropertyAwareArray)
|
|
16
|
+
expect(concatenated).toBeInstanceOf(PropertyAwareArray)
|
|
17
|
+
|
|
18
|
+
expect(mapped).toEqual([2, 4, 6])
|
|
19
|
+
expect(filtered).toEqual([2, 3])
|
|
20
|
+
expect(sliced).toEqual([2, 3])
|
|
21
|
+
expect(concatenated).toEqual([1, 2, 3, 4])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('creates from arrayLike', () => {
|
|
25
|
+
const from = PropertyAwareArray.from({ 0: 'a', 1: 'b', length: 2 })
|
|
26
|
+
|
|
27
|
+
expect(from).toBeInstanceOf(PropertyAwareArray)
|
|
28
|
+
expect(from).toEqual(['a', 'b'])
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { RequiredRule } from '../../../../src/vue/forms/validation/rules/RequiredRule'
|
|
3
|
+
import { EmailRule } from '../../../../src/vue/forms/validation/rules/EmailRule'
|
|
4
|
+
import { MinRule } from '../../../../src/vue/forms/validation/rules/MinRule'
|
|
5
|
+
import { UrlRule } from '../../../../src/vue/forms/validation/rules/UrlRule'
|
|
6
|
+
import { ConfirmedRule } from '../../../../src/vue/forms/validation/rules/ConfirmedRule'
|
|
7
|
+
import { JsonRule } from '../../../../src/vue/forms/validation/rules/JsonRule'
|
|
8
|
+
import { ValidationMode } from '../../../../src/vue/forms/validation/ValidationMode.enum'
|
|
9
|
+
|
|
10
|
+
describe('Validation rules', () => {
|
|
11
|
+
it('RequiredRule validates presence', () => {
|
|
12
|
+
const rule = new RequiredRule()
|
|
13
|
+
|
|
14
|
+
expect(rule.validate('')).toBe(false)
|
|
15
|
+
expect(rule.validate(null as unknown as string)).toBe(false)
|
|
16
|
+
expect(rule.validate(undefined as unknown as string)).toBe(false)
|
|
17
|
+
expect(rule.validate(0 as unknown as string)).toBe(true)
|
|
18
|
+
expect(rule.getMessage()).toBe('This field is required')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('EmailRule validates email format', () => {
|
|
22
|
+
const rule = new EmailRule()
|
|
23
|
+
|
|
24
|
+
expect(rule.validate('')).toBe(true)
|
|
25
|
+
expect(rule.validate(123)).toBe(false)
|
|
26
|
+
expect(rule.validate('invalid')).toBe(false)
|
|
27
|
+
expect(rule.validate('test@example.com')).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('MinRule validates numbers, strings, and arrays', () => {
|
|
31
|
+
const rule = new MinRule(3)
|
|
32
|
+
|
|
33
|
+
expect(rule.validate(null)).toBe(true)
|
|
34
|
+
expect(rule.validate('ab')).toBe(false)
|
|
35
|
+
expect(rule.validate('abc')).toBe(true)
|
|
36
|
+
expect(rule.validate(2)).toBe(false)
|
|
37
|
+
expect(rule.validate(3)).toBe(true)
|
|
38
|
+
expect(rule.validate([1, 2])).toBe(false)
|
|
39
|
+
expect(rule.validate([1, 2, 3])).toBe(true)
|
|
40
|
+
expect(rule.validate({})).toBe(false)
|
|
41
|
+
expect(rule.getMessage()).toBe('This field must be at least 3')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('UrlRule validates URLs', () => {
|
|
45
|
+
const rule = new UrlRule()
|
|
46
|
+
|
|
47
|
+
expect(rule.validate('')).toBe(false)
|
|
48
|
+
expect(rule.validate('not a url')).toBe(false)
|
|
49
|
+
expect(rule.validate('https://example.com')).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('ConfirmedRule validates matching fields', () => {
|
|
53
|
+
const rule = new ConfirmedRule<{ password: string; confirm: string }>('confirm')
|
|
54
|
+
|
|
55
|
+
expect(rule.dependsOn).toEqual(['confirm'])
|
|
56
|
+
expect(rule.getBidirectionalFields()).toEqual(['confirm'])
|
|
57
|
+
|
|
58
|
+
expect(rule.validate('', { password: '', confirm: '' })).toBe(true)
|
|
59
|
+
expect(rule.validate('a', { password: 'a', confirm: 'a' })).toBe(true)
|
|
60
|
+
expect(rule.validate('a', { password: 'a', confirm: 'b' })).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('JsonRule validates JSON strings', () => {
|
|
64
|
+
const rule = new JsonRule()
|
|
65
|
+
|
|
66
|
+
expect(rule.validate('')).toBe(true)
|
|
67
|
+
expect(rule.validate('{"ok":true}')).toBe(true)
|
|
68
|
+
expect(rule.validate('{ bad }')).toBe(false)
|
|
69
|
+
expect(rule.validate(123)).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('ValidationMode has combined flags', () => {
|
|
73
|
+
expect(ValidationMode.DEFAULT).toBe(ValidationMode.ON_TOUCH | ValidationMode.ON_DIRTY | ValidationMode.ON_SUBMIT)
|
|
74
|
+
expect(ValidationMode.AGGRESSIVE).toBe(
|
|
75
|
+
ValidationMode.INSTANTLY | ValidationMode.ON_TOUCH | ValidationMode.ON_DIRTY | ValidationMode.ON_SUBMIT
|
|
76
|
+
)
|
|
77
|
+
expect(ValidationMode.PASSIVE).toBe(ValidationMode.ON_SUBMIT)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { VueRequestLoader } from '../../../src/vue/requests/loaders/VueRequestLoader'
|
|
3
|
+
import { VueRequestBatchLoader } from '../../../src/vue/requests/loaders/VueRequestBatchLoader'
|
|
4
|
+
import { VueRequestLoaderFactory } from '../../../src/vue/requests/factories/VueRequestLoaderFactory'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe('Vue request loaders', () => {
|
|
8
|
+
it('VueRequestLoader tracks loading state', () => {
|
|
9
|
+
const loader = new VueRequestLoader()
|
|
10
|
+
|
|
11
|
+
expect(loader.isLoading().value).toBe(false)
|
|
12
|
+
|
|
13
|
+
loader.setLoading(true)
|
|
14
|
+
expect(loader.isLoading().value).toBe(true)
|
|
15
|
+
|
|
16
|
+
loader.setLoading(false)
|
|
17
|
+
expect(loader.isLoading().value).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('VueRequestBatchLoader tracks batch state', () => {
|
|
21
|
+
const loader = new VueRequestBatchLoader(2)
|
|
22
|
+
|
|
23
|
+
expect(loader.isLoading().value).toBe(true)
|
|
24
|
+
|
|
25
|
+
loader.setLoading(true)
|
|
26
|
+
expect(loader.isLoading().value).toBe(true)
|
|
27
|
+
|
|
28
|
+
loader.setLoading(false)
|
|
29
|
+
expect(loader.isLoading().value).toBe(true)
|
|
30
|
+
|
|
31
|
+
loader.setLoading(false)
|
|
32
|
+
expect(loader.isLoading().value).toBe(false)
|
|
33
|
+
|
|
34
|
+
loader.startBatch(1)
|
|
35
|
+
expect(loader.isLoading().value).toBe(true)
|
|
36
|
+
|
|
37
|
+
loader.abortBatch()
|
|
38
|
+
expect(loader.isLoading().value).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('VueRequestLoaderFactory creates a loader', () => {
|
|
42
|
+
const factory = new VueRequestLoaderFactory()
|
|
43
|
+
|
|
44
|
+
const loader = factory.make()
|
|
45
|
+
|
|
46
|
+
expect(loader.isLoading().value).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
import { defineRoute } from '../../../../src/vue/router/routeResourceBinding/defineRoute'
|
|
4
|
+
import { RouteResourceRequestResolver } from '../../../../src/vue/router/routeResourceBinding/RouteResourceRequestResolver'
|
|
5
|
+
import { useRouteResource } from '../../../../src/vue/router/routeResourceBinding/useRouteResource'
|
|
6
|
+
|
|
7
|
+
const mockRoute = reactive({
|
|
8
|
+
meta: {} as Record<string, unknown>,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
vi.mock('vue-router', () => ({
|
|
12
|
+
useRoute: () => mockRoute,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('Route resource helpers', () => {
|
|
16
|
+
it('RouteResourceRequestResolver resolves data from request', async () => {
|
|
17
|
+
const request = {
|
|
18
|
+
send: vi.fn().mockResolvedValue({ getData: () => ({ id: 1 }) }),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const resolver = new RouteResourceRequestResolver(request)
|
|
22
|
+
await expect(resolver.resolve()).resolves.toEqual({ id: 1 })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('useRouteResource exposes loading, error, and refresh', async () => {
|
|
26
|
+
const refreshMock = vi.fn().mockResolvedValue('ok')
|
|
27
|
+
mockRoute.meta = {
|
|
28
|
+
refresh: refreshMock,
|
|
29
|
+
_injectionState: { product: { loading: true, error: new Error('fail') } },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { isLoading, error, refresh } = useRouteResource('product')
|
|
33
|
+
|
|
34
|
+
expect(isLoading.value).toBe(true)
|
|
35
|
+
expect(error.value).toBeInstanceOf(Error)
|
|
36
|
+
|
|
37
|
+
await refresh({ silent: true })
|
|
38
|
+
expect(refreshMock).toHaveBeenCalledWith('product', { silent: true })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('defineRoute moves meta config and merges injected props', () => {
|
|
42
|
+
const ErrorComp = { name: 'ErrorComp' }
|
|
43
|
+
const LoadingComp = { name: 'LoadingComp' }
|
|
44
|
+
|
|
45
|
+
const route = defineRoute<{ product: string }>()({
|
|
46
|
+
path: '/products/:id',
|
|
47
|
+
component: { name: 'Page' },
|
|
48
|
+
props: true,
|
|
49
|
+
errorComponent: ErrorComp,
|
|
50
|
+
loadingComponent: LoadingComp,
|
|
51
|
+
lazy: true,
|
|
52
|
+
meta: {},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect((route.meta as Record<string, unknown>)._errorComponent).toBe(ErrorComp)
|
|
56
|
+
expect((route.meta as Record<string, unknown>)._loadingComponent).toBe(LoadingComp)
|
|
57
|
+
expect((route.meta as Record<string, unknown>)._lazy).toBe(true)
|
|
58
|
+
expect((route as Record<string, unknown>).errorComponent).toBeUndefined()
|
|
59
|
+
expect((route as Record<string, unknown>).loadingComponent).toBeUndefined()
|
|
60
|
+
expect((route as Record<string, unknown>).lazy).toBeUndefined()
|
|
61
|
+
|
|
62
|
+
const propsFn = route.props as (to: { params: Record<string, unknown>; meta: Record<string, unknown> }) => Record<string, unknown>
|
|
63
|
+
const props = propsFn({
|
|
64
|
+
params: { id: 5 },
|
|
65
|
+
meta: { _injectedProps: { product: 'Widget' } },
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(props).toEqual({ id: 5, product: 'Widget' })
|
|
69
|
+
})
|
|
70
|
+
})
|