@eturnity/eturnity_reusable_components 9.25.17 → 9.28.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.
@@ -1,150 +1,134 @@
1
1
  import InputNumber from './index.vue'
2
2
 
3
+ // import InputNumber from "@eturnity/eturnity_reusable_components/src/components/inputs/inputNumber"
4
+ // How to use:
5
+ // <input-number
6
+ // :value="inputValue" // required
7
+ // placeholder="Enter distance"
8
+ // unitName="pc"
9
+ // :numberPrecision="2"
10
+ // :minNumber="0"
11
+ // @input-change="onInputChange($event)"
12
+ // @on-enter-click="onInputSubmit()"
13
+ // />
14
+
3
15
  export default {
4
16
  title: 'Components/Inputs/InputNumber',
5
17
  component: InputNumber,
6
- // argTypes: {},
18
+ tags: ['autodocs'],
19
+ argTypes: {
20
+ value: { description: 'Current value (required). String or number.' },
21
+ placeholder: { description: 'Placeholder shown when empty' },
22
+ unitName: { description: 'Unit appended to the value, e.g. "pc"' },
23
+ showLinearUnitName: {
24
+ description: 'Render the unit next to the input instead of inside it',
25
+ control: 'boolean',
26
+ },
27
+ numberPrecision: {
28
+ description: 'Number of decimal places kept',
29
+ control: 'number',
30
+ },
31
+ minNumber: {
32
+ description: 'Values below this are clamped up to it on blur',
33
+ control: 'number',
34
+ },
35
+ showArrowControls: {
36
+ description: 'Show the up/down stepper buttons',
37
+ control: 'boolean',
38
+ },
39
+ isError: { description: 'Error state styling', control: 'boolean' },
40
+ errorMessage: { description: 'Message shown while isError is true' },
41
+ disabled: { description: 'Disable the input', control: 'boolean' },
42
+ labelText: { description: 'Text label above the input' },
43
+ labelInfoText: { description: 'Info tooltip shown next to the label' },
44
+ fontSize: { description: 'Font size of the input text' },
45
+ },
7
46
  }
8
47
 
