@davidbirchall/core 1.0.6 → 1.0.8
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/.storybook/main.ts +18 -0
- package/.storybook/preview.ts +14 -0
- package/package.json +1 -4
- package/src/components/Badge/Badge.stories.ts +147 -0
- package/src/components/Badge/Badge.test.ts +57 -0
- package/src/components/Badge/Badge.vue +79 -0
- package/src/components/Button/Button.stories.ts +80 -0
- package/src/components/Button/Button.test.ts +145 -0
- package/src/components/Button/Button.vue +108 -0
- package/src/components/Button/types.ts +4 -0
- package/src/components/Calendar/Calendar.stories.ts +261 -0
- package/src/components/Calendar/Calendar.test.ts +119 -0
- package/src/components/Calendar/Calendar.vue +528 -0
- package/src/components/Calendar/types.ts +20 -0
- package/src/components/Card/Card.stories.ts +88 -0
- package/src/components/Card/Card.test.ts +173 -0
- package/src/components/Card/Card.vue +59 -0
- package/{dist/Card/types.d.ts → src/components/Card/types.ts} +1 -1
- package/src/components/Checkbox/Checkbox.stories.ts +126 -0
- package/src/components/Checkbox/Checkbox.test.ts +155 -0
- package/src/components/Checkbox/Checkbox.vue +121 -0
- package/src/components/Checkbox/types.ts +7 -0
- package/src/components/DataTable/DataTable.stories.ts +156 -0
- package/src/components/DataTable/DataTable.test.ts +185 -0
- package/src/components/DataTable/DataTable.vue +177 -0
- package/src/components/DataTable/types.ts +12 -0
- package/src/components/DatePicker/DatePicker.stories.ts +172 -0
- package/src/components/DatePicker/DatePicker.test.ts +87 -0
- package/src/components/DatePicker/DatePicker.vue +302 -0
- package/src/components/Dropdown/Dropdown.stories.ts +231 -0
- package/src/components/Dropdown/Dropdown.vue +314 -0
- package/src/components/Dropdown/types.ts +14 -0
- package/src/components/EmptyState/EmptyState.stories.ts +189 -0
- package/src/components/EmptyState/EmptyState.vue +215 -0
- package/src/components/EmptyState/types.ts +8 -0
- package/src/components/ErrorSummary/ErrorSummary.vue +78 -0
- package/src/components/ErrorSummary/types.ts +4 -0
- package/src/components/FormGroup/FormGroup.stories.ts +264 -0
- package/src/components/FormGroup/FormGroup.test.ts +63 -0
- package/src/components/FormGroup/FormGroup.vue +58 -0
- package/src/components/Heading/Heading.stories.ts +121 -0
- package/src/components/Heading/Heading.test.ts +184 -0
- package/src/components/Heading/Heading.vue +95 -0
- package/src/components/Heading/types.ts +6 -0
- package/src/components/Input/Input.stories.ts +172 -0
- package/src/components/Input/Input.test.ts +213 -0
- package/src/components/Input/Input.vue +121 -0
- package/src/components/Input/types.ts +11 -0
- package/src/components/Modal/Modal.stories.ts +341 -0
- package/src/components/Modal/Modal.test.ts +99 -0
- package/src/components/Modal/Modal.vue +278 -0
- package/src/components/ProgressBar/ProgressBar.stories.ts +313 -0
- package/src/components/ProgressBar/ProgressBar.test.ts +98 -0
- package/src/components/ProgressBar/ProgressBar.vue +117 -0
- package/src/components/Select/Select.stories.ts +177 -0
- package/src/components/Select/Select.test.ts +225 -0
- package/src/components/Select/Select.vue +147 -0
- package/src/components/Select/types.ts +16 -0
- package/src/components/StatCard/StatCard.stories.ts +274 -0
- package/src/components/StatCard/StatCard.vue +226 -0
- package/src/components/StatCard/types.ts +12 -0
- package/src/components/Tag/Tag.stories.ts +78 -0
- package/src/components/Tag/Tag.test.ts +50 -0
- package/src/components/Tag/Tag.vue +71 -0
- package/src/components/Tag/types.ts +4 -0
- package/src/components/TextArea/TextArea.stories.ts +171 -0
- package/src/components/TextArea/TextArea.test.ts +202 -0
- package/src/components/TextArea/TextArea.vue +122 -0
- package/src/components/TextArea/types.ts +11 -0
- package/src/components/index.ts +5 -0
- package/src/test/setup.ts +1 -0
- package/src/vite-env.d.ts +6 -0
- package/tsconfig.json +29 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +28 -0
- package/dist/Button/types.d.ts +0 -4
- package/dist/Calendar/types.d.ts +0 -22
- package/dist/Checkbox/types.d.ts +0 -7
- package/dist/DataTable/types.d.ts +0 -11
- package/dist/Dropdown/types.d.ts +0 -13
- package/dist/EmptyState/types.d.ts +0 -8
- package/dist/ErrorSummary/types.d.ts +0 -4
- package/dist/Heading/types.d.ts +0 -6
- package/dist/Input/types.d.ts +0 -11
- package/dist/Select/types.d.ts +0 -15
- package/dist/StatCard/types.d.ts +0 -12
- package/dist/Tag/types.d.ts +0 -4
- package/dist/TextArea/types.d.ts +0 -11
- package/dist/core.css +0 -1
- package/dist/core.js +0 -24
- package/dist/core.js.map +0 -1
- package/dist/core.umd.cjs +0 -2
- package/dist/core.umd.cjs.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/package.json +0 -27
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import ProgressBar from './ProgressBar.vue'
|
|
4
|
+
|
|
5
|
+
describe('ProgressBar', () => {
|
|
6
|
+
it('renders with default props', () => {
|
|
7
|
+
const wrapper = mount(ProgressBar, {
|
|
8
|
+
props: {
|
|
9
|
+
percentage: 50
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
expect(wrapper.find('.progress-bar').exists()).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('displays correct percentage', () => {
|
|
16
|
+
const wrapper = mount(ProgressBar, {
|
|
17
|
+
props: {
|
|
18
|
+
percentage: 75
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
expect(wrapper.find('.progress-bar__percentage').text()).toBe('75%')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('clamps percentage to 0-100 range', () => {
|
|
25
|
+
const wrapper = mount(ProgressBar, {
|
|
26
|
+
props: {
|
|
27
|
+
percentage: 150
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
const fill = wrapper.find('.progress-bar__fill')
|
|
31
|
+
expect(fill.attributes('style')).toContain('width: 100%')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('applies correct variant class', () => {
|
|
35
|
+
const variants = ['primary', 'success', 'warning', 'danger'] as const
|
|
36
|
+
|
|
37
|
+
variants.forEach(variant => {
|
|
38
|
+
const wrapper = mount(ProgressBar, {
|
|
39
|
+
props: {
|
|
40
|
+
percentage: 50,
|
|
41
|
+
variant
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
expect(wrapper.find(`.progress-bar__fill--${variant}`).exists()).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('shows label when provided', () => {
|
|
49
|
+
const wrapper = mount(ProgressBar, {
|
|
50
|
+
props: {
|
|
51
|
+
percentage: 50,
|
|
52
|
+
label: 'Progress'
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
expect(wrapper.find('.progress-bar__label').text()).toBe('Progress')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('hides header when showLabel is false', () => {
|
|
59
|
+
const wrapper = mount(ProgressBar, {
|
|
60
|
+
props: {
|
|
61
|
+
percentage: 50,
|
|
62
|
+
showLabel: false
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
expect(wrapper.find('.progress-bar__header').exists()).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('shows percentage in bar when showPercentageInBar is true', () => {
|
|
69
|
+
const wrapper = mount(ProgressBar, {
|
|
70
|
+
props: {
|
|
71
|
+
percentage: 50,
|
|
72
|
+
showPercentageInBar: true
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
expect(wrapper.find('.progress-bar__text').exists()).toBe(true)
|
|
76
|
+
expect(wrapper.find('.progress-bar__text').text()).toBe('50%')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('applies custom height', () => {
|
|
80
|
+
const wrapper = mount(ProgressBar, {
|
|
81
|
+
props: {
|
|
82
|
+
percentage: 50,
|
|
83
|
+
height: 20
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
expect(wrapper.find('.progress-bar__track').attributes('style')).toContain('height: 20px')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('adds animated class when animated prop is true', () => {
|
|
90
|
+
const wrapper = mount(ProgressBar, {
|
|
91
|
+
props: {
|
|
92
|
+
percentage: 50,
|
|
93
|
+
animated: true
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
expect(wrapper.find('.progress-bar__fill--animated').exists()).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="progress-bar">
|
|
3
|
+
<div v-if="showLabel" class="progress-bar__header">
|
|
4
|
+
<span class="progress-bar__label">{{ label }}</span>
|
|
5
|
+
<span class="progress-bar__percentage">{{ displayPercentage }}%</span>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="progress-bar__track" :style="{ height: `${height}px` }">
|
|
8
|
+
<div
|
|
9
|
+
class="progress-bar__fill"
|
|
10
|
+
:class="[
|
|
11
|
+
`progress-bar__fill--${variant}`,
|
|
12
|
+
{ 'progress-bar__fill--animated': animated }
|
|
13
|
+
]"
|
|
14
|
+
:style="{ width: `${clampedPercentage}%` }"
|
|
15
|
+
>
|
|
16
|
+
<span v-if="showPercentageInBar" class="progress-bar__text">
|
|
17
|
+
{{ displayPercentage }}%
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { computed } from 'vue'
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
percentage: number
|
|
29
|
+
label?: string
|
|
30
|
+
showLabel?: boolean
|
|
31
|
+
showPercentageInBar?: boolean
|
|
32
|
+
height?: number
|
|
33
|
+
variant?: 'primary' | 'success' | 'warning' | 'danger'
|
|
34
|
+
animated?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
38
|
+
percentage: 0,
|
|
39
|
+
label: '',
|
|
40
|
+
showLabel: true,
|
|
41
|
+
showPercentageInBar: false,
|
|
42
|
+
height: 12,
|
|
43
|
+
variant: 'primary',
|
|
44
|
+
animated: true
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const clampedPercentage = computed(() => {
|
|
48
|
+
return Math.min(Math.max(props.percentage, 0), 100)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const displayPercentage = computed(() => {
|
|
52
|
+
return Math.round(clampedPercentage.value)
|
|
53
|
+
})
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style scoped>
|
|
57
|
+
.progress-bar {
|
|
58
|
+
width: 100%;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.progress-bar__header {
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: space-between;
|
|
64
|
+
margin-bottom: 0.75rem;
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
color: #374151;
|
|
67
|
+
font-size: 0.9375rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.progress-bar__percentage {
|
|
71
|
+
color: #667eea;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.progress-bar__track {
|
|
75
|
+
width: 100%;
|
|
76
|
+
background: #e5e7eb;
|
|
77
|
+
border-radius: 0.5rem;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
position: relative;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.progress-bar__fill {
|
|
83
|
+
height: 100%;
|
|
84
|
+
border-radius: 0.5rem;
|
|
85
|
+
transition: width 0.3s ease;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
position: relative;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.progress-bar__fill--animated {
|
|
93
|
+
transition: width 0.6s ease;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.progress-bar__fill--primary {
|
|
97
|
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.progress-bar__fill--success {
|
|
101
|
+
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.progress-bar__fill--warning {
|
|
105
|
+
background: linear-gradient(90deg, #f59e0b 0%, #d97706 100%);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.progress-bar__fill--danger {
|
|
109
|
+
background: linear-gradient(90deg, #ef4444 0%, #dc2626 100%);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.progress-bar__text {
|
|
113
|
+
color: white;
|
|
114
|
+
font-size: 0.75rem;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import Select from './Select.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form Fields/Select',
|
|
7
|
+
component: Select,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
label: {
|
|
11
|
+
control: 'text',
|
|
12
|
+
description: 'Label text'
|
|
13
|
+
},
|
|
14
|
+
placeholder: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
description: 'Placeholder text'
|
|
17
|
+
},
|
|
18
|
+
disabled: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
description: 'Disabled state'
|
|
21
|
+
},
|
|
22
|
+
required: {
|
|
23
|
+
control: 'boolean',
|
|
24
|
+
description: 'Required field'
|
|
25
|
+
},
|
|
26
|
+
error: {
|
|
27
|
+
control: 'text',
|
|
28
|
+
description: 'Error message'
|
|
29
|
+
},
|
|
30
|
+
hint: {
|
|
31
|
+
control: 'text',
|
|
32
|
+
description: 'Hint text'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} satisfies Meta<typeof Select>
|
|
36
|
+
|
|
37
|
+
export default meta
|
|
38
|
+
type Story = StoryObj<typeof meta>
|
|
39
|
+
|
|
40
|
+
const countryOptions = [
|
|
41
|
+
{ value: 'us', label: 'United States' },
|
|
42
|
+
{ value: 'uk', label: 'United Kingdom' },
|
|
43
|
+
{ value: 'ca', label: 'Canada' },
|
|
44
|
+
{ value: 'au', label: 'Australia' },
|
|
45
|
+
{ value: 'de', label: 'Germany' }
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
label: 'Country',
|
|
51
|
+
placeholder: 'Select a country',
|
|
52
|
+
options: countryOptions
|
|
53
|
+
},
|
|
54
|
+
render: (args: any) => ({
|
|
55
|
+
components: { Select },
|
|
56
|
+
setup() {
|
|
57
|
+
const value = ref('')
|
|
58
|
+
return { args, value }
|
|
59
|
+
},
|
|
60
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const WithValue: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
label: 'Country',
|
|
67
|
+
modelValue: 'uk',
|
|
68
|
+
options: countryOptions
|
|
69
|
+
},
|
|
70
|
+
render: (args: any) => ({
|
|
71
|
+
components: { Select },
|
|
72
|
+
setup() {
|
|
73
|
+
const value = ref(args.modelValue ?? '')
|
|
74
|
+
return { args, value }
|
|
75
|
+
},
|
|
76
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const WithHint: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
label: 'Shipping Method',
|
|
83
|
+
placeholder: 'Choose shipping method',
|
|
84
|
+
hint: 'Standard shipping takes 5-7 business days',
|
|
85
|
+
options: [
|
|
86
|
+
{ value: 'standard', label: 'Standard Shipping' },
|
|
87
|
+
{ value: 'express', label: 'Express Shipping' },
|
|
88
|
+
{ value: 'overnight', label: 'Overnight Shipping' }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
render: (args: any) => ({
|
|
92
|
+
components: { Select },
|
|
93
|
+
setup() {
|
|
94
|
+
const value = ref('')
|
|
95
|
+
return { args, value }
|
|
96
|
+
},
|
|
97
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const WithError: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
label: 'Country',
|
|
104
|
+
placeholder: 'Select a country',
|
|
105
|
+
error: 'Please select a country',
|
|
106
|
+
options: countryOptions
|
|
107
|
+
},
|
|
108
|
+
render: (args: any) => ({
|
|
109
|
+
components: { Select },
|
|
110
|
+
setup() {
|
|
111
|
+
const value = ref('')
|
|
112
|
+
return { args, value }
|
|
113
|
+
},
|
|
114
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const Required: Story = {
|
|
119
|
+
args: {
|
|
120
|
+
label: 'Country',
|
|
121
|
+
placeholder: 'Select a country',
|
|
122
|
+
required: true,
|
|
123
|
+
options: countryOptions
|
|
124
|
+
},
|
|
125
|
+
render: (args: any) => ({
|
|
126
|
+
components: { Select },
|
|
127
|
+
setup() {
|
|
128
|
+
const value = ref('')
|
|
129
|
+
return { args, value }
|
|
130
|
+
},
|
|
131
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const Disabled: Story = {
|
|
136
|
+
args: {
|
|
137
|
+
label: 'Country',
|
|
138
|
+
modelValue: 'us',
|
|
139
|
+
disabled: true,
|
|
140
|
+
options: countryOptions
|
|
141
|
+
},
|
|
142
|
+
render: (args: any) => ({
|
|
143
|
+
components: { Select },
|
|
144
|
+
setup() {
|
|
145
|
+
const value = ref(args.modelValue ?? '')
|
|
146
|
+
return { args, value }
|
|
147
|
+
},
|
|
148
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const ManyOptions: Story = {
|
|
153
|
+
args: {
|
|
154
|
+
label: 'State',
|
|
155
|
+
placeholder: 'Select a state',
|
|
156
|
+
options: [
|
|
157
|
+
{ value: 'al', label: 'Alabama' },
|
|
158
|
+
{ value: 'ak', label: 'Alaska' },
|
|
159
|
+
{ value: 'az', label: 'Arizona' },
|
|
160
|
+
{ value: 'ar', label: 'Arkansas' },
|
|
161
|
+
{ value: 'ca', label: 'California' },
|
|
162
|
+
{ value: 'co', label: 'Colorado' },
|
|
163
|
+
{ value: 'ct', label: 'Connecticut' },
|
|
164
|
+
{ value: 'de', label: 'Delaware' },
|
|
165
|
+
{ value: 'fl', label: 'Florida' },
|
|
166
|
+
{ value: 'ga', label: 'Georgia' }
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
render: (args: any) => ({
|
|
170
|
+
components: { Select },
|
|
171
|
+
setup() {
|
|
172
|
+
const value = ref('')
|
|
173
|
+
return { args, value }
|
|
174
|
+
},
|
|
175
|
+
template: '<Select v-bind="args" v-model="value" />'
|
|
176
|
+
})
|
|
177
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/vue'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import Select from './Select.vue'
|
|
5
|
+
|
|
6
|
+
const mockOptions = [
|
|
7
|
+
{ value: 'option1', label: 'Option 1' },
|
|
8
|
+
{ value: 'option2', label: 'Option 2' },
|
|
9
|
+
{ value: 'option3', label: 'Option 3' }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe('Select', () => {
|
|
13
|
+
describe('rendering', () => {
|
|
14
|
+
it('renders with label', () => {
|
|
15
|
+
render(Select, {
|
|
16
|
+
props: {
|
|
17
|
+
label: 'Choose an option',
|
|
18
|
+
options: mockOptions
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText('Choose an option')).toBeInTheDocument()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders without label', () => {
|
|
26
|
+
const { container } = render(Select, {
|
|
27
|
+
props: {
|
|
28
|
+
options: mockOptions
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(container.querySelector('.select-label')).not.toBeInTheDocument()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('renders all options', () => {
|
|
36
|
+
render(Select, {
|
|
37
|
+
props: {
|
|
38
|
+
options: mockOptions
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(screen.getByRole('option', { name: 'Option 1' })).toBeInTheDocument()
|
|
43
|
+
expect(screen.getByRole('option', { name: 'Option 2' })).toBeInTheDocument()
|
|
44
|
+
expect(screen.getByRole('option', { name: 'Option 3' })).toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders placeholder as disabled option', () => {
|
|
48
|
+
render(Select, {
|
|
49
|
+
props: {
|
|
50
|
+
placeholder: 'Select an option',
|
|
51
|
+
options: mockOptions
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const placeholderOption = screen.getByRole('option', { name: 'Select an option' }) as HTMLOptionElement
|
|
56
|
+
expect(placeholderOption).toBeInTheDocument()
|
|
57
|
+
expect(placeholderOption.disabled).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('renders error message', () => {
|
|
61
|
+
render(Select, {
|
|
62
|
+
props: {
|
|
63
|
+
error: 'This field is required',
|
|
64
|
+
options: mockOptions
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(screen.getByText('This field is required')).toBeInTheDocument()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('renders hint text', () => {
|
|
72
|
+
render(Select, {
|
|
73
|
+
props: {
|
|
74
|
+
hint: 'Choose wisely',
|
|
75
|
+
options: mockOptions
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(screen.getByText('Choose wisely')).toBeInTheDocument()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not show hint when error is present', () => {
|
|
83
|
+
render(Select, {
|
|
84
|
+
props: {
|
|
85
|
+
hint: 'Helper text',
|
|
86
|
+
error: 'Error message',
|
|
87
|
+
options: mockOptions
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(screen.queryByText('Helper text')).not.toBeInTheDocument()
|
|
92
|
+
expect(screen.getByText('Error message')).toBeInTheDocument()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('shows required asterisk when required', () => {
|
|
96
|
+
render(Select, {
|
|
97
|
+
props: {
|
|
98
|
+
label: 'Country',
|
|
99
|
+
required: true,
|
|
100
|
+
options: mockOptions
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText('*')).toBeInTheDocument()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('applies error class when error prop is set', () => {
|
|
108
|
+
render(Select, {
|
|
109
|
+
props: {
|
|
110
|
+
error: 'Error',
|
|
111
|
+
options: mockOptions
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const select = screen.getByRole('combobox')
|
|
116
|
+
expect(select).toHaveClass('select-field--error')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('v-model', () => {
|
|
121
|
+
it('displays initial value', () => {
|
|
122
|
+
render(Select, {
|
|
123
|
+
props: {
|
|
124
|
+
modelValue: 'option2',
|
|
125
|
+
options: mockOptions
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const select = screen.getByRole('combobox') as HTMLSelectElement
|
|
130
|
+
expect(select.value).toBe('option2')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('emits update:modelValue on change', async () => {
|
|
134
|
+
const user = userEvent.setup()
|
|
135
|
+
const { emitted } = render(Select, {
|
|
136
|
+
props: {
|
|
137
|
+
options: mockOptions
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const select = screen.getByRole('combobox')
|
|
142
|
+
await user.selectOptions(select, 'option2')
|
|
143
|
+
|
|
144
|
+
expect(emitted()['update:modelValue']).toBeTruthy()
|
|
145
|
+
expect(emitted()['update:modelValue'][0]).toEqual(['option2'])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('disabled state', () => {
|
|
150
|
+
it('disables the select when disabled prop is true', () => {
|
|
151
|
+
render(Select, {
|
|
152
|
+
props: {
|
|
153
|
+
disabled: true,
|
|
154
|
+
options: mockOptions
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const select = screen.getByRole('combobox')
|
|
159
|
+
expect(select).toBeDisabled()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('applies disabled class', () => {
|
|
163
|
+
render(Select, {
|
|
164
|
+
props: {
|
|
165
|
+
disabled: true,
|
|
166
|
+
options: mockOptions
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const select = screen.getByRole('combobox')
|
|
171
|
+
expect(select).toHaveClass('select-field--disabled')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('required attribute', () => {
|
|
176
|
+
it('sets required attribute when required prop is true', () => {
|
|
177
|
+
render(Select, {
|
|
178
|
+
props: {
|
|
179
|
+
required: true,
|
|
180
|
+
options: mockOptions
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const select = screen.getByRole('combobox')
|
|
185
|
+
expect(select).toBeRequired()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('does not set required attribute by default', () => {
|
|
189
|
+
render(Select, {
|
|
190
|
+
props: {
|
|
191
|
+
options: mockOptions
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const select = screen.getByRole('combobox')
|
|
196
|
+
expect(select).not.toBeRequired()
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('accessibility', () => {
|
|
201
|
+
it('associates label with select using htmlFor', () => {
|
|
202
|
+
render(Select, {
|
|
203
|
+
props: {
|
|
204
|
+
label: 'Choose option',
|
|
205
|
+
options: mockOptions
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const label = screen.getByText('Choose option') as HTMLLabelElement
|
|
210
|
+
const select = screen.getByRole('combobox')
|
|
211
|
+
|
|
212
|
+
expect(label.htmlFor).toBe(select.id)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('empty options', () => {
|
|
217
|
+
it('renders without options', () => {
|
|
218
|
+
const { container } = render(Select)
|
|
219
|
+
|
|
220
|
+
const select = container.querySelector('select')
|
|
221
|
+
expect(select).toBeInTheDocument()
|
|
222
|
+
expect(select?.children.length).toBe(0)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|