@weni/unnnic-system 2.13.0 → 2.13.1

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 (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +56 -19
  3. package/dist/style.css +1 -1
  4. package/dist/unnnic.mjs +4546 -4471
  5. package/dist/unnnic.umd.js +19 -19
  6. package/package.json +1 -1
  7. package/src/components/Alert/Alert.vue +2 -0
  8. package/src/components/Alert/AlertBanner.vue +2 -0
  9. package/src/components/Alert/Version1dot1.vue +1 -0
  10. package/src/components/Alert/__tests__/Alert.spec.js +84 -0
  11. package/src/components/Alert/__tests__/AlertBanner.spec.js +89 -0
  12. package/src/components/Alert/__tests__/AlertCaller.spec.js +98 -0
  13. package/src/components/Alert/__tests__/Version1dot1.spec.js +124 -0
  14. package/src/components/AudioRecorder/AudioRecorder.vue +30 -1
  15. package/src/components/AvatarIcon/__tests__/AvatarIcon.spec.js +84 -0
  16. package/src/components/AvatarIcon/__tests__/__snapshots__/AvatarIcon.spec.js.snap +7 -0
  17. package/src/components/Breadcrumb/Breadcrumb.vue +4 -0
  18. package/src/components/Breadcrumb/__tests__/Breadcrumb.spec.js +68 -0
  19. package/src/components/FormElement/FormElement.vue +101 -28
  20. package/src/components/Input/BaseInput.vue +30 -38
  21. package/src/components/Input/Input.scss +43 -0
  22. package/src/components/Input/TextInput.vue +24 -25
  23. package/src/components/Input/__test__/TextInput.spec.js +2 -2
  24. package/src/components/Input/__test__/__snapshots__/Input.spec.js.snap +1 -1
  25. package/src/components/Input/__test__/__snapshots__/TextInput.spec.js.snap +1 -1
  26. package/src/components/Pagination/Pagination.vue +23 -4
  27. package/src/components/Pagination/__tests__/Pagination.spec.js +208 -0
  28. package/src/components/SelectSmart/SelectSmart.vue +16 -44
  29. package/src/components/SelectSmart/SelectSmartMultipleHeader.vue +5 -13
  30. package/src/components/SelectSmart/SelectSmartOption.vue +13 -9
  31. package/src/components/SkeletonLoading/SkeletonLoading.vue +17 -11
  32. package/src/components/SkeletonLoading/__tests__/SkeletonLoading.spec.js +125 -0
  33. package/src/components/TextArea/TextArea.vue +45 -128
  34. package/src/components/TextArea/__test__/TextArea.spec.js +26 -24
  35. package/src/components/TextArea/__test__/__snapshots__/TextArea.spec.js.snap +4 -4
  36. package/src/stories/Input.mdx +76 -0
  37. package/src/stories/Input.stories.js +82 -8
  38. package/src/stories/SelectSmart.mdx +15 -17
  39. package/src/stories/SelectSmart.stories.js +72 -6
  40. package/src/stories/TextArea.mdx +68 -0
  41. package/src/stories/TextArea.stories.js +68 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weni/unnnic-system",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "type": "commonjs",
5
5
  "files": [
6
6
  "dist",
@@ -7,6 +7,7 @@
7
7
  :icon="icon"
8
8
  :scheme="scheme"
9
9
  size="sm"
10
+ data-test="unnnic-icon"
10
11
  />
11
12
  <div class="unnnic-alert__content">
12
13
  <div class="unnnic-alert__title">
@@ -29,6 +30,7 @@
29
30
  icon="close-1"
30
31
  scheme="brand-sec"
31
32
  size="xs"
33
+ data-test="unnnic-icon-close"
32
34
  @click="onClose"
33
35
  />
34
36
  </div>
@@ -18,6 +18,7 @@
18
18
  :icon="getIconType(type)"
19
19
  size="sm"
20
20
  scheme="neutral-white"
21
+ data-test="unnnic-icon"
21
22
  />
22
23
  <p class="text">{{ text }}</p>
23
24
  <a
@@ -39,6 +40,7 @@
39
40
  icon="close"
40
41
  size="sm"
41
42
  scheme="neutral-white"
43
+ data-test="unnnic-close-icon"
42
44
  />
43
45
  </div>
44
46
  </section>
@@ -91,6 +91,7 @@ export default {
91
91
  default: 'default',
92
92
  },
93
93
  },
94
+ emits: ['close'],
94
95
 
95
96
  mounted() {
96
97
  this.$refs.progress.addEventListener('animationend', () => {
@@ -0,0 +1,84 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { beforeEach, describe, expect, afterEach, test, vi } from 'vitest';
3
+ import UnnnicAlert from '@/components/Alert/Alert.vue';
4
+
5
+ describe('UnnnicAlert.vue', () => {
6
+ let wrapper;
7
+
8
+ beforeEach(() => {
9
+ wrapper = mount(UnnnicAlert, {
10
+ props: {
11
+ version: '1.0',
12
+ title: 'Test Alert',
13
+ text: 'This is an alert message',
14
+ scheme: 'primary',
15
+ icon: 'alert-icon',
16
+ position: 'top-right',
17
+ closeText: 'Close',
18
+ },
19
+ });
20
+ });
21
+
22
+ afterEach(() => {
23
+ wrapper.unmount();
24
+ });
25
+
26
+ test('renders correctly', () => {
27
+ expect(wrapper.exists()).toBe(true);
28
+ });
29
+
30
+ test('renders title and text in uppercase', () => {
31
+ const title = wrapper.find('.unnnic-alert__title');
32
+ const text = wrapper.find('.unnnic-alert__text');
33
+
34
+ expect(title.text()).toBe('TEST ALERT');
35
+ expect(text.text()).toBe('This is an alert message');
36
+ });
37
+
38
+ test('applies the correct position class', () => {
39
+ expect(wrapper.classes()).toContain('unnnic-alert-position--top-right');
40
+ });
41
+
42
+ test('renders the icon with correct props', () => {
43
+ const icon = wrapper.findComponent('[data-test="unnnic-icon"]');
44
+ expect(icon.exists()).toBe(true);
45
+ expect(icon.props('icon')).toBe('alert-icon');
46
+ expect(icon.props('scheme')).toBe('primary');
47
+ expect(icon.props('size')).toBe('sm');
48
+ });
49
+
50
+ test('renders closeText and calls onClose when clicked', async () => {
51
+ const closeText = wrapper.find('.unnnic-alert__close-text');
52
+ expect(closeText.exists()).toBe(true);
53
+ expect(closeText.text()).toBe('CLOSE');
54
+
55
+ const onCloseSpy = vi.spyOn(wrapper.props(), 'onClose');
56
+ await closeText.trigger('click');
57
+ expect(onCloseSpy).toHaveBeenCalled();
58
+ });
59
+
60
+ test('renders default close icon if closeText is not provided', async () => {
61
+ await wrapper.setProps({ closeText: '', onClose: () => vi.fn() });
62
+ await wrapper.vm.$nextTick();
63
+ const closeIcon = wrapper.findComponent('[data-test="unnnic-icon-close"]');
64
+ expect(closeIcon.exists()).toBe(true);
65
+ expect(closeIcon.props('icon')).toBe('close-1');
66
+ expect(closeIcon.props('scheme')).toBe('brand-sec');
67
+ expect(closeIcon.props('size')).toBe('xs');
68
+ });
69
+
70
+ test('renders the correct component for version 1.1', async () => {
71
+ await wrapper.setProps({ version: '1.1' });
72
+ const versionComponent = wrapper.findComponent({ name: 'Version1dot1' });
73
+ expect(versionComponent.exists()).toBe(true);
74
+ });
75
+
76
+ test('does not render alert if version is not 1.0', async () => {
77
+ await wrapper.setProps({ version: '1.2' });
78
+ expect(wrapper.find('.unnnic-alert').exists()).toBe(false);
79
+ });
80
+
81
+ test('matches the snapshot', () => {
82
+ expect(wrapper.html()).toMatchSnapshot();
83
+ });
84
+ });
@@ -0,0 +1,89 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import AlertBanner from '@/components/Alert/AlertBanner.vue';
4
+
5
+ describe('AlertBanner.vue', () => {
6
+ let wrapper;
7
+
8
+ beforeEach(() => {
9
+ wrapper = mount(AlertBanner, {
10
+ props: {
11
+ text: 'This is a banner alert',
12
+ type: 'danger',
13
+ showCloseButton: true,
14
+ linkHref: 'https://example.com',
15
+ linkText: 'Learn more',
16
+ linkTarget: '_self',
17
+ onClose: vi.fn(),
18
+ },
19
+ });
20
+ });
21
+
22
+ afterEach(() => {
23
+ wrapper.unmount();
24
+ });
25
+
26
+ test('renders correctly', () => {
27
+ expect(wrapper.exists()).toBe(true);
28
+ });
29
+
30
+ test('renders the correct text and link', () => {
31
+ const textElement = wrapper.find('.text');
32
+ const linkElement = wrapper.find('.banner-alert__link');
33
+
34
+ expect(textElement.text()).toBe('This is a banner alert');
35
+ expect(linkElement.text()).toBe('Learn more');
36
+ expect(linkElement.attributes('href')).toBe('https://example.com');
37
+ expect(linkElement.attributes('target')).toBe('_self');
38
+ });
39
+
40
+ test('applies the correct class based on the type', () => {
41
+ expect(wrapper.classes()).toContain('banner-alert--banner-danger');
42
+ });
43
+
44
+ test('renders the correct icon based on the type', () => {
45
+ const icon = wrapper.findComponent('[data-test="unnnic-icon"]');
46
+ expect(icon.exists()).toBe(true);
47
+ expect(icon.props('icon')).toBe('block');
48
+ expect(icon.props('size')).toBe('sm');
49
+ expect(icon.props('scheme')).toBe('neutral-white');
50
+ });
51
+
52
+ test('does not render the icon when type is not danger, warning, or info', async () => {
53
+ await wrapper.setProps({ type: 'success' });
54
+ const icon = wrapper.findComponent('[data-test="unnnic-icon"]');
55
+ expect(icon.element.style.display).toBe('none');
56
+ });
57
+
58
+ test('calls emitClose and emits close event when close button is clicked', async () => {
59
+ const closeButton = wrapper.find('.banner-alert__close');
60
+ await closeButton.trigger('click');
61
+
62
+ expect(wrapper.emitted()).toHaveProperty('close');
63
+ expect(wrapper.props().onClose).toHaveBeenCalled();
64
+ });
65
+
66
+ test('does not render close button if showCloseButton is false', async () => {
67
+ await wrapper.setProps({ showCloseButton: false });
68
+ const closeButton = wrapper.find('.banner-alert__close');
69
+ expect(closeButton.element.style.display).toBe('none');
70
+ });
71
+
72
+ test('matches the snapshot', () => {
73
+ expect(wrapper.html()).toMatchSnapshot();
74
+ });
75
+
76
+ test('getIconType function returns the correct icon based on type', () => {
77
+ expect(wrapper.vm.getIconType('danger')).toBe('block');
78
+ expect(wrapper.vm.getIconType('info')).toBe('info');
79
+ expect(wrapper.vm.getIconType('')).toBe('info');
80
+ });
81
+
82
+ test('isShowTextIcon function returns true for danger, warning, and info types', () => {
83
+ expect(wrapper.vm.isShowTextIcon('danger')).toBe(true);
84
+ expect(wrapper.vm.isShowTextIcon('warning')).toBe(true);
85
+ expect(wrapper.vm.isShowTextIcon('info')).toBe(true);
86
+ expect(wrapper.vm.isShowTextIcon('')).toBe(true);
87
+ expect(wrapper.vm.isShowTextIcon('success')).toBe(false);
88
+ });
89
+ });
@@ -0,0 +1,98 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
3
+ import AlertCaller from '@/components/Alert/AlertCaller.vue';
4
+ import alert from '@/utils/call';
5
+
6
+ vi.mock('@/utils/call', () => ({
7
+ default: {
8
+ callAlert: vi.fn(),
9
+ },
10
+ }));
11
+
12
+ describe('AlertCaller.vue', () => {
13
+ let wrapper;
14
+
15
+ const defaultProps = {
16
+ title: 'Test Title',
17
+ text: 'Test Text',
18
+ icon: 'test-icon',
19
+ enabled: true,
20
+ scheme: 'primary',
21
+ seconds: 5,
22
+ closeText: 'Close',
23
+ position: 'top-right',
24
+ };
25
+
26
+ beforeEach(() => {
27
+ wrapper = mount(AlertCaller, {
28
+ props: defaultProps,
29
+ });
30
+ });
31
+
32
+ afterEach(() => {
33
+ wrapper.unmount();
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ test('renders correctly', () => {
38
+ expect(wrapper.exists()).toBe(true);
39
+ expect(wrapper.find('button').text()).toBe('Launch alert');
40
+ });
41
+
42
+ test('calls callAlert method when button is clicked', async () => {
43
+ const callAlertSpy = vi.spyOn(wrapper.vm, 'callAlert');
44
+ await wrapper.find('button').trigger('click');
45
+ expect(callAlertSpy).toHaveBeenCalledTimes(1);
46
+ });
47
+
48
+ test('callAlert method invokes alert.callAlert with correct props', async () => {
49
+ await wrapper.find('button').trigger('click');
50
+ expect(alert.callAlert).toHaveBeenCalledWith({
51
+ props: defaultProps,
52
+ seconds: defaultProps.seconds,
53
+ });
54
+ });
55
+
56
+ test.each([
57
+ ['title', 'New Title'],
58
+ ['text', 'New Text'],
59
+ ['icon', 'new-icon'],
60
+ ['enabled', false],
61
+ ['scheme', 'secondary'],
62
+ ['seconds', 10],
63
+ ['closeText', 'Dismiss'],
64
+ ['position', 'bottom-left'],
65
+ ])('updates %s prop correctly', async (propName, newValue) => {
66
+ await wrapper.setProps({ [propName]: newValue });
67
+ await wrapper.find('button').trigger('click');
68
+ const expectedProps = { ...defaultProps, [propName]: newValue };
69
+ expect(alert.callAlert).toHaveBeenCalledWith({
70
+ props: expectedProps,
71
+ seconds: expectedProps.seconds,
72
+ });
73
+ });
74
+
75
+ test('passes default values when props are not provided', async () => {
76
+ wrapper = mount(AlertCaller, {
77
+ props: {},
78
+ });
79
+ await wrapper.find('button').trigger('click');
80
+ expect(alert.callAlert).toHaveBeenCalledWith({
81
+ props: {
82
+ title: null,
83
+ text: null,
84
+ icon: null,
85
+ enabled: true,
86
+ scheme: null,
87
+ seconds: 3,
88
+ closeText: null,
89
+ position: 'top-right',
90
+ },
91
+ seconds: 3,
92
+ });
93
+ });
94
+
95
+ test('matches snapshot', () => {
96
+ expect(wrapper.html()).toMatchSnapshot();
97
+ });
98
+ });
@@ -0,0 +1,124 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
3
+ import Alert from '@/components/Alert/Version1dot1.vue';
4
+ import UnnnicIcon from '@/components/Icon.vue';
5
+
6
+ vi.mock('@/components/Icon.vue');
7
+
8
+ describe('Alert.vue', () => {
9
+ let wrapper;
10
+
11
+ const defaultProps = {
12
+ version: '1.0',
13
+ text: 'Test Alert',
14
+ scheme: 'feedback-green',
15
+ onClose: vi.fn(),
16
+ linkHref: 'https://example.com',
17
+ linkTarget: '_blank',
18
+ linkText: 'Learn more',
19
+ type: 'success',
20
+ };
21
+
22
+ beforeEach(() => {
23
+ wrapper = mount(Alert, {
24
+ props: defaultProps,
25
+ global: {
26
+ stubs: {
27
+ UnnnicIcon: true,
28
+ },
29
+ },
30
+ });
31
+ });
32
+
33
+ afterEach(() => {
34
+ wrapper.unmount();
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ test('renders correctly', () => {
39
+ expect(wrapper.exists()).toBe(true);
40
+ expect(wrapper.find('.alert').exists()).toBe(true);
41
+ expect(wrapper.find('.alert__text').text()).toBe('Test Alert');
42
+ });
43
+
44
+ test('applies correct classes based on scheme and type props', () => {
45
+ expect(wrapper.find('.alert--scheme-aux-green').exists()).toBe(true);
46
+
47
+ wrapper = mount(Alert, {
48
+ props: { ...defaultProps, scheme: 'feedback-red', type: 'error' },
49
+ global: { stubs: { UnnnicIcon: true } },
50
+ });
51
+ expect(wrapper.find('.alert--scheme-aux-red').exists()).toBe(true);
52
+ });
53
+
54
+ test('renders link when linkHref is provided', () => {
55
+ const link = wrapper.find('.alert__link');
56
+ expect(link.exists()).toBe(true);
57
+ expect(link.attributes('href')).toBe('https://example.com');
58
+ expect(link.attributes('target')).toBe('_blank');
59
+ expect(link.text()).toBe('Learn more');
60
+ });
61
+
62
+ test('does not render link when linkHref is not provided', async () => {
63
+ await wrapper.setProps({ linkHref: '' });
64
+ expect(wrapper.find('.alert__link').exists()).toBe(false);
65
+ });
66
+
67
+ test('emits close event and calls onClose prop when close button is clicked', async () => {
68
+ await wrapper.find('.alert__close').trigger('click');
69
+ expect(wrapper.emitted('close')).toBeTruthy();
70
+ expect(defaultProps.onClose).toHaveBeenCalled();
71
+ });
72
+
73
+ test('adds slide-down class on progress animation end', async () => {
74
+ const progressElement = wrapper.find('.alert__progress');
75
+ await progressElement.trigger('animationend');
76
+ expect(wrapper.find('.alert-container').classes()).toContain('slide-down');
77
+ });
78
+
79
+ test('emits close event on slide-down animation end', async () => {
80
+ const alertContainer = wrapper.find('.alert-container');
81
+ await alertContainer.trigger('animationend', {
82
+ animationName: 'slideDown',
83
+ });
84
+ expect(wrapper.emitted('close')).toBeTruthy();
85
+ });
86
+
87
+ test('does not emit close event on non-slideDown animation end', async () => {
88
+ const alertContainer = wrapper.find('.alert-container');
89
+ await alertContainer.trigger('animationend', {
90
+ animationName: 'someOtherAnimation',
91
+ });
92
+ expect(wrapper.emitted('close')).toBeFalsy();
93
+ });
94
+
95
+ test.each([
96
+ ['version', '1.1'],
97
+ ['text', 'New Alert Text'],
98
+ ['scheme', 'feedback-red'],
99
+ ['linkHref', 'https://newexample.com'],
100
+ ['linkTarget', '_self'],
101
+ ['linkText', 'Click here'],
102
+ ['type', 'error'],
103
+ ])('updates %s prop correctly', async (propName, newValue) => {
104
+ await wrapper.setProps({ [propName]: newValue });
105
+ expect(wrapper.props(propName)).toBe(newValue);
106
+ });
107
+
108
+ test('renders UnnnicIcon component', () => {
109
+ expect(wrapper.findComponent(UnnnicIcon).exists()).toBe(true);
110
+ expect(wrapper.findComponent(UnnnicIcon).props()).toEqual({
111
+ clickable: false,
112
+ filled: false,
113
+ icon: 'close-1',
114
+ lineHeight: null,
115
+ next: false,
116
+ size: 'sm',
117
+ scheme: 'neutral-white',
118
+ });
119
+ });
120
+
121
+ test('matches snapshot', () => {
122
+ expect(wrapper.html()).toMatchSnapshot();
123
+ });
124
+ });
@@ -165,7 +165,8 @@ export default {
165
165
  }
166
166
 
167
167
  this.audio = new Audio();
168
- this.audio.setAttribute('src', this.src);
168
+ this.audio.src = this.src;
169
+ this.duration = await this.getBlobDuration(this.src);
169
170
 
170
171
  this.addAudioEventListeners();
171
172
  },
@@ -176,6 +177,34 @@ export default {
176
177
  },
177
178
 
178
179
  methods: {
180
+ getBlobDuration(blob) {
181
+ const tempAudioEl = document.createElement('audio');
182
+
183
+ const durationPromisse = new Promise((resolve, reject) => {
184
+ tempAudioEl.addEventListener('loadedmetadata', () => {
185
+ // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=642012
186
+ if (tempAudioEl.duration === Infinity) {
187
+ tempAudioEl.currentTime = Number.MAX_SAFE_INTEGER;
188
+ tempAudioEl.ontimeupdate = () => {
189
+ tempAudioEl.ontimeupdate = null;
190
+ resolve(tempAudioEl.duration);
191
+ tempAudioEl.currentTime = 0;
192
+ };
193
+ }
194
+ // Normal behavior
195
+ else resolve(tempAudioEl.duration);
196
+ });
197
+ tempAudioEl.onerror = (event) => reject(event.target.error);
198
+ });
199
+
200
+ tempAudioEl.src =
201
+ typeof blob === 'string' || blob instanceof String
202
+ ? blob
203
+ : window.URL.createObjectURL(blob);
204
+
205
+ return durationPromisse;
206
+ },
207
+
179
208
  // entry point; accessed by external components
180
209
  async record() {
181
210
  if (this.hasInUseRecordDevice()) return;
@@ -0,0 +1,84 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, test, expect, beforeEach } from 'vitest';
3
+ import AvatarIcon from '@/components/AvatarIcon/AvatarIcon.vue';
4
+
5
+ describe('AvatarIcon.vue', () => {
6
+ let wrapper;
7
+
8
+ beforeEach(() => {
9
+ wrapper = mount(AvatarIcon, {
10
+ props: {
11
+ enabled: true,
12
+ icon: 'graph-stats-1',
13
+ size: 'sm',
14
+ scheme: 'aux-blue',
15
+ filled: false,
16
+ opacity: true,
17
+ },
18
+ global: {
19
+ stubs: {
20
+ UnnnicIcon: true,
21
+ },
22
+ },
23
+ });
24
+ });
25
+
26
+ test('renders the component correctly', () => {
27
+ expect(wrapper.exists()).toBe(true);
28
+ });
29
+
30
+ test('applies the correct classes based on props', async () => {
31
+ expect(wrapper.classes()).toContain('unnnic-avatar-icon');
32
+ expect(wrapper.classes()).toContain('aux-blue');
33
+ expect(wrapper.classes()).toContain('sm');
34
+ expect(wrapper.classes()).not.toContain('disabled');
35
+ expect(wrapper.classes()).not.toContain('filled');
36
+ expect(wrapper.classes()).not.toContain('opacity');
37
+
38
+ await wrapper.setProps({
39
+ enabled: false,
40
+ size: 'lg',
41
+ filled: true,
42
+ opacity: false,
43
+ });
44
+ expect(wrapper.classes()).toContain('lg');
45
+ expect(wrapper.classes()).toContain('disabled');
46
+ expect(wrapper.classes()).toContain('opacity');
47
+ });
48
+
49
+ test('renders UnnnicIcon with correct props', () => {
50
+ const icon = wrapper.findComponent({ name: 'UnnnicIcon' });
51
+ expect(icon.exists()).toBe(true);
52
+ expect(icon.props('icon')).toBe('graph-stats-1');
53
+ expect(icon.props('scheme')).toBe('aux-blue');
54
+ expect(icon.props('size')).toBe('md');
55
+ expect(icon.props('filled')).toBe(false);
56
+ });
57
+
58
+ test('computes iconSize correctly based on size prop', async () => {
59
+ expect(wrapper.vm.iconSize).toBe('md');
60
+
61
+ await wrapper.setProps({ size: 'nano' });
62
+ expect(wrapper.vm.iconSize).toBe('xs');
63
+
64
+ await wrapper.setProps({ size: 'xl' });
65
+ expect(wrapper.vm.iconSize).toBe('xl');
66
+ });
67
+
68
+ test('renders neutral-cloudy scheme when enabled is false', async () => {
69
+ await wrapper.setProps({ enabled: false });
70
+ const icon = wrapper.findComponent({ name: 'UnnnicIcon' });
71
+ expect(icon.props('scheme')).toBe('neutral-cloudy');
72
+ });
73
+
74
+ test('applies opacity class based on prop', async () => {
75
+ expect(wrapper.classes()).not.toContain('opacity');
76
+
77
+ await wrapper.setProps({ opacity: false });
78
+ expect(wrapper.classes()).toContain('opacity');
79
+ });
80
+
81
+ test('matches the snapshot', () => {
82
+ expect(wrapper.html()).toMatchSnapshot();
83
+ });
84
+ });
@@ -0,0 +1,7 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`AvatarIcon.vue > matches the snapshot 1`] = `
4
+ "<div data-v-5f185a90="" class="unnnic-avatar-icon aux-blue sm">
5
+ <unnnic-icon-stub data-v-5f185a90="" filled="false" next="false" icon="graph-stats-1" clickable="false" size="md" scheme="aux-blue"></unnnic-icon-stub>
6
+ </div>"
7
+ `;
@@ -10,6 +10,7 @@
10
10
  'unnnic-breadcrumb__container__link__active':
11
11
  index === crumbs.length - 1,
12
12
  }"
13
+ :data-test="crumb.name"
13
14
  @click="$emit('crumb-click', crumb)"
14
15
  >
15
16
  {{ crumb.name }}
@@ -17,6 +18,7 @@
17
18
 
18
19
  <UnnnicIcon
19
20
  v-if="index !== crumbs.length - 1"
21
+ data-test="right-icon"
20
22
  icon="arrow-right-1-1"
21
23
  size="xs"
22
24
  lineHeight="md"
@@ -40,6 +42,8 @@ export default {
40
42
  validator: (crumb) => crumb.every((c) => c.name !== null),
41
43
  },
42
44
  },
45
+
46
+ emits: ['crumb-click'],
43
47
  };
44
48
  </script>
45
49
 
@@ -0,0 +1,68 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import Breadcrumb from '../Breadcrumb.vue';
4
+
5
+ function setup(props) {
6
+ return mount(Breadcrumb, { props });
7
+ }
8
+
9
+ describe('Breadcrumb', () => {
10
+ let wrapper;
11
+
12
+ const crumbs = [
13
+ { name: 'First Page Name', path: '/first-page' },
14
+ { name: 'Second Page Name', path: '/first-page/second-page' },
15
+ { name: 'Last Page Name', path: '/first-page/second-page/last-page' },
16
+ ];
17
+
18
+ beforeEach(() => {
19
+ wrapper = setup({
20
+ crumbs,
21
+ });
22
+ });
23
+
24
+ it('renders the right icon the same amount as the breadcrumbs length minus one', () => {
25
+ const rightIcons = wrapper.findAll('[data-test="right-icon"]');
26
+
27
+ expect(rightIcons.length).toBe(crumbs.length - 1);
28
+ });
29
+
30
+ it.each(crumbs)('renders $name breadcrumb', ({ name }) => {
31
+ const breadcrumb = wrapper.find(`[data-test="${name}"]`);
32
+
33
+ expect(breadcrumb.exists()).toBeTruthy();
34
+ expect(breadcrumb.text()).contains(name);
35
+ });
36
+
37
+ const allCrumbsButLast = crumbs.slice(0, -1);
38
+ const lastCrumb = crumbs.at(-1);
39
+
40
+ it.each(allCrumbsButLast)(
41
+ '$name has not the "active" class name',
42
+ ({ name }) => {
43
+ const breadcrumb = wrapper.find(`[data-test="${name}"]`);
44
+
45
+ expect(breadcrumb.wrapperElement.className).not.contains('active');
46
+ },
47
+ );
48
+
49
+ it(`'${lastCrumb.name}' has the "active" class name`, () => {
50
+ const breadcrumb = wrapper.find(`[data-test="${lastCrumb.name}"]`);
51
+
52
+ expect(breadcrumb.wrapperElement.className).contains('active');
53
+ });
54
+
55
+ describe.each(crumbs)(
56
+ 'when the user clicks on the $name breadcrumb',
57
+ (crumb) => {
58
+ it('emits the crumb-click event with the respective crumb object', () => {
59
+ const breadcrumb = wrapper.find(`[data-test="${crumb.name}"]`);
60
+
61
+ breadcrumb.trigger('click');
62
+
63
+ expect(wrapper.emitted('crumb-click')).toHaveLength(1);
64
+ expect(wrapper.emitted('crumb-click')).toContainEqual([crumb]);
65
+ });
66
+ },
67
+ );
68
+ });