9
- const Template = (args, { argTypes }) => ({
10
- // Components used in your story `template` are defined in the `components` object
48
+ const Template = (args) => ({
11
49
  components: { InputNumber },
12
- // The story's `args` need to be mapped into the template through the `setup()` method
13
- props: Object.keys(argTypes),
14
- template: '<input-number v-bind="$props" />',
15
-
16
- // import InputNumber from "@eturnity/eturnity_reusable_components/src/components/inputs/inputNumber"
17
- // How to use:
18
- // <input-number
19
- // placeholder="Enter distance"
20
- // :isError="false" //default is false
21
- // inputWidth="150px" //by default, this is 100%
22
- // :numberPrecision="3"
23
- // unitName="pc"
24
- // :value="inputValue" //required -- String
25
- // @input-change="onInputChange($event)" //required
26
- // @on-enter-click="onInputSubmit()"
27
- // :errorMessage="Enter a number between 1 and 10"
28
- // :disabled="false"
29
- // :noBorder="true"
30
- // textAlign="left" // "left, right, center"
31
- // :showLinearUnitName="true"
32
- // fontSize="13px"
33
- // labelText="Number of Modules"
34
- // labelInfoText="Here is some information for you..."
35
- // labelInfoAlign="left"
36
- // />
50
+ setup() {
51
+ return { args }
52
+ },
53
+ template: `
54
+ <div style="width: 260px; padding: 12px;">
55
+ <InputNumber v-bind="args" />
56
+ </div>
57
+ `,
37
58
  })
38
59
 
39
60
  export const Default = Template.bind({})
40
61
  Default.args = {
41
- placeholder: 'Enter Value',
42
- disabled: false,
43
62
  value: '',
63
+ placeholder: 'Enter value',
44
64
  inputWidth: '200px',
45
65
  minWidth: '150px',
46
66
  unitName: 'pc',
47
- isError: false,
48
67
  numberPrecision: 0,
49
- noBorder: false,
50
- textAlign: 'left',
51
- showLinearUnitName: false,
52
68
  }
53
69
 
54
- export const hasError = Template.bind({})
55
- hasError.args = {
56
- placeholder: 'Enter Value',
57
- errorMessage: 'This field is required',
58
- isError: true,
59
- disabled: false,
60
- inputWidth: '200px',
61
- }
62
-
63
- export const Disabled = Template.bind({})
64
- Disabled.args = {
65
- placeholder: 'Enter Value',
66
- disabled: true,
67
- value: '',
68
- inputWidth: '200px',
69
- isError: false,
70
- numberPrecision: 0,
71
- noBorder: false,
72
- textAlign: 'left',
73
- showLinearUnitName: false,
70
+ export const WithValueAndUnit = Template.bind({})
71
+ WithValueAndUnit.args = {
72
+ ...Default.args,
73
+ value: 10,
74
+ numberPrecision: 2,
74
75
  }
75
76
 
76
77
  export const LinearUnit = Template.bind({})
77
78
  LinearUnit.args = {
78
- placeholder: 'Enter Value',
79
- disabled: false,
79
+ ...Default.args,
80
80
  value: 10,
81
- inputWidth: '200px',
82
- unitName: 'pc',
83
- isError: false,
84
81
  numberPrecision: 2,
85
- noBorder: false,
86
- textAlign: 'left',
87
82
  showLinearUnitName: true,
88
83
  }
89
84
 
90
- export const NormalUnit = Template.bind({})
91
- NormalUnit.args = {
92
- placeholder: 'Enter Value',
93
- disabled: false,
85
+ export const WithArrowControls = Template.bind({})
86
+ WithArrowControls.args = {
87
+ ...Default.args,
88
+ value: 5,
89
+ showArrowControls: true,
90
+ }
91
+
92
+ export const WithMinNumber = Template.bind({})
93
+ WithMinNumber.args = {
94
+ ...Default.args,
95
+ value: 3,
96
+ minNumber: 5,
97
+ showArrowControls: true,
98
+ }
99
+
100
+ export const HasError = Template.bind({})
101
+ HasError.args = {
102
+ ...Default.args,
103
+ value: '',
104
+ isError: true,
105
+ errorMessage: 'This field is required',
106
+ }
107
+
108
+ export const Disabled = Template.bind({})
109
+ Disabled.args = {
110
+ ...Default.args,
94
111
  value: 10,
95
- inputWidth: '200px',
96
- unitName: 'pc',
97
- isError: false,
98
- numberPrecision: 2,
99
- noBorder: false,
100
- textAlign: 'left',
101
- showLinearUnitName: false,
112
+ disabled: true,
102
113
  }
103
114
 
104
115
  export const WithLabel = Template.bind({})
105
116
  WithLabel.args = {
106
- placeholder: 'Enter Value',
107
- disabled: false,
117
+ ...Default.args,
108
118
  value: 10,
109
- inputWidth: '200px',
110
- unitName: 'pc',
111
- isError: false,
112
119
  numberPrecision: 2,
113
- noBorder: false,
114
- textAlign: 'left',
115
- showLinearUnitName: false,
116
- labelText: 'Number Input',
120
+ labelText: 'Number of modules',
117
121
  }
118
122
 
119
123
  export const WithLabelInfo = Template.bind({})
120
124
  WithLabelInfo.args = {
121
- placeholder: 'Enter Value',
122
- disabled: false,
123
- value: 10,
124
- inputWidth: '200px',
125
- unitName: 'pc',
126
- isError: false,
127
- numberPrecision: 2,
128
- noBorder: false,
129
- textAlign: 'left',
130
- showLinearUnitName: false,
131
- labelText: 'Number Input',
125
+ ...WithLabel.args,
132
126
  labelInfoText: 'Here is some information for you...',
133
127
  labelInfoAlign: 'right',
134
128
  }
135
129
 
136
130
  export const LargerFont = Template.bind({})
137
131
  LargerFont.args = {
138
- placeholder: 'Enter Value',
139
- disabled: false,
140
- value: 10,
141
- inputWidth: '200px',
142
- unitName: 'pc',
143
- isError: false,
144
- numberPrecision: 2,
145
- noBorder: false,
146
- textAlign: 'left',
147
- showLinearUnitName: false,
148
- labelText: 'Number Input',
132
+ ...WithLabel.args,
149
133
  fontSize: '16px',
150
134
  }
@@ -0,0 +1,133 @@
1
+ import { mount } from '@vue/test-utils'
2
+
3
+ // error_icon.png / date-fns (via translateLang) can't be parsed by jest, so
4
+ // stub them out. iconCache uses import.meta and is mocked like other specs.
5
+ jest.mock('../../../assets/icons/error_icon.png', () => 'error_icon.png')
6
+ jest.mock('@/helpers/translateLang', () => ({
7
+ langForLocaleString: () => 'en-US',
8
+ translateLang: (lang) => lang,
9
+ }))
10
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
11
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
12
+ }))
13
+
14
+ import InputNumber from '@/components/inputs/inputNumber'
15
+
16
+ const mountInput = (props = {}) =>
17
+ mount(InputNumber, { props: { value: null, numberPrecision: 0, ...props } })
18
+
19
+ describe('inputNumber/index.vue', () => {
20
+ describe('rendering', () => {
21
+ it('renders the value with its unit appended', () => {
22
+ const wrapper = mountInput({ value: 5, unitName: 'pc' })
23
+ expect(wrapper.find('input').element.value).toBe('5 pc')
24
+ })
25
+
26
+ it('keeps the unit out of the input when shown as a linear unit', () => {
27
+ const wrapper = mountInput({
28
+ value: 5,
29
+ unitName: 'pc',
30
+ showLinearUnitName: true,
31
+ })
32
+ expect(wrapper.find('input').element.value).toBe('5')
33
+ expect(wrapper.text()).toContain('pc')
34
+ })
35
+
36
+ it('renders the label text', () => {
37
+ const wrapper = mountInput({ value: 5, labelText: 'Modules' })
38
+ expect(wrapper.text()).toContain('Modules')
39
+ })
40
+
41
+ it('renders the error message when isError is set', () => {
42
+ const wrapper = mountInput({
43
+ value: 5,
44
+ isError: true,
45
+ errorMessage: 'Invalid number',
46
+ })
47
+ expect(wrapper.text()).toContain('Invalid number')
48
+ })
49
+
50
+ it('disables the input and hides the arrow controls when disabled', () => {
51
+ const wrapper = mountInput({
52
+ value: 5,
53
+ disabled: true,
54
+ showArrowControls: true,
55
+ })
56
+ expect(wrapper.find('input').attributes('disabled')).toBeDefined()
57
+ expect(wrapper.findAll('button')).toHaveLength(0)
58
+ })
59
+ })
60
+
61
+ describe('emits', () => {
62
+ it('emits input-focus on focus', async () => {
63
+ const wrapper = mountInput({ value: 5 })
64
+ await wrapper.find('input').trigger('focus')
65
+ expect(wrapper.emitted('input-focus')).toHaveLength(1)
66
+ })
67
+
68
+ it('emits input-change with the parsed number while typing', async () => {
69
+ const wrapper = mountInput({ value: 5 })
70
+ await wrapper.find('input').trigger('focus')
71
+ await wrapper.find('input').setValue('7')
72
+ expect(wrapper.emitted('input-change')).toBeTruthy()
73
+ expect(wrapper.emitted('input-change').at(-1)).toEqual([7])
74
+ })
75
+
76
+ it('emits on-enter-click when Enter is pressed', async () => {
77
+ const wrapper = mountInput({ value: 5 })
78
+ await wrapper.find('input').trigger('keyup.enter')
79
+ expect(wrapper.emitted('on-enter-click')).toHaveLength(1)
80
+ })
81
+ })
82
+
83
+ // Boundary Value Analysis: onInputBlur clamps values below `minNumber`.
84
+ describe('boundary value analysis: minNumber clamping on blur', () => {
85
+ const blurWith = async (props) => {
86
+ const wrapper = mountInput(props)
87
+ await wrapper.find('input').trigger('blur')
88
+ return wrapper.emitted('input-blur')
89
+ }
90
+
91
+ // minNumber = 10 -> just-below / at / just-above the boundary
92
+ it.each([
93
+ ['below the minimum (min - 1)', 9, 10],
94
+ ['at the minimum', 10, 10],
95
+ ['above the minimum (min + 1)', 11, 11],
96
+ ])('clamps a value %s', async (_label, value, expected) => {
97
+ const emitted = await blurWith({ value, minNumber: 10 })
98
+ expect(emitted).toEqual([[expected]])
99
+ })
100
+
101
+ it('does not clamp when minNumber is 0 (0 is treated as no minimum)', async () => {
102
+ // `if (this.minNumber && ...)` makes 0 falsy, so a negative value passes
103
+ const emitted = await blurWith({ value: -5, minNumber: 0 })
104
+ expect(emitted).toEqual([[-5]])
105
+ })
106
+ })
107
+
108
+ // Boundary Value Analysis: decrement is guarded at the negative boundary.
109
+ describe('boundary value analysis: decrement negative guard', () => {
110
+ const decrement = async (props) => {
111
+ const wrapper = mountInput({ showArrowControls: true, ...props })
112
+ // buttons: [0] increment (up), [1] decrement (down)
113
+ await wrapper.findAll('button')[1].trigger('click')
114
+ return wrapper.emitted('input-change')
115
+ }
116
+
117
+ it('emits a negative value when allowNegative is true', async () => {
118
+ const emitted = await decrement({ value: 5, allowNegative: true })
119
+ expect(emitted).toEqual([[-1]])
120
+ })
121
+
122
+ it('blocks crossing below zero when allowNegative is false', async () => {
123
+ const emitted = await decrement({ value: 5, allowNegative: false })
124
+ expect(emitted).toBeUndefined()
125
+ })
126
+
127
+ it('increments from the zero baseline by the interaction step', async () => {
128
+ const wrapper = mountInput({ value: 5, showArrowControls: true })
129
+ await wrapper.findAll('button')[0].trigger('click')
130
+ expect(wrapper.emitted('input-change')).toEqual([[1]])
131
+ })
132
+ })
133
+ })
@@ -0,0 +1,103 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+
4
+ // numberConverter pulls in translateLang (date-fns), which jest can't parse.
5
+ // Stub it so locale formatting is deterministic (en-US) under test.
6
+ jest.mock('@/helpers/translateLang', () => ({
7
+ langForLocaleString: () => 'en-US',
8
+ translateLang: (lang) => lang,
9
+ }))
10
+
11
+ import InputNumberQuestion from '@/components/inputs/inputNumberQuestion'
12
+
13
+ const questionWithUnit = {
14
+ number_format_precision: 2,
15
+ number_min_allowed: 0,
16
+ number_max_allowed: 500,
17
+ unit_short_name: 'kWh',
18
+ }
19
+
20
+ const questionPlain = {
21
+ number_format_precision: 0,
22
+ number_min_allowed: 1,
23
+ number_max_allowed: 99,
24
+ unit_short_name: '',
25
+ }
26
+
27
+ const mountInput = (props = {}) =>
28
+ mount(InputNumberQuestion, {
29
+ props: { question: questionPlain, value: null, ...props },
30
+ })
31
+
32
+ describe('inputNumberQuestion/index.vue', () => {
33
+ describe('rendering', () => {
34
+ it('formats the initial value through the question precision', () => {
35
+ const wrapper = mountInput({ question: questionWithUnit, value: '120' })
36
+ expect(wrapper.find('input').element.value).toBe('120')
37
+ })
38
+
39
+ it('renders the unit when the question carries one', () => {
40
+ const wrapper = mountInput({ question: questionWithUnit, value: '120' })
41
+ expect(wrapper.text()).toContain('kWh')
42
+ })
43
+
44
+ it('renders no unit container when the question has no unit', () => {
45
+ const wrapper = mountInput({ question: questionPlain, value: '5' })
46
+ expect(wrapper.find('span span').exists()).toBe(false)
47
+ })
48
+
49
+ it('renders the placeholder on the input', () => {
50
+ const wrapper = mountInput({ placeholder: 'Floor number' })
51
+ expect(wrapper.find('input').attributes('placeholder')).toBe(
52
+ 'Floor number'
53
+ )
54
+ })
55
+
56
+ it('renders the error message only when isError is set', () => {
57
+ const withError = mountInput({
58
+ isError: true,
59
+ errorMessage: 'Enter a number between 1 and 99',
60
+ })
61
+ expect(withError.text()).toContain('Enter a number between 1 and 99')
62
+
63
+ const withoutError = mountInput({
64
+ isError: false,
65
+ errorMessage: 'Enter a number between 1 and 99',
66
+ })
67
+ expect(withoutError.text()).not.toContain(
68
+ 'Enter a number between 1 and 99'
69
+ )
70
+ })
71
+ })
72
+
73
+ describe('emits', () => {
74
+ // Characterization test — documents current behaviour, do not "fix" here.
75
+ // The template binds `@input="onChangeHandler"`, so onChangeHandler receives
76
+ // the native InputEvent, but it treats its argument as the raw value string
77
+ // (no `$event.target.value`, unlike the sibling `inputNumber.onInput`).
78
+ // Parsing the event object yields NaN, so typing always emits '' rather than
79
+ // the parsed number. See EPDM-11715 notes.
80
+ it('emits input-change on typing (currently always empty, see note)', async () => {
81
+ const wrapper = mountInput({ question: questionPlain, value: null })
82
+ await wrapper.find('input').setValue('42')
83
+ expect(wrapper.emitted('input-change')).toBeTruthy()
84
+ expect(wrapper.emitted('input-change').at(-1)).toEqual([''])
85
+ })
86
+
87
+ it('emits on-enter-click when Enter is pressed', async () => {
88
+ const wrapper = mountInput({ value: '5' })
89
+ await wrapper.find('input').trigger('keyup.enter')
90
+ expect(wrapper.emitted('on-enter-click')).toHaveLength(1)
91
+ })
92
+ })
93
+
94
+ describe('clearInput watcher', () => {
95
+ it('clears the displayed text when clearInput flips to true', async () => {
96
+ const wrapper = mountInput({ question: questionWithUnit, value: '120' })
97
+ expect(wrapper.find('input').element.value).toBe('120')
98
+
99
+ await wrapper.setProps({ clearInput: true })
100
+ expect(wrapper.find('input').element.value).toBe('')
101
+ })
102
+ })
103
+ })
@@ -1,6 +1,18 @@
1
1
  import { ref } from 'vue'
2
2
  import InputNumberQuestion from './index.vue'
3
- import theme from '@/assets/theme'
3
+
4
+ // import InputNumberQuestion from "@eturnity/eturnity_reusable_components/src/components/inputs/inputNumberQuestion"
5
+ // How to use:
6
+ // <input-number-question
7
+ // :question="question" // required — { number_format_precision, number_min_allowed, number_max_allowed, unit_short_name }
8
+ // :value="inputValue" // required
9
+ // placeholder="Enter distance"
10
+ // inputWidth="150px" // default 100%
11
+ // :isError="false"
12
+ // errorMessage="Enter a number between 1 and 10"
13
+ // @input-change="onInputChange($event)"
14
+ // @on-enter-click="onInputSubmit()"
15
+ // />
4
16
 
5
17
  const questionWithUnit = {
6
18
  number_format_precision: 2,
@@ -23,6 +35,22 @@ export default {
23
35
  parameters: {
24
36
  layout: 'centered',
25
37
  },
38
+ argTypes: {
39
+ question: {
40
+ description:
41
+ 'Required. Drives formatting: number_format_precision, ' +
42
+ 'number_min_allowed, number_max_allowed, unit_short_name.',
43
+ },
44
+ value: { description: 'Current value (required). String or number.' },
45
+ placeholder: { description: 'Placeholder shown when empty' },
46
+ inputWidth: { description: 'Width of the input, defaults to 100%' },
47
+ isError: { description: 'Error state styling', control: 'boolean' },
48
+ errorMessage: { description: 'Message shown while isError is true' },
49
+ clearInput: {
50
+ description: 'When flipped to true, clears the displayed text',
51
+ control: 'boolean',
52
+ },
53
+ },
26
54
  }
27
55
 
28
56
  const Template = (args) => ({
@@ -32,21 +60,14 @@ const Template = (args) => ({
32
60
  return { args, val }
33
61
  },
34
62
  template: `
35
- <div data-test-id="input-number-question-story" style="min-width: 280px; padding: 16px;">
63
+ <div style="min-width: 280px; padding: 16px;">
36
64
  <InputNumberQuestion
37
- :question="args.question"
65
+ v-bind="args"
38
66
  :value="val"
39
- :placeholder="args.placeholder"
40
- :is-error="args.isError"
41
- :input-width="args.inputWidth"
42
- :error-message="args.errorMessage"
43
67
  @input-change="val = $event"
44
68
  />
45
69
  </div>
46
70
  `,
47
- provide: {
48
- theme,
49
- },
50
71
  })
51
72
 
52
73
  export const WithUnit = Template.bind({})
@@ -0,0 +1,154 @@
1
+ /* eslint-disable */
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import Select from './index.vue'
4
+ import Option from './option/index.vue'
5
+
6
+ // import.meta in the real iconCache.mjs cannot be parsed by Jest.
7
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
8
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
9
+ }))
10
+
11
+ // jsdom does not implement ResizeObserver, which the select observes on mount.
12
+ beforeAll(() => {
13
+ global.ResizeObserver = class {
14
+ observe() {}
15
+ unobserve() {}
16
+ disconnect() {}
17
+ }
18
+ })
19
+
20
+ // Host component mirrors the documented usage (selector + dropdown slots) so the
21
+ // spec exercises the public contract rather than internals.
22
+ const Host = {
23
+ components: { Select, Option },
24
+ props: ['selectProps', 'options'],
25
+ template: `
26
+ <Select v-bind="selectProps">
27
+ <template #selector="{ selectedValue }">
28
+ <span data-test-id="selector">{{ selectedValue }}</span>
29
+ </template>
30
+ <template #dropdown>
31
+ <Option
32
+ v-for="opt in options"
33
+ :key="opt"
34
+ :value="opt"
35
+ :data-id="'opt_' + opt"
36
+ >{{ opt }}</Option>
37
+ </template>
38
+ </Select>
39
+ `,
40
+ }
41
+
42
+ const THREE_OPTIONS = ['one', 'two', 'three']
43
+ const SIX_OPTIONS = ['one', 'two', 'three', 'four', 'five', 'six']
44
+
45
+ const mountSelect = (selectProps = {}, options = THREE_OPTIONS) =>
46
+ mount(Host, {
47
+ props: {
48
+ // isFixedDropdownPosition avoids the requestAnimationFrame scroll loop,
49
+ // which would otherwise keep running after the test unmounts.
50
+ // shouldUseTeleport keeps the dropdown inside the wrapper so it can be queried.
51
+ selectProps: {
52
+ isFixedDropdownPosition: true,
53
+ shouldUseTeleport: false,
54
+ ...selectProps,
55
+ },
56
+ options,
57
+ },
58
+ })
59
+
60
+ const selectInstance = (wrapper) => wrapper.findComponent(Select)
61
+
62
+ const openDropdown = async (wrapper) => {
63
+ await wrapper.find('.select-button').trigger('click')
64
+ await flushPromises()
65
+ await wrapper.vm.$nextTick()
66
+ }
67
+
68
+ describe('RCselect', () => {
69
+ it('renders the label with the optional hint and required star', () => {
70
+ const wrapper = mountSelect({
71
+ label: 'Service territory',
72
+ labelOptional: true,
73
+ isRequiredLabel: true,
74
+ })
75
+ expect(wrapper.text()).toContain('Service territory')
76
+ expect(wrapper.text()).toContain('optional')
77
+ // IsRequiredLabelStar renders the asterisk character.
78
+ expect(wrapper.text()).toContain('*')
79
+ })
80
+
81
+ it('initialises the selector slot from the value prop', () => {
82
+ const wrapper = mountSelect({ value: 'two' })
83
+ expect(wrapper.find('[data-test-id="selector"]').text()).toBe('two')
84
+ })
85
+
86
+ it('updates the selector when the value prop changes', async () => {
87
+ const wrapper = mountSelect({ value: 'one' })
88
+ await wrapper.setProps({ selectProps: { value: 'three' } })
89
+ expect(wrapper.find('[data-test-id="selector"]').text()).toBe('three')
90
+ })
91
+
92
+ it('opens the dropdown on trigger click and emits on-dropdown-open', async () => {
93
+ const wrapper = mountSelect()
94
+ expect(selectInstance(wrapper).emitted('on-dropdown-open')).toBeUndefined()
95
+
96
+ await openDropdown(wrapper)
97
+
98
+ expect(selectInstance(wrapper).emitted('on-dropdown-open')).toHaveLength(1)
99
+ expect(wrapper.find('.rc-select-dropdown').isVisible()).toBe(true)
100
+ })
101
+
102
+ it('emits input-change with the option value and closes on selection', async () => {
103
+ const wrapper = mountSelect()
104
+ await openDropdown(wrapper)
105
+
106
+ await wrapper.find('[data-id="opt_two"]').trigger('click')
107
+
108
+ const select = selectInstance(wrapper)
109
+ expect(select.emitted('input-change')).toHaveLength(1)
110
+ expect(select.emitted('input-change')[0]).toEqual(['two'])
111
+ expect(select.emitted('on-dropdown-close')).toHaveLength(1)
112
+ })
113
+
114
+ it('shows the search field once the option count reaches the threshold', async () => {
115
+ const wrapper = mountSelect({}, SIX_OPTIONS)
116
+ await openDropdown(wrapper)
117
+ expect(wrapper.find('[data-test-id="input"]').exists()).toBe(true)
118
+ })
119
+
120
+ it('hides the search field when there are fewer options than the threshold', async () => {
121
+ const wrapper = mountSelect({}, THREE_OPTIONS)
122
+ await openDropdown(wrapper)
123
+ expect(wrapper.find('[data-test-id="input"]').exists()).toBe(false)
124
+ })
125
+
126
+ it('honours a custom minOptionLength for the search field', async () => {
127
+ const wrapper = mountSelect({ minOptionLength: 3 }, THREE_OPTIONS)
128
+ await openDropdown(wrapper)
129
+ expect(wrapper.find('[data-test-id="input"]').exists()).toBe(true)
130
+ })
131
+
132
+ it('does not show the search field when isSearchable is false', async () => {
133
+ const wrapper = mountSelect({ isSearchable: false }, SIX_OPTIONS)
134
+ await openDropdown(wrapper)
135
+ expect(wrapper.find('[data-test-id="input"]').exists()).toBe(false)
136
+ })
137
+
138
+ it('renders the error message when hasError is set', () => {
139
+ const wrapper = mountSelect({
140
+ hasError: true,
141
+ errorMessage: 'Selection required',
142
+ })
143
+ const error = wrapper.find('[data-test-id="error_message_wrapper"]')
144
+ expect(error.exists()).toBe(true)
145
+ expect(error.text()).toBe('Selection required')
146
+ })
147
+
148
+ it('hides the caret when showCaret is false', () => {
149
+ expect(mountSelect().find('.caret_dropdown').exists()).toBe(true)
150
+ expect(
151
+ mountSelect({ showCaret: false }).find('.caret_dropdown').exists()
152
+ ).toBe(false)
153
+ })
154
+ })