@ditojs/admin 3.0.0 → 3.1.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.
@@ -1,395 +0,0 @@
1
- import NumberMixin from '../../mixins/NumberMixin.js'
2
- import { registerTypeComponent } from './types.js'
3
- import {
4
- getValidations,
5
- getValueValidationErrors,
6
- getDataValidationErrors,
7
- getMatchingValidationErrors
8
- } from './validation.js'
9
-
10
- // Register minimal type options, as the actual type components can't be
11
- // imported without a Vue SFC compiler:
12
- registerTypeComponent('text', { defaultNested: true })
13
- registerTypeComponent('password', {
14
- defaultNested: true,
15
- getTypeValidations: () => ({ password: true })
16
- })
17
- registerTypeComponent('number', { ...NumberMixin, defaultNested: true })
18
- registerTypeComponent('integer', { ...NumberMixin, defaultNested: true })
19
- registerTypeComponent('section', { defaultNested: false })
20
- registerTypeComponent('panel', { defaultNested: false })
21
- registerTypeComponent('list', {
22
- defaultNested: true,
23
- getSourceType: () => 'list'
24
- })
25
- registerTypeComponent('object', {
26
- defaultNested: true,
27
- getSourceType: () => 'object'
28
- })
29
-
30
- describe('getValidations()', () => {
31
- it('returns no validations for plain schemas', () => {
32
- expect(getValidations({ type: 'text' })).toEqual({})
33
- })
34
-
35
- it('supports `required` as value and callback', () => {
36
- expect(getValidations({ type: 'text', required: true })).toEqual({
37
- required: true
38
- })
39
- const schema = { type: 'text', required: ({ value }) => value === 1 }
40
- expect(getValidations(schema, { value: 1 })).toEqual({
41
- required: true
42
- })
43
- expect(getValidations(schema, { value: 2 })).toEqual({})
44
- })
45
-
46
- it('adds the validations of the type', () => {
47
- expect(getValidations({ type: 'password' })).toEqual({
48
- password: true
49
- })
50
- })
51
-
52
- it('applies `schema.rules` overrides', () => {
53
- expect(
54
- getValidations({
55
- type: 'password',
56
- required: true,
57
- rules: { password: undefined, required: undefined, email: true }
58
- })
59
- ).toEqual({ email: true })
60
- })
61
-
62
- it('ignores unknown types', () => {
63
- expect(getValidations({ type: 'unknown', required: true })).toEqual({
64
- required: true
65
- })
66
- })
67
-
68
- describe('number types', () => {
69
- it.each([
70
- [{ type: 'number' }, {}],
71
- [{ type: 'number', min: 1 }, { min: 1 }],
72
- [{ type: 'number', max: 2 }, { max: 2 }],
73
- [{ type: 'number', min: 1, max: 2 }, { range: [1, 2] }],
74
- [{ type: 'number', range: [1, 2] }, { range: [1, 2] }],
75
- [{ type: 'number', range: [1, 2], min: 0 }, { range: [0, 2] }],
76
- [{ type: 'number', decimals: 2 }, { decimals: 2 }],
77
- [{ type: 'number', step: 0.25 }, { decimals: 2 }],
78
- [{ type: 'number', step: 5 }, { integer: true }],
79
- [{ type: 'number', step: 0.5, decimals: 3 }, { decimals: 3 }],
80
- [{ type: 'integer' }, { integer: true }],
81
- [
82
- { type: 'integer', range: [0.5, 1.5], step: 0.5 },
83
- { range: [0, 2], integer: true }
84
- ]
85
- ])('%j', (schema, validations) => {
86
- expect(getValidations(schema)).toEqual(validations)
87
- })
88
-
89
- it('evaluates callbacks with the context', () => {
90
- const schema = { type: 'number', max: ({ item }) => item.limit }
91
- expect(getValidations(schema, { item: { limit: 3 } })).toEqual({
92
- max: 3
93
- })
94
- })
95
- })
96
- })
97
-
98
- describe('getValueValidationErrors()', () => {
99
- it('returns no errors for valid values', () => {
100
- const validations = { required: true, range: [0, 2] }
101
- expect(getValueValidationErrors(1, validations)).toEqual([])
102
- })
103
-
104
- it('applies `required` only to empty values', () => {
105
- expect(getValueValidationErrors(null, { required: true })).toEqual([
106
- { message: 'is required' }
107
- ])
108
- expect(getValueValidationErrors('', { required: true })).toEqual([
109
- { message: 'is required' }
110
- ])
111
- expect(getValueValidationErrors(undefined, { required: true })).toEqual([
112
- { message: 'is required' }
113
- ])
114
- expect(getValueValidationErrors(0, { required: true })).toEqual([])
115
- })
116
-
117
- it('applies other validations only to non-empty values', () => {
118
- expect(getValueValidationErrors(null, { min: 1, email: true })).toEqual([])
119
- expect(getValueValidationErrors('', { min: 1, email: true })).toEqual([])
120
- expect(getValueValidationErrors(0, { min: 1 })).toEqual([
121
- { message: 'must be 1 or more' }
122
- ])
123
- })
124
-
125
- it('returns all errors, with messages without label prefix', () => {
126
- expect(getValueValidationErrors(1.5, { min: 2, integer: true })).toEqual([
127
- { message: 'must be 2 or more' },
128
- { message: 'must be whole number' }
129
- ])
130
- expect(getValueValidationErrors(5, { range: [1, 2] })).toEqual([
131
- { message: 'must be between 1 and 2' }
132
- ])
133
- })
134
-
135
- it('passes the other validations to the validators', () => {
136
- // Unchanged passwords are `undefined` and satisfy `required`:
137
- const validations = { required: true, password: true }
138
- expect(getValueValidationErrors(undefined, validations)).toEqual([])
139
- expect(getValueValidationErrors(null, validations)).toEqual([
140
- { message: 'is required' }
141
- ])
142
- })
143
-
144
- it('ignores unknown validations', () => {
145
- expect(getValueValidationErrors('a', { unknown: true })).toEqual([])
146
- })
147
- })
148
-
149
- describe('getDataValidationErrors()', () => {
150
- const required = { message: 'is required' }
151
-
152
- it('returns `null` for valid data', () => {
153
- const schema = {
154
- components: {
155
- title: { type: 'text', required: true }
156
- }
157
- }
158
- expect(getDataValidationErrors(schema, { title: 'Title' })).toBe(null)
159
- })
160
-
161
- it('validates the components of panel components', () => {
162
- // Panels share the data of their schema, like unnested sections.
163
- const schema = {
164
- components: {
165
- links: {
166
- type: 'panel',
167
- components: { website: { type: 'text', required: true } }
168
- }
169
- }
170
- }
171
- expect(getDataValidationErrors(schema, { website: '' })).toEqual({
172
- website: [required]
173
- })
174
- })
175
-
176
- it('returns errors by data path, in the format of server errors', () => {
177
- const schema = {
178
- components: {
179
- title: { type: 'text', required: true },
180
- count: { type: 'integer', min: 1 },
181
- note: { type: 'text' }
182
- }
183
- }
184
- expect(getDataValidationErrors(schema, { title: '', count: 0.5 })).toEqual({
185
- title: [required],
186
- count: [
187
- { message: 'must be 1 or more' },
188
- { message: 'must be whole number' }
189
- ]
190
- })
191
- })
192
-
193
- it('prefixes errors with `dataPath`', () => {
194
- const schema = {
195
- components: { title: { type: 'text', required: true } }
196
- }
197
- expect(
198
- getDataValidationErrors(schema, {}, { dataPath: 'items/0' })
199
- ).toEqual({
200
- 'items/0/title': [required]
201
- })
202
- })
203
-
204
- it('validates tabs, panels and unnested components', () => {
205
- const schema = {
206
- components: {
207
- section: {
208
- type: 'section',
209
- components: { a: { type: 'text', required: true } }
210
- }
211
- },
212
- tabs: {
213
- tab: {
214
- type: 'tab',
215
- components: { b: { type: 'text', required: true } }
216
- }
217
- },
218
- panels: {
219
- panel: {
220
- type: 'panel',
221
- components: { c: { type: 'text', required: true } }
222
- }
223
- }
224
- }
225
- expect(getDataValidationErrors(schema, {})).toEqual({
226
- a: [required],
227
- b: [required],
228
- c: [required]
229
- })
230
- })
231
-
232
- it('validates the items of nested forms', () => {
233
- const schema = {
234
- components: {
235
- items: {
236
- type: 'list',
237
- form: {
238
- type: 'form',
239
- components: { title: { type: 'text', required: true } }
240
- }
241
- },
242
- object: {
243
- type: 'object',
244
- forms: {
245
- a: {
246
- type: 'form',
247
- components: { a: { type: 'text', required: true } }
248
- },
249
- b: {
250
- type: 'form',
251
- components: { b: { type: 'text', required: true } }
252
- }
253
- }
254
- }
255
- }
256
- }
257
- const data = {
258
- items: [{ title: 'Title' }, { title: null }],
259
- object: { type: 'b' }
260
- }
261
- expect(getDataValidationErrors(schema, data)).toEqual({
262
- 'items/1/title': [required],
263
- 'object/b': [required]
264
- })
265
- })
266
-
267
- it('skips components whose `if` is false', () => {
268
- const schema = {
269
- components: {
270
- show: { type: 'text' },
271
- a: {
272
- type: 'text',
273
- required: true,
274
- if: ({ item }) => item.show
275
- },
276
- section: {
277
- type: 'section',
278
- if: ({ item }) => item.show,
279
- components: { b: { type: 'text', required: true } }
280
- },
281
- items: {
282
- type: 'list',
283
- if: ({ item }) => item.show,
284
- form: {
285
- type: 'form',
286
- components: { c: { type: 'text', required: true } }
287
- }
288
- }
289
- },
290
- tabs: {
291
- tab: {
292
- type: 'tab',
293
- if: ({ item }) => item.show,
294
- components: { d: { type: 'text', required: true } }
295
- }
296
- }
297
- }
298
- const data = { items: [{}] }
299
- expect(getDataValidationErrors(schema, data)).toBe(null)
300
- expect(getDataValidationErrors(schema, { ...data, show: true })).toEqual({
301
- 'a': [required],
302
- 'b': [required],
303
- 'items/0/c': [required],
304
- 'd': [required]
305
- })
306
- })
307
-
308
- it('skips sources with their own resource', () => {
309
- const schema = {
310
- components: {
311
- items: {
312
- type: 'list',
313
- resource: { path: 'items' },
314
- form: {
315
- type: 'form',
316
- components: { title: { type: 'text', required: true } }
317
- }
318
- }
319
- }
320
- }
321
- expect(getDataValidationErrors(schema, { items: [{}] })).toBe(null)
322
- })
323
-
324
- it('validates formatted values', () => {
325
- const schema = {
326
- components: {
327
- name: {
328
- type: 'text',
329
- required: true,
330
- format: ({ value }) => value?.trim()
331
- }
332
- }
333
- }
334
- expect(getDataValidationErrors(schema, { name: ' ' })).toEqual({
335
- name: [required]
336
- })
337
- })
338
-
339
- it('passes `rootData` and `dataPath` to callbacks', () => {
340
- const rootData = { strict: true }
341
- const schema = {
342
- components: {
343
- name: {
344
- type: 'text',
345
- required: ({ rootItem, dataPath }) => (
346
- rootItem.strict && dataPath === 'item/name'
347
- )
348
- }
349
- }
350
- }
351
- expect(
352
- getDataValidationErrors(schema, {}, { dataPath: 'item', rootData })
353
- ).toEqual({ 'item/name': [required] })
354
- })
355
- })
356
-
357
- describe('getMatchingValidationErrors()', () => {
358
- const required = [{ message: 'is required' }]
359
- const errors = {
360
- 'title': required,
361
- 'items/0/title': required,
362
- 'items/1/name': required
363
- }
364
-
365
- it('matches data paths with functions', () => {
366
- expect(
367
- getMatchingValidationErrors(errors, dataPath => dataPath.length > 5)
368
- ).toEqual({
369
- 'items/0/title': required,
370
- 'items/1/name': required
371
- })
372
- })
373
-
374
- it('matches data paths with regular expressions', () => {
375
- expect(getMatchingValidationErrors(errors, /^items\/\d+\/title$/)).toEqual(
376
- { 'items/0/title': required }
377
- )
378
- })
379
-
380
- it('matches normalized data paths and arrays of them', () => {
381
- expect(getMatchingValidationErrors(errors, 'items[1].name')).toEqual({
382
- 'items/1/name': required
383
- })
384
- expect(getMatchingValidationErrors(errors, ['title', 'items/1/name']))
385
- .toEqual({
386
- 'title': required,
387
- 'items/1/name': required
388
- })
389
- })
390
-
391
- it('returns `null` if nothing matches', () => {
392
- expect(getMatchingValidationErrors(errors, 'name')).toBe(null)
393
- expect(getMatchingValidationErrors(null, /.*/)).toBe(null)
394
- })
395
- })
@@ -1,71 +0,0 @@
1
- import { formatFileSize, parseFileSize } from './units.js'
2
-
3
- describe('formatFileSize()', () => {
4
- // Expected values match the previously used `filesize` package.
5
- it.each([
6
- [0, '0 B'],
7
- [1, '1 B'],
8
- [0.5, '1 B'],
9
- [999, '999 B'],
10
- [1000, '1 kB'],
11
- [1001, '1 kB'],
12
- [1234, '1.23 kB'],
13
- [1500, '1.5 kB'],
14
- [9999, '10 kB'],
15
- [123456, '123.46 kB'],
16
- [999999, '1 MB'],
17
- [1234567, '1.23 MB'],
18
- [1e9, '1 GB'],
19
- [1.5e12, '1.5 TB'],
20
- [2.5e15, '2.5 PB'],
21
- [1e18, '1 EB'],
22
- [1e21, '1 ZB'],
23
- [-1500, '-1.5 kB']
24
- ])('formats %o as %o', (size, expected) => {
25
- expect(formatFileSize(size)).toBe(expected)
26
- })
27
-
28
- it('supports base 2', () => {
29
- expect(formatFileSize(1024, { base: 2 })).toBe('1 KiB')
30
- expect(formatFileSize(1536, { base: 2 })).toBe('1.5 KiB')
31
- expect(formatFileSize(1048576, { base: 2 })).toBe('1 MiB')
32
- expect(formatFileSize(1073741824, { base: 2 })).toBe('1 GiB')
33
- })
34
- })
35
-
36
- describe('parseFileSize()', () => {
37
- it.each([
38
- ['1024', 1024],
39
- [1024, 1024],
40
- ['100 bytes', 100],
41
- ['100B', 100],
42
- ['1k', 1000],
43
- ['1 kB', 1000],
44
- ['1 KB', 1000],
45
- ['1kb', 1000],
46
- ['1.5 MB', 1.5e6],
47
- ['1,5M', 1.5e6],
48
- ['1T', 1e12],
49
- ['1KiB', 1024],
50
- ['1 Ki', 1024],
51
- ['1.5 MiB', 1.5 * 1024 ** 2],
52
- ['2 GiB', 2 * 1024 ** 3],
53
- ['8b', 1],
54
- ['8 bits', 1],
55
- ['1Kb', 125],
56
- ['1Mb', 125000]
57
- ])('parses %o as %o bytes', (input, expected) => {
58
- expect(parseFileSize(input)).toBe(expected)
59
- })
60
-
61
- it('supports base 2', () => {
62
- expect(parseFileSize('1 MB', { base: 2 })).toBe(1024 ** 2)
63
- expect(parseFileSize('1 MiB', { base: 2 })).toBe(1024 ** 2)
64
- expect(parseFileSize('1Kb', { base: 2 })).toBe(128)
65
- })
66
-
67
- it('throws on invalid input', () => {
68
- expect(() => parseFileSize('MB')).toThrow()
69
- expect(() => parseFileSize('10 XB')).toThrow()
70
- })
71
- })