js-kompo-utils 1.0.28 → 1.0.30

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.
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import VlMultiStateCheckbox from './VlMultiStateCheckbox.vue'
3
+ import { mountKompo } from './__test_helpers__/mountKompo'
4
+
5
+ const SINGLE = { mode: 'single', values: [1, 2, 3], colors: ['bg-r', 'bg-y', 'bg-g'] }
6
+ const SECTION = { mode: 'section', values: [1, 2, 3], colors: ['bg-r', 'bg-y', 'bg-g'] }
7
+
8
+ function mountMscb(config, value) {
9
+ return mountKompo(VlMultiStateCheckbox, { config, value })
10
+ }
11
+
12
+ describe('VlMultiStateCheckbox — single mode rendering', () => {
13
+ it('shows the color matching the current value', () => {
14
+ const w = mountMscb(SINGLE, 2)
15
+ expect(w.find('.vl-mscb__swatch').classes()).toContain('bg-y')
16
+ })
17
+
18
+ it('shows empty when value is null', () => {
19
+ const w = mountMscb(SINGLE, null)
20
+ expect(w.find('.vl-mscb__swatch').classes()).toContain('vl-mscb__swatch--empty')
21
+ })
22
+
23
+ it('shows empty when value is not in the values list', () => {
24
+ const w = mountMscb(SINGLE, 99)
25
+ expect(w.find('.vl-mscb__swatch').classes()).toContain('vl-mscb__swatch--empty')
26
+ })
27
+ })
28
+
29
+ describe('VlMultiStateCheckbox — single mode click', () => {
30
+ it.each([
31
+ [null, 1],
32
+ [1, 2],
33
+ [2, 3],
34
+ [3, null],
35
+ [99, 1],
36
+ ])('cycles %s → %s', async (from, to) => {
37
+ const w = mountMscb(SINGLE, from)
38
+ await w.find('.vl-mscb').trigger('click')
39
+ expect(w.vm.component.value).toBe(to)
40
+ })
41
+
42
+ it('does not change value when readonly', async () => {
43
+ const w = mountMscb({ ...SINGLE, readonly: true }, 1)
44
+ await w.find('.vl-mscb').trigger('click')
45
+ expect(w.vm.component.value).toBe(1)
46
+ })
47
+
48
+ it.each(['keydown.enter', 'keydown.space'])('cycles via %s', async (key) => {
49
+ const w = mountMscb(SINGLE, null)
50
+ await w.find('.vl-mscb').trigger(key)
51
+ expect(w.vm.component.value).toBe(1)
52
+ })
53
+ })
54
+
55
+ describe('VlMultiStateCheckbox — section mode rendering', () => {
56
+ it('renders one bar per distinct active value (mixed)', () => {
57
+ const w = mountMscb(SECTION, [1, 2])
58
+ const bars = w.findAll('.vl-mscb__bar--visible')
59
+ expect(bars.length).toBe(2)
60
+ expect(bars.at(0).classes()).toContain('bg-r')
61
+ expect(bars.at(1).classes()).toContain('bg-y')
62
+ })
63
+
64
+ it('renders one bar when all entries share a value (uniform)', () => {
65
+ const w = mountMscb(SECTION, [2, 2, 2])
66
+ const bars = w.findAll('.vl-mscb__bar--visible')
67
+ expect(bars.length).toBe(1)
68
+ expect(bars.at(0).classes()).toContain('bg-y')
69
+ })
70
+
71
+ it.each([
72
+ ['empty array', []],
73
+ ['all-null', [null, null]],
74
+ ['null value', null],
75
+ ])('renders no bars when %s', (_label, value) => {
76
+ const w = mountMscb(SECTION, value)
77
+ expect(w.findAll('.vl-mscb__bar--visible').length).toBe(0)
78
+ })
79
+
80
+ it('skips entries not in the values whitelist', () => {
81
+ const w = mountMscb(SECTION, [1, 99, 2])
82
+ const bars = w.findAll('.vl-mscb__bar--visible')
83
+ expect(bars.length).toBe(2)
84
+ })
85
+ })
86
+
87
+ describe('VlMultiStateCheckbox — section mode click', () => {
88
+ it.each([
89
+ ['uniform — advances from shared value', [2, 2], 3],
90
+ ['uniform at last — cycles to null', [3, 3], null],
91
+ ['mixed — starts at first state', [1, 2], 1],
92
+ ['empty — starts at first state', [], 1],
93
+ ])('%s', async (_label, value, expected) => {
94
+ const w = mountMscb(SECTION, value)
95
+ await w.find('.vl-mscb').trigger('click')
96
+ expect(w.vm.component.value).toBe(expected)
97
+ })
98
+
99
+ it('does not change value when readonly', async () => {
100
+ const w = mountMscb({ ...SECTION, readonly: true }, [1, 2])
101
+ await w.find('.vl-mscb').trigger('click')
102
+ expect(w.vm.component.value).toEqual([1, 2])
103
+ })
104
+
105
+ it('emits a scalar value, not an array', async () => {
106
+ const w = mountMscb(SECTION, [1, 1])
107
+ await w.find('.vl-mscb').trigger('click')
108
+ expect(Array.isArray(w.vm.component.value)).toBe(false)
109
+ expect(w.vm.component.value).toBe(2)
110
+ })
111
+
112
+ it('updates the visual to a uniform bar at the new target after click', async () => {
113
+ const w = mountMscb(SECTION, [1, 2]) // mixed — click resets to states[0]=1
114
+ await w.find('.vl-mscb').trigger('click')
115
+ const bars = w.findAll('.vl-mscb__bar--visible')
116
+ expect(bars.length).toBe(1)
117
+ expect(bars.at(0).classes()).toContain('bg-r')
118
+ })
119
+
120
+ it('renders no bars after a click that cycles past the last state to null', async () => {
121
+ const w = mountMscb(SECTION, [3, 3]) // uniform at last → click → null
122
+ await w.find('.vl-mscb').trigger('click')
123
+ expect(w.findAll('.vl-mscb__bar--visible').length).toBe(0)
124
+ })
125
+ })
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <vl-form-field v-bind="$_wrapperAttributes">
3
+ <div class="vl-mscb inline-block"
4
+ v-bind="$_config('attrs') || {}"
5
+ :class="rootClass"
6
+ tabindex="0"
7
+ @click.stop="onClick"
8
+ @keydown.enter.stop.prevent="onClick"
9
+ @keydown.space.stop.prevent="onClick">
10
+
11
+ <span v-if="!isSection"
12
+ class="vl-mscb__swatch block w-[1.1rem] h-[1.1rem] border border-black rounded"
13
+ :class="swatchClass" />
14
+
15
+ <span v-else
16
+ class="vl-mscb__section flex w-[1.1rem] h-[1.1rem] border border-black rounded overflow-hidden">
17
+ <span v-for="bar in sectionBars" :key="bar.key"
18
+ class="vl-mscb__bar vl-mscb__bar--visible flex-1 h-full"
19
+ :class="bar.color" />
20
+ </span>
21
+ </div>
22
+ </vl-form-field>
23
+ </template>
24
+
25
+ <script>
26
+ import Field from 'vue-kompo/js/form/mixins/Field'
27
+
28
+ export default {
29
+ name: 'VlMultiStateCheckbox',
30
+ mixins: [Field],
31
+
32
+ // Local display state for section mode.
33
+ //
34
+ // sectionOverride: holds the visual array right after a section click
35
+ // sets a cascade target (uniform across all cells). Cleared when a cell
36
+ // change comes in, so cellValues wins.
37
+ //
38
+ // cellValues: a live map keyed by permission_id, populated as each cell
39
+ // mounts and emits its initial state, then kept current via cell-change
40
+ // events. The section's bars derive from this map. PHP's $_value array
41
+ // is only used as a fallback when no cell-state events have arrived.
42
+ data() {
43
+ return { sectionOverride: null, cellValues: {} }
44
+ },
45
+
46
+ computed: {
47
+ isSection() { return this.$_config('mode') === 'section' },
48
+ states() { return this.$_config('values') || [] },
49
+ colors() { return this.$_config('colors') || [] },
50
+ isReadonly() { return this.$_readOnly || !!this.$_config('readonly') },
51
+
52
+ rootClass() {
53
+ return {
54
+ 'vl-mscb--readonly': this.isReadonly,
55
+ 'vl-mscb--section': this.isSection,
56
+ }
57
+ },
58
+
59
+ // Single-mode swatch: highlight the color matching $_value, or empty.
60
+ swatchClass() {
61
+ const i = this.indexOf(this.$_value)
62
+ return i < 0 ? ['vl-mscb__swatch--empty'] : [this.colors[i] || '']
63
+ },
64
+
65
+ // What array of cell values to display in section mode.
66
+ // Priority: explicit override (just-clicked cascade) > live cellValues
67
+ // populated from cell mount/change events > PHP-seeded prop array.
68
+ sectionDisplay() {
69
+ if (this.sectionOverride !== null) return this.sectionOverride
70
+ const live = Object.values(this.cellValues)
71
+ if (live.length > 0) return live
72
+ return Array.isArray(this.$_value) ? this.$_value : []
73
+ },
74
+
75
+ // Sync-group key. Cells and section sharing the same group exchange
76
+ // visual state via the global $kompo.events bus.
77
+ groupKey() { return this.$_config('group') || null },
78
+ permId() { return this.$_config('permission_id') || null },
79
+ cellEvent() { return this.groupKey ? 'mscb:cell-state:' + this.groupKey : null },
80
+ cascadeEvent() { return this.groupKey ? 'mscb:section-cascade:' + this.groupKey : null },
81
+
82
+ // One bar per distinct active value in sectionDisplay, restricted to
83
+ // known states, in first-occurrence order.
84
+ sectionBars() {
85
+ const seen = new Map()
86
+ for (const v of this.sectionDisplay) {
87
+ if (v === null || v === undefined || seen.has(v)) continue
88
+ const i = this.indexOf(v)
89
+ if (i < 0) continue
90
+ seen.set(v, { key: v, color: this.colors[i] })
91
+ }
92
+ return Array.from(seen.values())
93
+ },
94
+
95
+ // Next state on click.
96
+ // Single mode: advance from $_value.
97
+ // Section mode: if cells share one state, advance from that state;
98
+ // otherwise (mixed or empty) start at the first state.
99
+ nextValue() {
100
+ if (!this.isSection) return this.advance(this.$_value)
101
+ const bars = this.sectionBars
102
+ return bars.length === 1 ? this.advance(bars[0].key) : this.states[0]
103
+ },
104
+ },
105
+
106
+ methods: {
107
+ indexOf(value) {
108
+ if (value === null || value === undefined) return -1
109
+ return this.states.findIndex(s => s == value)
110
+ },
111
+ advance(from) {
112
+ const i = this.indexOf(from)
113
+ if (i < 0) return this.states[0]
114
+ return (i + 1 >= this.states.length) ? null : this.states[i + 1]
115
+ },
116
+ onClick() {
117
+ if (this.isReadonly) return
118
+ const target = this.nextValue
119
+
120
+ if (this.isSection) {
121
+ // Cascade visual: every bar becomes the new target (or none if null).
122
+ const len = this.sectionDisplay.length || 1
123
+ this.sectionOverride = target === null ? [] : new Array(len).fill(target)
124
+ if (this.cascadeEvent) {
125
+ this.$kompo.events.$emit(this.cascadeEvent, target)
126
+ }
127
+ } else if (this.cellEvent && this.permId !== null) {
128
+ // Single-cell click: tell the section header our new value.
129
+ this.$kompo.events.$emit(this.cellEvent, {
130
+ permission_id: this.permId,
131
+ value: target,
132
+ })
133
+ }
134
+
135
+ this.component = Object.assign({}, this.component, { value: target })
136
+ this.$_changeAction()
137
+ },
138
+ $_resetSortValue() {
139
+ this.component.value = null
140
+ this.sectionOverride = null
141
+ this.cellValues = {}
142
+ },
143
+ },
144
+
145
+ mounted() {
146
+ if (!this.groupKey) return
147
+
148
+ if (this.isSection) {
149
+ // Listen for cells reporting their state (initial mount + every change).
150
+ this.$_vlOn(this.cellEvent, ({ permission_id, value }) => {
151
+ this.cellValues = Object.assign({}, this.cellValues, { [permission_id]: value })
152
+ // A real cell change supersedes any earlier cascade override.
153
+ this.sectionOverride = null
154
+ })
155
+ } else if (this.permId !== null) {
156
+ // Cells: react to section cascade by adopting the new value
157
+ // and re-broadcasting their state so the section's cellValues stays current.
158
+ this.$_vlOn(this.cascadeEvent, (newValue) => {
159
+ this.component = Object.assign({}, this.component, { value: newValue })
160
+ this.$kompo.events.$emit(this.cellEvent, {
161
+ permission_id: this.permId,
162
+ value: newValue,
163
+ })
164
+ })
165
+
166
+ // Announce our initial value so the section can build its cellValues map.
167
+ this.$kompo.events.$emit(this.cellEvent, {
168
+ permission_id: this.permId,
169
+ value: this.$_value,
170
+ })
171
+ }
172
+ },
173
+
174
+ beforeDestroy() {
175
+ if (!this.groupKey) return
176
+ if (this.isSection) {
177
+ this.$_vlOff(this.cellEvent)
178
+ } else {
179
+ this.$_vlOff(this.cascadeEvent)
180
+ }
181
+ },
182
+ }
183
+ </script>
@@ -34,11 +34,15 @@
34
34
  <option v-for="opt in nativeOptionsList" :key="opt.value" :value="opt.value">{{ nativeLabel(opt) }}</option>
