@weni/unnnic-system 3.28.2-alpha.3 → 3.29.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.
Files changed (40) hide show
  1. package/dist/index.d.ts +12 -324
  2. package/dist/style.css +1 -1
  3. package/dist/unnnic.mjs +7345 -7474
  4. package/dist/unnnic.umd.js +29 -29
  5. package/package.json +2 -2
  6. package/src/components/Button/__tests__/Button.spec.js +81 -0
  7. package/src/components/Button/__tests__/ButtonIcon.spec.js +56 -0
  8. package/src/components/Carousel/TagCarousel.vue +8 -2
  9. package/src/components/Carousel/__tests__/TagCarousel.spec.js +258 -0
  10. package/src/components/ChartLine/__tests__/ChartLine.spec.js +170 -0
  11. package/src/components/Checkbox/__tests__/Checkbox.spec.js +14 -0
  12. package/src/components/CheckboxGroup/__tests__/CheckboxGroup.spec.js +62 -0
  13. package/src/components/DatePicker/DatePicker.vue +1 -0
  14. package/src/components/DatePicker/__tests__/DatePicker.spec.js +364 -0
  15. package/src/components/Disclaimer/__tests__/Disclaimer.spec.js +16 -0
  16. package/src/components/DropArea/DropArea.vue +1 -2
  17. package/src/components/DropArea/__tests__/DropArea.spec.js +333 -0
  18. package/src/components/MultiSelect/__tests__/MultiSelect.spec.js +109 -2
  19. package/src/components/MultiSelect/index.vue +1 -1
  20. package/src/components/PageHeader/__tests__/PageHeader.spec.js +97 -0
  21. package/src/components/Pagination/__tests__/Pagination.spec.js +99 -0
  22. package/src/components/Radio/__test__/Radio.spec.js +67 -0
  23. package/src/components/RadioGroup/__tests__/RadioGroup.spec.js +100 -0
  24. package/src/components/Slider/__tests__/Slider.spec.js +321 -0
  25. package/src/components/Table/Table.vue +2 -21
  26. package/src/components/Table/TableRow.vue +3 -28
  27. package/src/components/Tag/DefaultTag.vue +4 -3
  28. package/src/components/Tag/__tests__/DefaultTag.spec.js +51 -0
  29. package/src/components/Tag/__tests__/Tag.spec.js +82 -0
  30. package/src/components/TextArea/__test__/TextArea.spec.js +19 -0
  31. package/src/components/ToolTip/__tests__/ToolTip.spec.js +82 -1
  32. package/src/components/index.ts +0 -13
  33. package/src/stories/Table.stories.js +2 -75
  34. package/src/components/ui/table/Table.vue +0 -29
  35. package/src/components/ui/table/TableBody.vue +0 -14
  36. package/src/components/ui/table/TableCell.vue +0 -28
  37. package/src/components/ui/table/TableHead.vue +0 -27
  38. package/src/components/ui/table/TableHeader.vue +0 -24
  39. package/src/components/ui/table/TableRow.vue +0 -47
  40. package/src/components/ui/table/index.ts +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weni/unnnic-system",
3
- "version": "3.28.2-alpha.3",
3
+ "version": "3.29.0",
4
4
  "type": "commonjs",
5
5
  "files": [
6
6
  "dist",
@@ -140,4 +140,4 @@
140
140
  "vue-eslint-parser": "^10.4.0",
141
141
  "vue-tsc": "^3.0.5"
142
142
  }
143
- }
143
+ }
@@ -91,6 +91,25 @@ describe('Button', () => {
91
91
  expect(invalidType).toThrow(Error);
92
92
  });
93
93
 
