@weni/unnnic-system 2.15.0 → 2.16.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 +12 -0
- package/dist/style.css +1 -1
- package/dist/unnnic.mjs +5802 -5721
- package/dist/unnnic.umd.js +31 -31
- package/package.json +6 -3
- package/src/components/ChartFunnel/ChartFunnel.vue +34 -8
- package/src/components/ChartFunnel/DefaultFunnel/ChartDefaultFunnelBase.vue +195 -0
- package/src/components/ChartFunnel/{ChartFunnelBaseRow.vue → SvgFunnel/ChartFunnelBaseRow.vue} +1 -1
- package/src/components/ChartFunnel/{ChartFunnelFiveRows.vue → SvgFunnel/ChartFunnelFiveRows.vue} +1 -1
- package/src/components/ChartFunnel/{ChartFunnelFourRows.vue → SvgFunnel/ChartFunnelFourRows.vue} +1 -1
- package/src/components/ChartFunnel/{ChartFunnelThreeRows.vue → SvgFunnel/ChartFunnelThreeRows.vue} +1 -1
- package/src/components/Dropdown/Dropdown.vue +1 -0
- package/src/components/Dropdown/DropdownItem.vue +4 -1
- package/src/components/Dropdown/DropdownSkeleton.vue +2 -0
- package/src/components/Dropdown/__tests__/Dropdown.spec.js +98 -0
- package/src/components/Dropdown/__tests__/DropdownItem.spec.js +25 -0
- package/src/components/Dropdown/__tests__/DropdownSkeleton.spec.js +147 -0
- package/src/components/SelectSmart/SelectSmart.vue +5 -0
- package/src/components/SelectSmart/SelectSmartMultipleHeader.vue +2 -0
- package/src/components/SelectSmart/SelectSmartOption.vue +4 -0
- package/src/components/SelectSmart/__tests__/SelectSmart.spec.js +240 -0
- package/src/components/SelectSmart/__tests__/SelectSmartMultipleHeader.spec.js +86 -0
- package/src/components/SelectSmart/__tests__/SelectSmartOption.spec.js +114 -0
- package/src/components/SelectSmart/__tests__/__snapshots__/SelectSmartMultipleHeader.spec.js.snap +8 -0
- package/src/components/SelectSmart/__tests__/__snapshots__/SelectSmartOption.spec.js.snap +10 -0
- package/src/stories/ChartFunnel.stories.js +54 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weni/unnnic-system",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -61,10 +61,12 @@
|
|
|
61
61
|
"@storybook/testing-library": "^0.2.2",
|
|
62
62
|
"@storybook/vue3": "^8.0.10",
|
|
63
63
|
"@storybook/vue3-vite": "^8.0.10",
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
|
65
|
+
"@typescript-eslint/parser": "^8.11.0",
|
|
64
66
|
"@vitejs/plugin-vue": "^4.2.3",
|
|
65
67
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
|
66
|
-
"@vitest/coverage-v8": "^1.6.0",
|
|
67
68
|
"@vitest/coverage-istanbul": "^1.6.0",
|
|
69
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
68
70
|
"@vitest/ui": "^1.6.0",
|
|
69
71
|
"@vue/eslint-config-prettier": "^9.0.0",
|
|
70
72
|
"@vue/test-utils": "^2.4.6",
|
|
@@ -79,8 +81,9 @@
|
|
|
79
81
|
"react-dom": "^18.2.0",
|
|
80
82
|
"sass": "^1.62.1",
|
|
81
83
|
"storybook": "^8.0.10",
|
|
84
|
+
"typescript": "^5.6.3",
|
|
82
85
|
"vite": "4.3.5",
|
|
83
86
|
"vitest": "^1.6.0",
|
|
84
87
|
"vue-eslint-parser": "^9.4.2"
|
|
85
88
|
}
|
|
86
|
-
}
|
|
89
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="chartComponent"
|
|
4
|
-
:data="
|
|
4
|
+
:data="chartData"
|
|
5
5
|
/>
|
|
6
6
|
</template>
|
|
7
7
|
|
|
8
8
|
<script>
|
|
9
|
-
import ChartFunnelThreeRows from './ChartFunnelThreeRows.vue';
|
|
10
|
-
import ChartFunnelFourRows from './ChartFunnelFourRows.vue';
|
|
11
|
-
import ChartFunnelFiveRows from './ChartFunnelFiveRows.vue';
|
|
9
|
+
import ChartFunnelThreeRows from './SvgFunnel/ChartFunnelThreeRows.vue';
|
|
10
|
+
import ChartFunnelFourRows from './SvgFunnel/ChartFunnelFourRows.vue';
|
|
11
|
+
import ChartFunnelFiveRows from './SvgFunnel/ChartFunnelFiveRows.vue';
|
|
12
|
+
import ChartDefaultFunnelBase from './DefaultFunnel/ChartDefaultFunnelBase.vue';
|
|
12
13
|
|
|
13
14
|
export default {
|
|
14
15
|
name: 'UnnnicChartFunnel',
|
|
@@ -17,6 +18,7 @@ export default {
|
|
|
17
18
|
ChartFunnelThreeRows,
|
|
18
19
|
ChartFunnelFourRows,
|
|
19
20
|
ChartFunnelFiveRows,
|
|
21
|
+
ChartDefaultFunnelBase,
|
|
20
22
|
},
|
|
21
23
|
|
|
22
24
|
props: {
|
|
@@ -24,16 +26,40 @@ export default {
|
|
|
24
26
|
type: Array,
|
|
25
27
|
required: true,
|
|
26
28
|
},
|
|
29
|
+
type: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: 'default',
|
|
32
|
+
},
|
|
27
33
|
},
|
|
28
34
|
|
|
29
35
|
computed: {
|
|
30
36
|
chartComponent() {
|
|
31
37
|
const componentMap = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
default: {
|
|
39
|
+
3: ChartDefaultFunnelBase,
|
|
40
|
+
4: ChartDefaultFunnelBase,
|
|
41
|
+
5: ChartDefaultFunnelBase,
|
|
42
|
+
},
|
|
43
|
+
basic: {
|
|
44
|
+
3: 'ChartFunnelThreeRows',
|
|
45
|
+
4: 'ChartFunnelFourRows',
|
|
46
|
+
5: 'ChartFunnelFiveRows',
|
|
47
|
+
},
|
|
35
48
|
};
|
|
36
|
-
return componentMap[this.data.length] || null;
|
|
49
|
+
return componentMap[this.type][this.data.length] || null;
|
|
50
|
+
},
|
|
51
|
+
chartData() {
|
|
52
|
+
const classIndex = ['w-60', 'w-50', 'w-40', 'w-30', 'w-20'];
|
|
53
|
+
if (this.type === 'default')
|
|
54
|
+
return this.data.map((e, index) => ({
|
|
55
|
+
percentage: e.title,
|
|
56
|
+
value: e.value,
|
|
57
|
+
description: e.description,
|
|
58
|
+
widthClass: classIndex[index],
|
|
59
|
+
color: e.color,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
return this.data;
|
|
37
63
|
},
|
|
38
64
|
},
|
|
39
65
|
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="unnnic-chart-funnel-base-container">
|
|
3
|
+
<section
|
|
4
|
+
v-for="(step, index) in data"
|
|
5
|
+
:key="index"
|
|
6
|
+
class="unnnic-chart-funnel-base-item"
|
|
7
|
+
>
|
|
8
|
+
<section
|
|
9
|
+
:class="[
|
|
10
|
+
'overflow-hidden',
|
|
11
|
+
step.widthClass,
|
|
12
|
+
{ 'first-item': index === 0, 'last-item': index === data.length - 1 },
|
|
13
|
+
]"
|
|
14
|
+
>
|
|
15
|
+
<section
|
|
16
|
+
:class="[
|
|
17
|
+
'unnnic-chart-funnel-base-item__card',
|
|
18
|
+
{ 'first-item': index === 0 },
|
|
19
|
+
]"
|
|
20
|
+
:style="{ backgroundColor: step.color }"
|
|
21
|
+
></section>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section
|
|
25
|
+
:class="[
|
|
26
|
+
'unnnic-chart-funnel-base-item__text',
|
|
27
|
+
{ 'last-item': index === data.length - 1 },
|
|
28
|
+
]"
|
|
29
|
+
>
|
|
30
|
+
<section class="unnnic-chart-funnel-base-item__text__values">
|
|
31
|
+
<p class="unnnic-chart-funnel-base-item__text__values-title">
|
|
32
|
+
{{ step.percentage }}
|
|
33
|
+
</p>
|
|
34
|
+
<p class="unnnic-chart-funnel-base-item__text__values-sub-title">
|
|
35
|
+
| {{ step.value }}
|
|
36
|
+
</p>
|
|
37
|
+
</section>
|
|
38
|
+
<p class="unnnic-chart-funnel-base-item__text-description">
|
|
39
|
+
{{ step.description }}
|
|
40
|
+
</p>
|
|
41
|
+
</section>
|
|
42
|
+
</section>
|
|
43
|
+
</section>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
interface FunnelStep {
|
|
48
|
+
percentage: number | string;
|
|
49
|
+
value: number | string;
|
|
50
|
+
description: string;
|
|
51
|
+
widthClass: string;
|
|
52
|
+
color: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
defineProps<{
|
|
56
|
+
data: FunnelStep[];
|
|
57
|
+
}>();
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<style lang="scss" scoped>
|
|
61
|
+
@import '../../../assets/scss/unnnic.scss';
|
|
62
|
+
|
|
63
|
+
.unnnic-chart-funnel-base-container {
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
justify-content: space-between;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.unnnic-chart-funnel-base-item {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: flex-start;
|
|
75
|
+
flex-grow: 1;
|
|
76
|
+
|
|
77
|
+
&__card {
|
|
78
|
+
height: 100%;
|
|
79
|
+
transition: background-color 0.3s ease;
|
|
80
|
+
transform: skew(-12deg, 0deg) translateX(-20px);
|
|
81
|
+
border-radius: 0 0 $unnnic-spacing-xs 0;
|
|
82
|
+
|
|
83
|
+
&.first-item {
|
|
84
|
+
border-radius: $unnnic-spacing-xs $unnnic-spacing-xs $unnnic-spacing-xs 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&__text {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
position: relative;
|
|
93
|
+
padding-left: $unnnic-spacing-sm;
|
|
94
|
+
padding-right: $unnnic-spacing-sm;
|
|
95
|
+
height: 100%;
|
|
96
|
+
|
|
97
|
+
&::after {
|
|
98
|
+
content: '';
|
|
99
|
+
position: absolute;
|
|
100
|
+
left: 0;
|
|
101
|
+
bottom: 0px;
|
|
102
|
+
width: 100%;
|
|
103
|
+
height: 1px;
|
|
104
|
+
background-color: $unnnic-color-neutral-soft;
|
|
105
|
+
z-index: -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&.last-item::after {
|
|
109
|
+
display: none;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&-description {
|
|
113
|
+
margin: 0;
|
|
114
|
+
color: $unnnic-color-neutral-dark;
|
|
115
|
+
text-align: center;
|
|
116
|
+
|
|
117
|
+
font-family: $unnnic-font-family-secondary;
|
|
118
|
+
font-size: $unnnic-font-size-body-md;
|
|
119
|
+
font-style: normal;
|
|
120
|
+
font-weight: $unnnic-font-weight-regular;
|
|
121
|
+
line-height: $unnnic-font-size-body-md + $unnnic-line-height-md;
|
|
122
|
+
}
|
|
123
|
+
&__values {
|
|
124
|
+
display: flex;
|
|
125
|
+
&-title {
|
|
126
|
+
margin: 0;
|
|
127
|
+
color: $unnnic-color-neutral-dark;
|
|
128
|
+
|
|
129
|
+
font-family: $unnnic-font-family-secondary;
|
|
130
|
+
font-size: $unnnic-font-size-body-lg;
|
|
131
|
+
font-style: normal;
|
|
132
|
+
font-weight: $unnnic-font-weight-bold;
|
|
133
|
+
line-height: $unnnic-font-size-body-lg + $unnnic-line-height-md;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&-sub-title {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: end;
|
|
139
|
+
margin: 0;
|
|
140
|
+
color: $unnnic-color-neutral-cloudy;
|
|
141
|
+
|
|
142
|
+
font-family: $unnnic-font-family-secondary;
|
|
143
|
+
font-size: $unnnic-font-size-body-md;
|
|
144
|
+
font-style: normal;
|
|
145
|
+
font-weight: $unnnic-font-weight-regular;
|
|
146
|
+
line-height: $unnnic-font-size-body-md + $unnnic-line-height-md;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.w-60 {
|
|
153
|
+
width: 60%;
|
|
154
|
+
}
|
|
155
|
+
.w-50 {
|
|
156
|
+
width: 50%;
|
|
157
|
+
}
|
|
158
|
+
.w-40 {
|
|
159
|
+
width: 40%;
|
|
160
|
+
}
|
|
161
|
+
.w-30 {
|
|
162
|
+
width: 30%;
|
|
163
|
+
}
|
|
164
|
+
.w-20 {
|
|
165
|
+
width: 20%;
|
|
166
|
+
}
|
|
167
|
+
.overflow-hidden {
|
|
168
|
+
height: 100%;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
position: relative;
|
|
171
|
+
|
|
172
|
+
&.first-item {
|
|
173
|
+
border-radius: $unnnic-spacing-xs $unnnic-spacing-xs 0px 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
&.last-item {
|
|
177
|
+
border-radius: 0 0 $unnnic-spacing-xs $unnnic-spacing-xs;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&::after {
|
|
181
|
+
content: '';
|
|
182
|
+
position: absolute;
|
|
183
|
+
left: 0;
|
|
184
|
+
bottom: -1px;
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: 2px;
|
|
187
|
+
background-color: $unnnic-color-neutral-soft;
|
|
188
|
+
z-index: -1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
&.last-item::after {
|
|
192
|
+
display: none;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
3
|
ref="dropdown"
|
|
4
|
+
data-testid="dropdown-skeleton"
|
|
4
5
|
:class="['dropdown', { active }]"
|
|
5
6
|
>
|
|
6
7
|
<slot> </slot>
|
|
7
8
|
<div
|
|
8
9
|
ref="dropdown-data"
|
|
10
|
+
data-testid="dropdown-data"
|
|
9
11
|
class="dropdown-data"
|
|
10
12
|
:style="{ position: 'fixed', ...positions }"
|
|
11
13
|
>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
3
|
+
import Dropdown from '../Dropdown.vue';
|
|
4
|
+
|
|
5
|
+
describe('Dropdown.vue', () => {
|
|
6
|
+
let wrapper;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
wrapper = mount(Dropdown, {
|
|
10
|
+
props: {
|
|
11
|
+
open: false,
|
|
12
|
+
position: 'bottom-left',
|
|
13
|
+
},
|
|
14
|
+
slots: {
|
|
15
|
+
trigger: '<button data-testid="dropdown-trigger">Trigger</button>',
|
|
16
|
+
default: '<div data-testid="dropdown-slot">Dropdown Content</div>',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('Render elements', () => {
|
|
22
|
+
it('should render the trigger slot', () => {
|
|
23
|
+
const trigger = wrapper.find('[data-testid="dropdown-trigger"]');
|
|
24
|
+
expect(trigger.exists()).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should not display the dropdown content initially when `open` is false', () => {
|
|
28
|
+
const slot = wrapper.find('[data-testid="dropdown-slot"]');
|
|
29
|
+
expect(slot.exists()).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should display the dropdown content initially when `open` is true', async () => {
|
|
33
|
+
await wrapper.setProps({ open: true });
|
|
34
|
+
const slot = wrapper.find('[data-testid="dropdown-slot"]');
|
|
35
|
+
expect(slot.exists()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Props and Positioning', () => {
|
|
40
|
+
it('should set the dropdown position based on the `position` prop', async () => {
|
|
41
|
+
await wrapper.setProps({ open: true, position: 'top-left' });
|
|
42
|
+
const content = wrapper.find('[data-testid="dropdown-content"]');
|
|
43
|
+
expect(content.classes()).toContain(
|
|
44
|
+
'unnnic-dropdown__content__position-top-left',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should validate the `position` prop with allowed values', () => {
|
|
49
|
+
const { validator } = Dropdown.props.position;
|
|
50
|
+
|
|
51
|
+
expect(validator('top-left')).toBe(true);
|
|
52
|
+
expect(validator('none')).toBe(true);
|
|
53
|
+
expect(validator('bottom-left')).toBe(true);
|
|
54
|
+
expect(validator('bottom-right')).toBe(true);
|
|
55
|
+
expect(validator('invalid-position')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Trigger Functionality', () => {
|
|
60
|
+
it('should toggle `active` state on trigger click', async () => {
|
|
61
|
+
const trigger = wrapper.find('[data-testid="dropdown-trigger"]');
|
|
62
|
+
await trigger.trigger('click');
|
|
63
|
+
expect(wrapper.vm.active).toBe(true);
|
|
64
|
+
|
|
65
|
+
await trigger.trigger('click');
|
|
66
|
+
expect(wrapper.vm.active).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should emit "update:open" event when `active` state changes', async () => {
|
|
70
|
+
await wrapper.setProps({ open: false });
|
|
71
|
+
const trigger = wrapper.find('[data-testid="dropdown-trigger"]');
|
|
72
|
+
await trigger.trigger('click');
|
|
73
|
+
|
|
74
|
+
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
75
|
+
expect(wrapper.emitted('update:open')[0]).toEqual([true]);
|
|
76
|
+
|
|
77
|
+
await trigger.trigger('click');
|
|
78
|
+
expect(wrapper.emitted('update:open')[1]).toEqual([false]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Click Outside Behavior', () => {
|
|
83
|
+
it('should close the dropdown when clicking outside', async () => {
|
|
84
|
+
await wrapper.setProps({ open: true });
|
|
85
|
+
expect(wrapper.vm.active).toBe(true);
|
|
86
|
+
|
|
87
|
+
await wrapper.vm.onClickOutside();
|
|
88
|
+
expect(wrapper.vm.active).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not close the dropdown if `active` is already false on outside click', async () => {
|
|
92
|
+
await wrapper.setProps({ open: false });
|
|
93
|
+
await wrapper.vm.onClickOutside();
|
|
94
|
+
|
|
95
|
+
expect(wrapper.vm.active).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
3
|
+
import DropdownItem from '../DropdownItem.vue';
|
|
4
|
+
|
|
5
|
+
describe('DropdownItem.vue', () => {
|
|
6
|
+
let wrapper;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
wrapper = mount(DropdownItem, {
|
|
10
|
+
slots: {
|
|
11
|
+
default: '<span>Dropdown Item Content</span>',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders slot content', () => {
|
|
17
|
+
expect(wrapper.html()).toContain('Dropdown Item Content');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('has the correct classes applied', () => {
|
|
21
|
+
const dropdownItem = wrapper.find('[data-testid="dropdown-item"]');
|
|
22
|
+
expect(dropdownItem.classes()).toContain('unnnic-dropdown-item');
|
|
23
|
+
expect(dropdownItem.classes()).toContain('unnnic--clickable');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
|
+
import DropdownSkeleton from '../DropdownSkeleton.vue';
|
|
4
|
+
|
|
5
|
+
describe('DropdownSkeleton.vue', () => {
|
|
6
|
+
let wrapper;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
wrapper = mount(DropdownSkeleton, {
|
|
10
|
+
props: {
|
|
11
|
+
type: 'automatic',
|
|
12
|
+
modelValue: false,
|
|
13
|
+
position: 'bottom-left',
|
|
14
|
+
},
|
|
15
|
+
slots: {
|
|
16
|
+
default: '<div data-testid="default-slot">Default Slot Content</div>',
|
|
17
|
+
inside: '<div data-testid="inside-slot">Inside Slot Content</div>',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const dropdown = () => wrapper.find('[data-testid="dropdown-skeleton"]');
|
|
23
|
+
const dropdownData = () => wrapper.find('[data-testid="dropdown-data"]');
|
|
24
|
+
|
|
25
|
+
describe('Rendering', () => {
|
|
26
|
+
it('should render the dropdown with the correct default classes', () => {
|
|
27
|
+
expect(dropdown().exists()).toBe(true);
|
|
28
|
+
expect(dropdown().classes()).toContain('dropdown');
|
|
29
|
+
expect(dropdown().classes()).not.toContain('active');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should render the default slot content inside the dropdown', () => {
|
|
33
|
+
const defaultSlot = wrapper.find('[data-testid="default-slot"]');
|
|
34
|
+
expect(defaultSlot.exists()).toBe(true);
|
|
35
|
+
expect(defaultSlot.text()).toBe('Default Slot Content');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should apply active class when modelValue is true and type is "manual"', async () => {
|
|
39
|
+
await wrapper.setProps({ modelValue: true, type: 'manual' });
|
|
40
|
+
expect(dropdown().classes()).toContain('active');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('Active State Management', () => {
|
|
45
|
+
it('should set active class to false when type is "automatic"', () => {
|
|
46
|
+
expect(dropdown().classes()).not.toContain('active');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should set active class to true when type is "manual" and modelValue is true', async () => {
|
|
50
|
+
await wrapper.setProps({ type: 'manual', modelValue: true });
|
|
51
|
+
expect(dropdown().classes()).toContain('active');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should set active class to false when type is "manual" and modelValue is false', async () => {
|
|
55
|
+
await wrapper.setProps({ type: 'manual', modelValue: false });
|
|
56
|
+
expect(dropdown().classes()).not.toContain('active');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not change active class when type is "manual" and modelValue is not changed', () => {
|
|
60
|
+
expect(dropdown().classes()).not.toContain('active');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('Position Calculation', () => {
|
|
65
|
+
it('should calculate position as bottom-left by default', () => {
|
|
66
|
+
const positions = wrapper.vm.positions;
|
|
67
|
+
|
|
68
|
+
expect(dropdownData().attributes('style')).toContain(
|
|
69
|
+
`left: ${positions.left}`,
|
|
70
|
+
);
|
|
71
|
+
expect(dropdownData().attributes('style')).toContain(
|
|
72
|
+
`top: ${positions.top}`,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should adjust position to avoid overflow on bottom-right', async () => {
|
|
77
|
+
await wrapper.setProps({ position: 'bottom-right' });
|
|
78
|
+
|
|
79
|
+
Object.defineProperty(wrapper.vm, 'clientHeight', { value: 500 });
|
|
80
|
+
Object.defineProperty(wrapper.vm, 'clientWidth', { value: 500 });
|
|
81
|
+
|
|
82
|
+
wrapper.vm.data.width = 600;
|
|
83
|
+
wrapper.vm.data.height = 600;
|
|
84
|
+
|
|
85
|
+
wrapper.vm.calculatePosition();
|
|
86
|
+
|
|
87
|
+
const positions = wrapper.vm.positions;
|
|
88
|
+
|
|
89
|
+
expect(dropdownData().attributes('style')).toContain(
|
|
90
|
+
`left: ${positions.left}`,
|
|
91
|
+
);
|
|
92
|
+
expect(dropdownData().attributes('style')).toContain(
|
|
93
|
+
`top: ${positions.top}`,
|
|
94
|
+
);
|
|
95
|
+
expect(parseInt(positions.left, 10)).toBeLessThanOrEqual(
|
|
96
|
+
wrapper.vm.clientWidth,
|
|
97
|
+
);
|
|
98
|
+
expect(parseInt(positions.top, 10)).toBeLessThanOrEqual(
|
|
99
|
+
wrapper.vm.clientHeight,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should correctly position on top-left when specified', async () => {
|
|
104
|
+
await wrapper.setProps({ position: 'top-left' });
|
|
105
|
+
|
|
106
|
+
wrapper.vm.data.width = 100;
|
|
107
|
+
wrapper.vm.data.height = 100;
|
|
108
|
+
|
|
109
|
+
wrapper.vm.calculatePosition();
|
|
110
|
+
|
|
111
|
+
const positions = wrapper.vm.positions;
|
|
112
|
+
|
|
113
|
+
expect(dropdownData().attributes('style')).toContain(
|
|
114
|
+
`left: ${positions.left}`,
|
|
115
|
+
);
|
|
116
|
+
expect(dropdownData().attributes('style')).toContain(
|
|
117
|
+
`top: ${positions.top}`,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Lifecycle', () => {
|
|
123
|
+
it('should clean up event listeners on beforeUnmount', async () => {
|
|
124
|
+
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
|
125
|
+
|
|
126
|
+
await wrapper.unmount();
|
|
127
|
+
|
|
128
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
129
|
+
'scroll',
|
|
130
|
+
wrapper.vm.calculatePosition,
|
|
131
|
+
);
|
|
132
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
133
|
+
'resize',
|
|
134
|
+
wrapper.vm.calculatePosition,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should set initial data properties on mount', () => {
|
|
139
|
+
expect(wrapper.vm.clientHeight).toBe(0);
|
|
140
|
+
expect(wrapper.vm.clientWidth).toBe(0);
|
|
141
|
+
expect(wrapper.vm.x).toBe(0);
|
|
142
|
+
expect(wrapper.vm.y).toBe(0);
|
|
143
|
+
expect(wrapper.vm.width).toBe(0);
|
|
144
|
+
expect(wrapper.vm.height).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<div
|
|
3
3
|
v-on-click-outside="onClickOutside"
|
|
4
4
|
class="unnnic-select-smart"
|
|
5
|
+
data-testid="select-smart"
|
|
5
6
|
@keydown="onKeyDownSelect"
|
|
6
7
|
>
|
|
7
8
|
<DropdownSkeleton
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
<TextInput
|
|
14
15
|
ref="selectSmartInput"
|
|
15
16
|
class="unnnic-select-smart__input"
|
|
17
|
+
data-testid="select-smart-input"
|
|
16
18
|
:modelValue="inputValue"
|
|
17
19
|
:placeholder="placeholder || autocompletePlaceholder || selectedLabel"
|
|
18
20
|
:type="type"
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
active: active,
|
|
38
40
|
inactive: !active,
|
|
39
41
|
}"
|
|
42
|
+
data-testid="options-container"
|
|
40
43
|
>
|
|
41
44
|
<div :style="{ overflow: 'auto' }">
|
|
42
45
|
<SelectSmartMultipleHeader
|
|
@@ -49,6 +52,7 @@
|
|
|
49
52
|
/>
|
|
50
53
|
<div
|
|
51
54
|
ref="selectSmartOptionsScrollArea"
|
|
55
|
+
data-testid="options-scroll-area"
|
|
52
56
|
:class="[
|
|
53
57
|
'unnnic-select-smart__options__scroll-area',
|
|
54
58
|
`size-${size}`,
|
|
@@ -61,6 +65,7 @@
|
|
|
61
65
|
<SelectSmartOption
|
|
62
66
|
v-for="(option, index) in filterOptions(options)"
|
|
63
67
|
:key="option.value"
|
|
68
|
+
data-testid="option"
|
|
64
69
|
:label="option.label"
|
|
65
70
|
:description="option.description"
|
|
66
71
|
:tabindex="index"
|