35
35
  </select>
36
36
  </div>
37
- <div class="vlOptions vlOptionsTeleported"
37
+ <div class="vlOptions"
38
38
  :key="optionsKey"
39
39
  ref="selectOptions"
40
40
  :style="optionsStyle"
41
- :class="{ 'vlOptionsFlipUp': flipUp, 'vlOptionsVisible': isFocused }">
41
+ :class="{
42
+ 'vlOptionsTeleported': useFloatingOptions,
43
+ 'vlOptionsFlipUp': useFloatingOptions && flipUp,
44
+ 'vlOptionsVisible': useFloatingOptions && isFocused
45
+ }">
42
46
  <template v-if="filteredOptions.length">
43
47
  <div v-for="(option,key) in filteredOptions" :key="option.value" :ref="key"
44
48
  class="vlOption"
@@ -85,7 +89,14 @@ export default {
85
89
  forceDown() {
86
90
  return !!this.$_config('openDownward') || this.$_config('autoOptionsPlacement') === false
87
91
  },
92
+ useFloatingOptions() {
93
+ const explicitFloating = this.$_config('floatingOptions')
94
+ if (explicitFloating !== undefined && explicitFloating !== null) return !!explicitFloating
95
+
96
+ return !!this.$_config('teleportedOptions') || this.forceUp
97
+ },
88
98
  optionsStyle() {
99
+ if (!this.useFloatingOptions) return null
89
100
  return this.isFocused ? null : { display: 'none' }
90
101
  },
91
102
  useNativeMobile() {
@@ -116,6 +127,14 @@ export default {
116
127
  },
117
128
  watch: {
118
129
  isFocused(focused) {
130
+ if (!this.useFloatingOptions) {
131
+ if (!focused) {
132
+ this.flipUp = false
133
+ this.detachListeners()
134
+ this.resetPanelStyles()
135
+ }
136
+ return
137
+ }
119
138
  if (focused) {
120
139
  this.attachListeners()
121
140
  this.$nextTick(() => {
@@ -129,9 +148,10 @@ export default {
129
148
  }
130
149
  },
131
150
  filteredOptions() {
132
- if (this.isFocused) this.$nextTick(() => this.queuePositionUpdate())
151
+ if (this.useFloatingOptions && this.isFocused) this.$nextTick(() => this.queuePositionUpdate())
133
152
  },
134
153
  optionsKey() {
154
+ if (!this.useFloatingOptions) return
135
155
  this.$nextTick(() => {
136
156
  this.teleportOptions()
137
157
  if (this.isFocused) this.startPositioning()
@@ -139,7 +159,7 @@ export default {
139
159
  },
140
160
  },
141
161
  mounted() {
142
- this.teleportOptions()
162
+ if (this.useFloatingOptions) this.teleportOptions()
143
163
  if (typeof window !== 'undefined' && window.matchMedia) {
144
164
  this.isTouchDevice = window.matchMedia('(pointer: coarse)').matches
145
165
  }
@@ -183,23 +203,56 @@ export default {
183
203
  tmp.innerHTML = str
184
204
  return (tmp.textContent || tmp.innerText || '').trim()
185
205
  },
186
- teleportOptions() {
206
+ teleportOptions(anchor = this.anchorEl()) {
207
+ if (!this.useFloatingOptions) return
187
208
  const opts = this.$refs.selectOptions
188
- if (opts && opts.parentNode !== document.body) {
189
- document.body.appendChild(opts)
209
+ const context = this.floatingContext(anchor)
210
+ const root = context?.root
211
+ if (opts && root && opts.parentNode !== root) {
212
+ root.appendChild(opts)
190
213
  }
191
214
  },
192
215
  cleanupOptions() {
216
+ if (!this.useFloatingOptions) return
193
217
  const opts = this.$refs.selectOptions
194
- if (opts && opts.parentNode === document.body) {
195
- document.body.removeChild(opts)
218
+ if (opts && opts.parentNode && this.$el && !this.$el.contains(opts)) {
219
+ opts.parentNode.removeChild(opts)
196
220
  }
197
221
  },
198
222
  anchorEl() {
199
223
  return this.$refs.taggableInput?.$el || this.$refs.input?.parentElement
200
224
  },
201
225
  overlayAncestor(anchor = this.anchorEl()) {
202
- return anchor?.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l') || null
226
+ return anchor?.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l, .vl-sidebar-r') || null
227
+ },
228
+ floatingContext(anchor = this.anchorEl()) {
229
+ if (!anchor || typeof document === 'undefined') return null
230
+
231
+ const drawerOrSidebar = anchor.closest('.kompoDrawer, .vl-sidebar-l, .vl-sidebar-r')
232
+ if (drawerOrSidebar && typeof window !== 'undefined') {
233
+ const position = window.getComputedStyle(drawerOrSidebar).position
234
+ if (position && position !== 'static') {
235
+ return {
236
+ root: drawerOrSidebar,
237
+ strategy: 'absolute',
238
+ zIndex: 5001,
239
+ }
240
+ }
241
+ }
242
+
243
+ if (this.overlayAncestor(anchor)) {
244
+ return {
245
+ root: document.body,
246
+ strategy: 'fixed',
247
+ zIndex: 5001,
248
+ }
249
+ }
250
+
251
+ return {
252
+ root: document.body,
253
+ strategy: 'absolute',
254
+ zIndex: 1000,
255
+ }
203
256
  },
204
257
  attachListeners() {
205
258
  if (this._listenersAttached) return
@@ -233,7 +286,10 @@ export default {
233
286
  if (!opts || !anchor || !this.isFocused) return
234
287
 
235
288
  this.stopPositioning()
236
- this.applyBasePanelStyles(opts, anchor)
289
+ this.teleportOptions(anchor)
290
+ const context = this.floatingContext(anchor)
291
+ if (!context) return
292
+ this.applyBasePanelStyles(opts, anchor, context)
237
293
  opts.style.visibility = 'hidden'
238
294
  opts.style.transform = 'translate3d(0px, 0px, 0)'
239
295
 
@@ -258,15 +314,16 @@ export default {
258
314
  this._autoUpdateCleanup()
259
315
  this._autoUpdateCleanup = null
260
316
  },
261
- applyBasePanelStyles(opts, anchor) {
262
- const inOverlay = !!this.overlayAncestor(anchor)
263
- opts.style.position = 'fixed'
317
+ applyBasePanelStyles(opts, anchor, context = this.floatingContext(anchor)) {
318
+ if (!context) return
319
+
320
+ opts.style.position = context.strategy
264
321
  opts.style.top = '0px'
265
322
  opts.style.left = '0px'
266
323
  opts.style.bottom = 'auto'
267
324
  opts.style.width = Math.round(anchor.getBoundingClientRect().width) + 'px'
268
325
  opts.style.maxHeight = '240px'
269
- opts.style.zIndex = inOverlay ? '5001' : '1000'
326
+ opts.style.zIndex = String(context.zIndex)
270
327
  opts.style.willChange = 'transform'
271
328
  },
272
329
  resetPanelStyles() {
@@ -315,6 +372,17 @@ export default {
315
372
  const referenceEl = anchor
316
373
  const positionVersion = this._positionVersion || 0
317
374
  const { placement, auto } = this.placementConfig()
375
+ const context = this.floatingContext(referenceEl)
376
+ if (!context) return
377
+
378
+ if (floatingEl.parentNode !== context.root) {
379
+ this.$nextTick(() => {
380
+ if (this.isFocused) this.startPositioning()
381
+ })
382
+ return
383
+ }
384
+
385
+ this.applyBasePanelStyles(floatingEl, referenceEl, context)
318
386
 
319
387
  const middleware = [
320
388
  offset(-1),
@@ -332,7 +400,7 @@ export default {
332
400
  ]
333
401
 
334
402
  computePosition(referenceEl, floatingEl, {
335
- strategy: 'fixed',
403
+ strategy: context.strategy,
336
404
  placement,
337
405
  middleware,
338
406
  }).then(({ x, y, placement, middlewareData }) => {
@@ -67,7 +67,7 @@ export default {
67
67
  return this.$_config('activeTab')
68
68
  },
69
69
  tabParamKey(){
70
- return 'tab_number'
70
+ return this.$_config('tabParamKey') || 'tab_number'
71
71
  },
72
72
  selectId(){
73
73
  return this.$_config('selectId')
@@ -317,6 +317,10 @@ export default {
317
317
  }else if(this.defaultActiveTab !== undefined && this.defaultActiveTab !== null){
318
318
  this.activeTab = this.defaultActiveTab
319
319
  }
320
+
321
+ if (this.activeTab >= this.elements.length || this.activeTab < 0 || !this.activeTab) {
322
+ this.activeTab = 0
323
+ }
320
324
  },
321
325
  mounted(){
322
326
  this.syncSelect()
@@ -0,0 +1,39 @@
1
+ import { mount } from '@vue/test-utils'
2
+
3
+ // Minimal stub for vl-form-field. Real impl is heavy and unrelated to the
4
+ // behaviour we're testing; we just need a transparent wrapper for the slot.
5
+ const VlFormFieldStub = {
6
+ props: ['component', 'errors'],
7
+ template: '<div class="vl-form-field-stub"><slot /></div>',
8
+ }
9
+
10
+ /**
11
+ * Mount a kompo Field-style Vue component with a synthesized vkompo blob.
12
+ * Use this for any component that mixes in `vue-kompo/js/form/mixins/Field`
13
+ * (or descendants of `BaseElement`) so tests don't have to hand-roll the
14
+ * shape every time.
15
+ *
16
+ * mountKompo(MyField, {
17
+ * config: { mode: 'single', values: [1,2], colors: ['a','b'] },
18
+ * value: null,
19
+ * })
20
+ *
21
+ * Reads `wrapper.vm.component.value` to observe value changes after click.
22
+ */
23
+ export function mountKompo(component, { config = {}, value = null, name = 'test', label = null } = {}) {
24
+ return mount(component, {
25
+ propsData: {
26
+ vkompo: {
27
+ vueComponent: component.name || 'TestComponent',
28
+ config,
29
+ value,
30
+ name,
31
+ label,
32
+ _kid: 'k-test',
33
+ },
34
+ },
35
+ stubs: {
36
+ 'vl-form-field': VlFormFieldStub,
37
+ },
38
+ })
39
+ }
@@ -0,0 +1,25 @@
1
+ // Test setup for components that use vue-kompo's Field mixin chain.
2
+ // The kompo runtime expects `_` (lodash) and `$kompo` to be available globally.
3
+ import _ from 'lodash'
4
+ import Vue from 'vue'
5
+
6
+ // Lodash is referenced as a bare global (`_`) inside vue-kompo mixins.
7
+ globalThis._ = _
8
+ // vue-kompo's KompoHelper module references `Vue` as a bare global at import time.
9
+ globalThis.Vue = Vue
10
+
11
+ // Stub the kompo bus that Field's $_changeAction reaches into.
12
+ // Tests don't exercise the real form/error pipeline, so the methods are no-ops.
13
+ Vue.prototype.$kompo = {
14
+ vlUpdateErrorState() {},
15
+ vlSubmit() {},
16
+ vlToggle() {},
17
+ vlEmitFrom() {},
18
+ vlHybridFilter() {},
19
+ vlJsInstantFilter() {},
20
+ vlSetFieldValue() {},
21
+ events: {
22
+ $on() {},
23
+ $off() {},
24
+ },
25
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "js-kompo-utils",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "vitest"
8
8
  },
9
9
  "keywords": [],
10
10
  "author": "xkairo",
@@ -27,13 +27,17 @@
27
27
  "vue-qrcode-reader": "^3.1.0"
28
28
  },
29
29
  "devDependencies": {
30
+ "@vitejs/plugin-vue2": "^2.3.1",
31
+ "@vue/test-utils": "^1.3.6",
30
32
  "autoprefixer": "^10.4.7",
31
33
  "axios": "^0.21",
34
+ "jsdom": "^24.0.0",
32
35
  "laravel-mix": "^6.0.49",
33
36
  "lodash": "^4.17.19",
34
37
  "postcss": "^8.4.14",
35
38
  "sass": "^1.83.0",
36
39
  "tailwindcss": "^3.1.6",
40
+ "vitest": "^1.6.0",
37
41
  "vue-kompo": "^4.0.3"
38
42
  }
39
43
  }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import vue from '@vitejs/plugin-vue2'
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ test: {
7
+ environment: 'jsdom',
8
+ include: ['components/**/*.test.js'],
9
+ setupFiles: ['./components/test-setup.js'],
10
+ },
11
+ })