@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.
- package/package.json +1 -1
- package/src/components/collapsableInfoText/collapsableInfoText.spec.js +57 -0
- package/src/components/collapsableInfoText/collapsableInfoText.stories.js +8 -0
- package/src/components/dropdown/Dropdown.stories.js +9 -0
- package/src/components/dropdown/dropdown.spec.js +99 -26
- package/src/components/dropdown/index.vue +2 -0
- package/src/components/filter/Filter.stories.js +177 -0
- package/src/components/filter/filter.spec.js +183 -0
- package/src/components/inputs/inputNumber/InputNumber.stories.js +87 -103
- package/src/components/inputs/inputNumber/inputNumber.spec.js +133 -0
- package/src/components/inputs/inputNumberQuestion/inputNumberQuestion.spec.js +103 -0
- package/src/components/inputs/inputNumberQuestion/inputNumberQuestion.stories.js +31 -10
- package/src/components/inputs/select/select.spec.js +154 -0
- package/src/components/inputs/select/select.stories.js +230 -56
- package/src/components/inputs/slider/slider.spec.js +59 -0
- package/src/components/inputs/slider/slider.stories.js +4 -2
- package/src/components/inputs/switchField/switchField.spec.js +88 -0
- package/src/components/inputs/textAreaInput/TextAreaInput.stories.js +78 -74
- package/src/components/inputs/textAreaInput/textAreaInput.spec.js +90 -0
- package/src/components/sideMenu/sideMenu.spec.js +109 -0
- package/src/components/sideMenu/sideMenu.stories.js +43 -0
|
@@ -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
|
-
|
|
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
|
|
10
|
-
// Components used in your story `template` are defined in the `components` object
|
|
48
|
+
const Template = (args) => ({
|
|
11
49
|
components: { InputNumber },
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
disabled: false,
|
|
117
|
+
...Default.args,
|
|
108
118
|
value: 10,
|
|
109
|
-
inputWidth: '200px',
|
|
110
|
-
unitName: 'pc',
|
|
111
|
-
isError: false,
|
|
112
119
|
numberPrecision: 2,
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
63
|
+
<div style="min-width: 280px; padding: 16px;">
|
|
36
64
|
<InputNumberQuestion
|
|
37
|
-
|
|
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
|
+
})
|