94
+ it('should count slot content as text for icon spacing', () => {
95
+ const slotWrapper = createWrapper(
96
+ { iconLeft: 'search-1', text: '' },
97
+ { default: 'Label' },
98
+ );
99
+
100
+ expect(slotWrapper.find('[data-testid="icon-left"]').classes()).toContain(
101
+ 'unnnic-button__icon-left',
102
+ );
103
+ });
104
+
105
+ it('should render button text from slot without text prop', () => {
106
+ const slotOnlyWrapper = createWrapper(
107
+ { text: '' },
108
+ { default: 'Slot only' },
109
+ );
110
+ expect(slotOnlyWrapper.text()).toContain('Slot only');
111
+ });
112
+
94
113
  it('should render filled icons when iconsFilled prop is true', async () => {
95
114
  const wrapper = createWrapper({
96
115
  iconLeft: 'search-1',
@@ -104,4 +123,66 @@ describe('Button', () => {
104
123
  expect(leftIcon.props('filled')).toBe(true);
105
124
  expect(rightIcon.props('filled')).toBe(true);
106
125
  });
126
+
127
+ it('should apply pressed class when pressed prop is true', () => {
128
+ const pressedWrapper = createWrapper({ text: 'Button', pressed: true });
129
+ expect(pressedWrapper.classes()).toContain('unnnic-button--pressed');
130
+ });
131
+
132
+ it.each(['primary', 'secondary', 'tertiary', 'warning', 'attention'])(
133
+ 'should render %s type',
134
+ (type) => {
135
+ const typeWrapper = createWrapper({ text: 'Button', type });
136
+ expect(typeWrapper.classes()).toContain(`unnnic-button--${type}`);
137
+ },
138
+ );
139
+
140
+ it('should map alternative type to tertiary', () => {
141
+ const altWrapper = createWrapper({ text: 'Button', type: 'alternative' });
142
+ expect(altWrapper.classes()).toContain('unnnic-button--tertiary');
143
+ });
144
+
145
+ it('should use muted icon scheme when disabled', async () => {
146
+ const disabledWrapper = createWrapper({
147
+ text: 'Button',
148
+ iconLeft: 'search-1',
149
+ disabled: true,
150
+ });
151
+
152
+ expect(
153
+ disabledWrapper
154
+ .findComponent('[data-testid="icon-left"]')
155
+ .props('scheme'),
156
+ ).toBe('fg-muted');
157
+ });
158
+
159
+ it('should use extra-large icon size for extra-large float button', () => {
160
+ const floatWrapper = createWrapper({
161
+ iconCenter: 'search-1',
162
+ float: true,
163
+ size: 'extra-large',
164
+ text: '',
165
+ });
166
+
167
+ expect(
168
+ floatWrapper.findComponent('[data-testid="icon-center"]').props('size'),
169
+ ).toBe('lg');
170
+ });
171
+
172
+ it('should render small size variation', () => {
173
+ const smallWrapper = createWrapper({ text: 'Button', size: 'small' });
174
+ expect(smallWrapper.classes()).toContain('unnnic-button--size-small');
175
+ });
176
+
177
+ it('should throw only invalid size message when size is invalid', () => {
178
+ expect(() => createWrapper({ size: 'small', float: true })).toThrow(
179
+ /Invalid size prop/,
180
+ );
181
+ });
182
+
183
+ it('should throw only invalid type message when type is invalid', () => {
184
+ expect(() => createWrapper({ type: 'invalid-type' })).toThrow(
185
+ /Invalid type prop/,
186
+ );
187
+ });
107
188
  });
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+
4
+ import ButtonIcon from '../ButtonIcon.vue';
5
+
6
+ const mountButtonIcon = (props = {}) =>
7
+ mount(ButtonIcon, {
8
+ props: {
9
+ size: 'large',
10
+ icon: 'search-1',
11
+ ...props,
12
+ },
13
+ global: {
14
+ stubs: {
15
+ UnnnicIcon: {
16
+ props: ['icon', 'scheme', 'size'],
17
+ template:
18
+ '<span data-testid="button-icon" :data-icon="icon" :data-scheme="scheme" />',
19
+ },
20
+ },
21
+ },
22
+ });
23
+
24
+ describe('ButtonIcon', () => {
25
+ it('renders with primary type icon scheme', () => {
26
+ const wrapper = mountButtonIcon({ type: 'primary' });
27
+
28
+ expect(
29
+ wrapper.find('[data-testid="button-icon"]').attributes('data-scheme'),
30
+ ).toBe('fg-inverted');
31
+ expect(wrapper.classes()).toContain('primary');
32
+ });
33
+
34
+ it('renders with secondary type icon scheme', () => {
35
+ const wrapper = mountButtonIcon({ type: 'secondary' });
36
+
37
+ expect(
38
+ wrapper.find('[data-testid="button-icon"]').attributes('data-scheme'),
39
+ ).toBe('fg-emphasized');
40
+ expect(wrapper.classes()).toContain('secondary');
41
+ });
42
+
43
+ it('applies size class', () => {
44
+ const wrapper = mountButtonIcon({ size: 'small' });
45
+
46
+ expect(wrapper.classes()).toContain('small');
47
+ });
48
+
49
+ it('rejects invalid size prop in validator', () => {
50
+ const { validator } = ButtonIcon.props.size;
51
+
52
+ expect(validator('large')).toBe(true);
53
+ expect(validator('small')).toBe(true);
54
+ expect(validator('invalid')).toBe(false);
55
+ });
56
+ });
@@ -9,7 +9,7 @@
9
9
  size="sm"
10
10
  :scheme="hasPrev ? 'fg-emphasized' : 'fg-muted'"
11
11
  clickable
12
- @click="previous()"
12
+ @click="handlePrevious"
13
13
  />
14
14
  </div>
15
15
  <span
@@ -51,7 +51,7 @@
51
51
  size="sm"
52
52
  :scheme="hasNext ? 'fg-emphasized' : 'fg-muted'"
53
53
  clickable
54
- @click="next()"
54
+ @click="handleNext"
55
55
  />
56
56
  </div>
57
57
  </div>
@@ -91,6 +91,12 @@ export default {
91
91
  this.slides = this.tagItems;
92
92
  },
93
93
  methods: {
94
+ handleNext() {
95
+ this.next();
96
+ },
97
+ handlePrevious() {
98
+ this.previous();
99
+ },
94
100
  save(tagItem) {
95
101
  let valueLocal = this.modelValue;
96
102
  if (this.checkIsInclude(tagItem)) {
@@ -0,0 +1,258 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+
4
+ import TagCarousel from '../TagCarousel.vue';
5
+
6
+ const tagItems = [
7
+ { id: 1, name: 'Tag 1' },
8
+ { id: 2, name: 'Tag 2' },
9
+ { id: 3, name: 'Tag 3' },
10
+ ];
11
+
12
+ const mountCarousel = async (props = {}) => {
13
+ const wrapper = mount(TagCarousel, {
14
+ props: {
15
+ modelValue: [],
16
+ tagItems,
17
+ ...props,
18
+ },
19
+ attachTo: document.body,
20
+ global: {
21
+ stubs: {
22
+ UnnnicIcon: true,
23
+ },
24
+ },
25
+ });
26
+
27
+ await flushPromises();
28
+ await wrapper.vm.$nextTick();
29
+
30
+ return wrapper;
31
+ };
32
+
33
+ const mockScrollElement = (
34
+ wrapper,
35
+ { scrollWidth = 800, clientWidth = 400, scrollLeft = 0 } = {},
36
+ ) => {
37
+ const element = wrapper.find('#scroll').element;
38
+
39
+ Object.defineProperty(element, 'scrollWidth', {
40
+ configurable: true,
41
+ value: scrollWidth,
42
+ });
43
+ Object.defineProperty(element, 'clientWidth', {
44
+ configurable: true,
45
+ value: clientWidth,
46
+ });
47
+
48
+ let currentScrollLeft = scrollLeft;
49
+ Object.defineProperty(element, 'scrollLeft', {
50
+ configurable: true,
51
+ get: () => currentScrollLeft,
52
+ set: (value) => {
53
+ currentScrollLeft = value;
54
+ },
55
+ });
56
+
57
+ return element;
58
+ };
59
+
60
+ describe('TagCarousel', () => {
61
+ afterEach(() => {
62
+ document.body.innerHTML = '';
63
+ });
64
+
65
+ it('renders tag slides from tagItems', async () => {
66
+ const wrapper = await mountCarousel();
67
+
68
+ expect(wrapper.vm.slides).toHaveLength(3);
69
+ expect(wrapper.findAll('.chip')).toHaveLength(3);
70
+ expect(wrapper.text()).toContain('Tag 1');
71
+ });
72
+
73
+ it('adds a tag to selection when clicked', async () => {
74
+ const wrapper = await mountCarousel();
75
+
76
+ await wrapper.findAll('.chip')[0].trigger('click');
77
+
78
+ expect(wrapper.emitted('selected')[0]).toEqual([[1]]);
79
+ });
80
+
81
+ it('removes a tag from selection when already selected', async () => {
82
+ const wrapper = await mountCarousel({ modelValue: [1] });
83
+
84
+ await wrapper.findAll('.chip')[0].trigger('click');
85
+
86
+ expect(wrapper.emitted('selected')[0]).toEqual([[]]);
87
+ });
88
+
89
+ it('checks whether a tag is included in modelValue', async () => {
90
+ const wrapper = await mountCarousel({ modelValue: [2] });
91
+
92
+ expect(wrapper.vm.checkIsInclude({ id: 2, name: 'Tag 2' })).toBe(true);
93
+ expect(wrapper.vm.checkIsInclude({ id: 1, name: 'Tag 1' })).toBe(false);
94
+ });
95
+
96
+ it('enables previous button after moving forward', async () => {
97
+ const wrapper = await mountCarousel();
98
+ mockScrollElement(wrapper, {
99
+ scrollWidth: 800,
100
+ clientWidth: 400,
101
+ scrollLeft: 0,
102
+ });
103
+
104
+ wrapper.vm.next();
105
+
106
+ expect(wrapper.vm.hasPrev).toBe(true);
107
+ expect(wrapper.vm.hasNext).toBe(true);
108
+ });
109
+
110
+ it('disables next button when scrolled to the end', async () => {
111
+ const wrapper = await mountCarousel();
112
+ mockScrollElement(wrapper, {
113
+ scrollWidth: 800,
114
+ clientWidth: 400,
115
+ scrollLeft: 200,
116
+ });
117
+
118
+ wrapper.vm.next();
119
+
120
+ expect(wrapper.vm.hasNext).toBe(false);
121
+ });
122
+
123
+ it('disables previous button when scrolled to the start', async () => {
124
+ const wrapper = await mountCarousel();
125
+ mockScrollElement(wrapper, {
126
+ scrollWidth: 800,
127
+ clientWidth: 400,
128
+ scrollLeft: 200,
129
+ });
130
+
131
+ wrapper.vm.previous();
132
+
133
+ expect(wrapper.vm.hasPrev).toBe(false);
134
+ expect(wrapper.vm.hasNext).toBe(true);
135
+ });
136
+
137
+ it('keeps next enabled when already disabled and not at end', async () => {
138
+ const wrapper = await mountCarousel();
139
+ mockScrollElement(wrapper, {
140
+ scrollWidth: 900,
141
+ clientWidth: 400,
142
+ scrollLeft: 0,
143
+ });
144
+ wrapper.vm.hasNext = false;
145
+
146
+ wrapper.vm.next();
147
+
148
+ expect(wrapper.vm.hasNext).toBe(true);
149
+ });
150
+
151
+ it('keeps previous enabled when already disabled and not at start', async () => {
152
+ const wrapper = await mountCarousel();
153
+ mockScrollElement(wrapper, {
154
+ scrollWidth: 900,
155
+ clientWidth: 400,
156
+ scrollLeft: 500,
157
+ });
158
+
159
+ wrapper.vm.previous();
160
+
161
+ expect(wrapper.vm.hasPrev).toBe(true);
162
+ expect(wrapper.vm.hasNext).toBe(true);
163
+ });
164
+
165
+ it('shows blur overlays when navigation is available', async () => {
166
+ const wrapper = await mountCarousel();
167
+ mockScrollElement(wrapper, {
168
+ scrollWidth: 800,
169
+ clientWidth: 400,
170
+ scrollLeft: 0,
171
+ });
172
+
173
+ expect(wrapper.vm.hasNext).toBe(true);
174
+ expect(wrapper.vm.hasPrev).toBe(false);
175
+
176
+ wrapper.vm.next();
177
+
178
+ expect(wrapper.vm.hasPrev).toBe(true);
179
+ expect(wrapper.vm.hasNext).toBe(true);
180
+ });
181
+
182
+ it('uses default modelValue when omitted', async () => {
183
+ const wrapper = mount(TagCarousel, {
184
+ props: { tagItems },
185
+ attachTo: document.body,
186
+ global: { stubs: { UnnnicIcon: true } },
187
+ });
188
+
189
+ await flushPromises();
190
+ await wrapper.vm.$nextTick();
191
+
192
+ expect(wrapper.vm.modelValue).toEqual([]);
193
+ expect(wrapper.vm.slides).toHaveLength(3);
194
+
195
+ wrapper.unmount();
196
+ });
197
+
198
+ it('uses default tagItems when omitted', async () => {
199
+ const wrapper = mount(TagCarousel, {
200
+ props: { modelValue: [] },
201
+ attachTo: document.body,
202
+ global: { stubs: { UnnnicIcon: true } },
203
+ });
204
+
205
+ await flushPromises();
206
+ await wrapper.vm.$nextTick();
207
+
208
+ expect(wrapper.vm.tagItems).toEqual([]);
209
+ expect(wrapper.vm.slides).toEqual([]);
210
+
211
+ wrapper.unmount();
212
+ });
213
+
214
+ it('keeps next enabled when it is already enabled', async () => {
215
+ const wrapper = await mountCarousel();
216
+ mockScrollElement(wrapper, {
217
+ scrollWidth: 900,
218
+ clientWidth: 400,
219
+ scrollLeft: 0,
220
+ });
221
+ wrapper.vm.hasNext = true;
222
+
223
+ wrapper.vm.handleNext();
224
+
225
+ expect(wrapper.vm.hasNext).toBe(true);
226
+ expect(wrapper.vm.hasPrev).toBe(true);
227
+ });
228
+
229
+ it('keeps previous enabled when it is already enabled', async () => {
230
+ const wrapper = await mountCarousel();
231
+ mockScrollElement(wrapper, {
232
+ scrollWidth: 900,
233
+ clientWidth: 400,
234
+ scrollLeft: 500,
235
+ });
236
+ wrapper.vm.hasPrev = true;
237
+
238
+ wrapper.vm.handlePrevious();
239
+
240
+ expect(wrapper.vm.hasPrev).toBe(true);
241
+ expect(wrapper.vm.hasNext).toBe(true);
242
+ });
243
+
244
+ it('delegates navigation to next and previous handlers', async () => {
245
+ const wrapper = await mountCarousel();
246
+ mockScrollElement(wrapper, {
247
+ scrollWidth: 800,
248
+ clientWidth: 400,
249
+ scrollLeft: 0,
250
+ });
251
+
252
+ wrapper.vm.handleNext();
253
+ expect(wrapper.vm.hasPrev).toBe(true);
254
+
255
+ wrapper.vm.handlePrevious();
256
+ expect(wrapper.vm.hasPrev).toBe(false);
257
+ });
258
+ });
@@ -0,0 +1,170 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+
4
+ import ChartLine from '../ChartLine.vue';
5
+
6
+ const sampleData = [
7
+ { title: 'Jan', value: 10 },
8
+ { title: 'Feb', value: 20 },
9
+ { title: 'Mar', value: 15 },
10
+ ];
11
+
12
+ const setChartWidth = (wrapper, width) => {
13
+ Object.defineProperty(wrapper.vm.$refs.chart, 'offsetWidth', {
14
+ configurable: true,
15
+ value: width,
16
+ });
17
+ };
18
+
19
+ const mountChart = (props = {}) => {
20
+ const wrapper = mount(ChartLine, {
21
+ props: {
22
+ data: sampleData,
23
+ ...props,
24
+ },
25
+ attachTo: document.body,
26
+ global: {
27
+ stubs: {
28
+ UnnnicToolTip: {
29
+ name: 'UnnnicToolTip',
30
+ template:
31
+ '<div class="tooltip-stub" :style="$attrs.style"><slot /></div>',
32
+ inheritAttrs: false,
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ setChartWidth(wrapper, 300);
39
+ wrapper.vm.chartContainerWidth = 300;
40
+
41
+ return wrapper;
42
+ };
43
+
44
+ describe('ChartLine', () => {
45
+ beforeEach(() => {
46
+ vi.useFakeTimers();
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.useRealTimers();
51
+ document.body.innerHTML = '';
52
+ });
53
+
54
+ it('renders title and data labels', () => {
55
+ const wrapper = mountChart({ title: 'Monthly trend' });
56
+
57
+ expect(wrapper.find('.header .title').text()).toBe('Monthly trend');
58
+ expect(
59
+ wrapper.findAll('.groups .title').map((node) => node.text()),
60
+ ).toEqual(['Jan', 'Feb', 'Mar']);
61
+ });
62
+
63
+ it('omits header when title is not provided', () => {
64
+ const wrapper = mountChart();
65
+
66
+ expect(wrapper.find('.header').exists()).toBe(false);
67
+ });
68
+
69
+ it('applies condensed class when prop is true', () => {
70
+ const wrapper = mountChart({ condensed: true });
71
+
72
+ expect(wrapper.classes()).toContain('condensed');
73
+ });
74
+
75
+ it('uses fixedMaxValue when provided', () => {
76
+ const wrapper = mountChart({ fixedMaxValue: 50 });
77
+
78
+ expect(wrapper.vm.maxValue).toBe(50);
79
+ });
80
+
81
+ it('computes maxValue from data when fixedMaxValue is absent', () => {
82
+ const wrapper = mountChart();
83
+
84
+ expect(wrapper.vm.maxValue).toBe(20);
85
+ });
86
+
87
+ it('formats reference values with one decimal when maxValue is 1 or less', () => {
88
+ const wrapper = mountChart({
89
+ fixedMaxValue: 1,
90
+ data: [{ title: 'A', value: 0.5 }],
91
+ });
92
+
93
+ expect(wrapper.vm.value(0.5)).toBe('0.5');
94
+ expect(wrapper.text()).toContain('0.5');
95
+ });
96
+
97
+ it('formats reference values as integers when maxValue is greater than 1', () => {
98
+ const wrapper = mountChart();
99
+
100
+ expect(wrapper.vm.value(10.2)).toBe('11');
101
+ });
102
+
103
+ it('generates svg background image for multiple data points', () => {
104
+ const wrapper = mountChart();
105
+
106
+ expect(wrapper.vm.svgChart).toContain('data:image/svg+xml');
107
+ expect(wrapper.vm.svgChart).toContain('linearGradient');
108
+ });
109
+
110
+ it('generates svg background image for a single data point', () => {
111
+ const wrapper = mountChart({
112
+ data: [{ title: 'Only', value: 8 }],
113
+ });
114
+
115
+ expect(wrapper.vm.svgChart).toContain('data:image/svg+xml');
116
+ });
117
+
118
+ it('updates chart container width on interval', async () => {
119
+ const wrapper = mountChart();
120
+
121
+ setChartWidth(wrapper, 320);
122
+ vi.advanceTimersByTime(100);
123
+ await wrapper.vm.$nextTick();
124
+
125
+ expect(wrapper.vm.chartContainerWidth).toBe(320);
126
+ });
127
+
128
+ it('clears width interval on unmount', () => {
129
+ const wrapper = mountChart();
130
+ const intervalId = wrapper.vm.chartContainerWidthInterval;
131
+ const clearSpy = vi.spyOn(globalThis, 'clearInterval');
132
+
133
+ wrapper.unmount();
134
+
135
+ expect(clearSpy).toHaveBeenCalledWith(intervalId);
136
+ });
137
+
138
+ it('clears width interval on unmount even when interval is unset', () => {
139
+ const wrapper = mountChart();
140
+ wrapper.vm.chartContainerWidthInterval = null;
141
+ const clearSpy = vi.spyOn(globalThis, 'clearInterval');
142
+
143
+ wrapper.unmount();
144
+
145
+ expect(clearSpy).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it('covers svg path generation for more than two bars', () => {
149
+ const wrapper = mountChart({
150
+ data: [
151
+ { title: 'A', value: 4 },
152
+ { title: 'B', value: 8 },
153
+ { title: 'C', value: 6 },
154
+ { title: 'D', value: 2 },
155
+ ],
156
+ });
157
+
158
+ expect(wrapper.vm.svgChart).toContain('linearGradient');
159
+ expect(wrapper.findAll('.tooltip-stub')).toHaveLength(4);
160
+ });
161
+
162
+ it('renders tooltip bars with proportional heights', () => {
163
+ const wrapper = mountChart();
164
+ const tooltips = wrapper.findAll('.tooltip-stub');
165
+
166
+ expect(tooltips).toHaveLength(3);
167
+ expect(tooltips[1].attributes('style')).toContain('height: 100%');
168
+ expect(tooltips[0].attributes('style')).toContain('height: 50%');
169
+ });
170
+ });
@@ -48,4 +48,18 @@ describe('Checkbox', () => {
48
48
  await checkbox.trigger('click');
49
49
  expect(wrapper.emitted('change')).eq(undefined);
50
50
  });
51
+
52
+ it('should render helper text', () => {
53
+ wrapper = createWrapper({ helper: 'Helper message' });
54
+ expect(wrapper.find('.unnnic-checkbox__helper').text()).toBe(
55
+ 'Helper message',
56
+ );
57
+ });
58
+
59
+ it('should render label prop', () => {
60
+ wrapper = createWrapper({ label: 'Checkbox label' });
61
+ expect(wrapper.find('[data-testid="checkbox-text-right"]').text()).toBe(
62
+ 'Checkbox label',
63
+ );
64
+ });
51
65
  });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+
4
+ import CheckboxGroup from '../CheckboxGroup.vue';
5
+
6
+ const mountCheckboxGroup = (props = {}, slots = {}) =>
7
+ mount(CheckboxGroup, {
8
+ props,
9
+ slots: {
10
+ default: '<input type="checkbox" data-testid="checkbox-slot" />',
11
+ ...slots,
12
+ },
13
+ global: {
14
+ stubs: {
15
+ UnnnicLabel: {
16
+ props: ['label', 'tooltip'],
17
+ template:
18
+ '<label data-testid="checkbox-group-label">{{ label }}</label>',
19
+ },
20
+ },
21
+ },
22
+ });
23
+
24
+ describe('CheckboxGroup', () => {
25
+ it('renders slot content', () => {
26
+ const wrapper = mountCheckboxGroup();
27
+
28
+ expect(wrapper.find('[data-testid="checkbox-slot"]').exists()).toBe(true);
29
+ });
30
+
31
+ it('renders label when provided', () => {
32
+ const wrapper = mountCheckboxGroup({ label: 'Group label' });
33
+
34
+ expect(wrapper.find('[data-testid="checkbox-group-label"]').text()).toBe(
35
+ 'Group label',
36
+ );
37
+ });
38
+
39
+ it('renders helper text when provided', () => {
40
+ const wrapper = mountCheckboxGroup({ helper: 'Helper text' });
41
+
42
+ expect(wrapper.find('.unnnic-checkbox-group__helper').text()).toBe(
43
+ 'Helper text',
44
+ );
45
+ });
46
+
47
+ it('applies horizontal state class by default', () => {
48
+ const wrapper = mountCheckboxGroup();
49
+
50
+ expect(wrapper.classes()).toContain(
51
+ 'unnnic-checkbox-group--state-horizontal',
52
+ );
53
+ });
54
+
55
+ it('applies vertical state class', () => {
56
+ const wrapper = mountCheckboxGroup({ state: 'vertical' });
57
+
58
+ expect(wrapper.classes()).toContain(
59
+ 'unnnic-checkbox-group--state-vertical',
60
+ );
61
+ });
62
+ });