@eturnity/eturnity_reusable_components 9.28.0 → 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/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/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/sideMenu/sideMenu.spec.js +109 -0
- package/src/components/sideMenu/sideMenu.stories.js +43 -0
package/package.json
CHANGED
|
@@ -123,3 +123,12 @@ DropdownBackgroundColor.args = {
|
|
|
123
123
|
...defaultDropdownProps,
|
|
124
124
|
backgroundColor: theme.colors.red,
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
// Long, unbroken option names should wrap inside the dropdown instead of
|
|
128
|
+
// overflowing horizontally (EPDM-11708).
|
|
129
|
+
export const DropdownLongContent = Template.bind({})
|
|
130
|
+
DropdownLongContent.args = {
|
|
131
|
+
...defaultDropdownProps,
|
|
132
|
+
openingMode: 'click',
|
|
133
|
+
dropdown: `<div>ThisIsAnExtremelyLongVariantNameWithoutAnySpacesThatUsedToCauseHorizontalScroll</div><div>Another duplicated variant with a very long descriptive name that keeps going</div>`,
|
|
134
|
+
}
|
|
@@ -8,48 +8,121 @@ jest.mock('@/components/icon/iconCache.mjs', () => ({
|
|
|
8
8
|
fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
|
|
9
9
|
}))
|
|
10
10
|
|
|
11
|
+
const mountDropdown = (props = {}) =>
|
|
12
|
+
mount(RCDropdown, {
|
|
13
|
+
props: { ...defaultProps, ...props },
|
|
14
|
+
slots: { ...defaultProps },
|
|
15
|
+
global: { provide: { theme } },
|
|
16
|
+
})
|
|
17
|
+
|
|
11
18
|
describe('dropdown/index.vue', () => {
|
|
12
|
-
it('
|
|
13
|
-
const wrapper =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
it('renders the trigger and dropdown slots', () => {
|
|
20
|
+
const wrapper = mountDropdown()
|
|
21
|
+
|
|
22
|
+
const trigger = wrapper.find('[data-test-id="dropdown_trigger"]')
|
|
23
|
+
const content = wrapper.find('[data-test-id="dropdown_dropdown_content"]')
|
|
24
|
+
|
|
25
|
+
expect(trigger.exists()).toBe(true)
|
|
26
|
+
expect(trigger.text()).toContain(defaultProps.trigger)
|
|
27
|
+
expect(content.html()).toContain('DROPDOWN OPTION 1')
|
|
28
|
+
expect(content.html()).toContain('DROPDOWN OPTION 2')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('opens on click only when opening-mode is "click"', async () => {
|
|
32
|
+
const wrapper = mountDropdown({ openingMode: 'click' })
|
|
33
|
+
|
|
34
|
+
const trigger = wrapper.find('[data-test-id="dropdown_trigger"]')
|
|
35
|
+
const dropdownWrapper = wrapper.find(
|
|
36
|
+
'[data-test-id="dropdown_dropdown_wrapper"]'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(false)
|
|
40
|
+
|
|
41
|
+
await trigger.trigger('hover')
|
|
42
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(false)
|
|
43
|
+
|
|
44
|
+
await trigger.trigger('click')
|
|
45
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('does not apply the openDropdown class in hover mode', async () => {
|
|
49
|
+
const wrapper = mountDropdown({ openingMode: 'hover' })
|
|
50
|
+
|
|
51
|
+
const trigger = wrapper.find('[data-test-id="dropdown_trigger"]')
|
|
52
|
+
const dropdownWrapper = wrapper.find(
|
|
53
|
+
'[data-test-id="dropdown_dropdown_wrapper"]'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
await trigger.trigger('click')
|
|
57
|
+
|
|
58
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('toggles open and closed on repeated clicks', async () => {
|
|
62
|
+
const wrapper = mountDropdown({ openingMode: 'click' })
|
|
63
|
+
|
|
64
|
+
const trigger = wrapper.find('[data-test-id="dropdown_trigger"]')
|
|
65
|
+
const dropdownWrapper = wrapper.find(
|
|
66
|
+
'[data-test-id="dropdown_dropdown_wrapper"]'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
await trigger.trigger('click')
|
|
70
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(true)
|
|
22
71
|
|
|
23
|
-
|
|
72
|
+
await trigger.trigger('click')
|
|
73
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('emits on-dropdown-toggle with the new open state', async () => {
|
|
77
|
+
const wrapper = mountDropdown({ openingMode: 'click' })
|
|
78
|
+
|
|
79
|
+
const trigger = wrapper.find('[data-test-id="dropdown_trigger"]')
|
|
24
80
|
|
|
25
|
-
|
|
26
|
-
|
|
81
|
+
await trigger.trigger('click')
|
|
82
|
+
await trigger.trigger('click')
|
|
83
|
+
|
|
84
|
+
expect(wrapper.emitted('on-dropdown-toggle')).toHaveLength(2)
|
|
85
|
+
expect(wrapper.emitted('on-dropdown-toggle')[0]).toEqual([true])
|
|
86
|
+
expect(wrapper.emitted('on-dropdown-toggle')[1]).toEqual([false])
|
|
27
87
|
})
|
|
28
88
|
|
|
29
|
-
it('
|
|
89
|
+
it('closes when clicking outside in click mode', async () => {
|
|
90
|
+
// attachTo so the mounted click-outside listener sees a connected element
|
|
30
91
|
const wrapper = mount(RCDropdown, {
|
|
31
92
|
props: { ...defaultProps, openingMode: 'click' },
|
|
32
93
|
slots: { ...defaultProps },
|
|
33
|
-
global: {
|
|
34
|
-
|
|
35
|
-
theme,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
94
|
+
global: { provide: { theme } },
|
|
95
|
+
attachTo: document.body,
|
|
38
96
|
})
|
|
39
97
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
98
|
+
const trigger = wrapper.find('[data-test-id="dropdown_trigger"]')
|
|
99
|
+
const dropdownWrapper = wrapper.find(
|
|
42
100
|
'[data-test-id="dropdown_dropdown_wrapper"]'
|
|
43
101
|
)
|
|
44
102
|
|
|
45
|
-
|
|
103
|
+
await trigger.trigger('click')
|
|
104
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(true)
|
|
46
105
|
|
|
47
|
-
|
|
106
|
+
document.body.click()
|
|
107
|
+
await wrapper.vm.$nextTick()
|
|
48
108
|
|
|
49
|
-
expect(
|
|
109
|
+
expect(dropdownWrapper.classes('openDropdown')).toBe(false)
|
|
110
|
+
wrapper.unmount()
|
|
111
|
+
})
|
|
50
112
|
|
|
51
|
-
|
|
113
|
+
it('renders long unbroken content inside the dropdown window (EPDM-11708)', () => {
|
|
114
|
+
const longName =
|
|
115
|
+
'ThisIsAnExtremelyLongVariantNameWithoutAnySpacesThatUsedToCauseHorizontalScroll'
|
|
116
|
+
const wrapper = mount(RCDropdown, {
|
|
117
|
+
props: { ...defaultProps },
|
|
118
|
+
slots: {
|
|
119
|
+
trigger: defaultProps.trigger,
|
|
120
|
+
dropdown: `<div>${longName}</div>`,
|
|
121
|
+
},
|
|
122
|
+
global: { provide: { theme } },
|
|
123
|
+
})
|
|
52
124
|
|
|
53
|
-
|
|
125
|
+
const content = wrapper.find('[data-test-id="dropdown_dropdown_content"]')
|
|
126
|
+
expect(content.text()).toContain(longName)
|
|
54
127
|
})
|
|
55
128
|
})
|
|
@@ -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,109 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import SideMenu from '@/components/sideMenu'
|
|
3
|
+
import Spinner from '@/components/spinner'
|
|
4
|
+
|
|
5
|
+
jest.mock('@/components/icon/iconCache.mjs', () => ({
|
|
6
|
+
// need to mock this due to how jest handles import.meta
|
|
7
|
+
fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
const tabsData = [
|
|
11
|
+
{ key: 'dashboard', label: 'Dashboard', icon: 'house' },
|
|
12
|
+
{ key: 'projects', label: 'Projects', icon: 'house' },
|
|
13
|
+
{
|
|
14
|
+
key: 'settings',
|
|
15
|
+
label: 'Settings',
|
|
16
|
+
icon: 'gear',
|
|
17
|
+
children: [
|
|
18
|
+
{ key: 'general', label: 'General' },
|
|
19
|
+
{ key: 'billing', label: 'Billing' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const mountMenu = (props = {}) =>
|
|
25
|
+
mount(SideMenu, {
|
|
26
|
+
props: { tabsData, activeTab: 'dashboard', ...props },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const item = (wrapper, key) =>
|
|
30
|
+
wrapper.find(`[data-id="sub_menu_settings_${key}"]`)
|
|
31
|
+
|
|
32
|
+
describe('sideMenu/index.vue', () => {
|
|
33
|
+
it('shows a spinner and no menu items while tabsData is empty', () => {
|
|
34
|
+
const wrapper = mount(SideMenu, {
|
|
35
|
+
props: { tabsData: [], activeTab: 'dashboard' },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(wrapper.findComponent(Spinner).exists()).toBe(true)
|
|
39
|
+
expect(item(wrapper, 'dashboard').exists()).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders a menu item with its label for each flat tab', () => {
|
|
43
|
+
const wrapper = mountMenu()
|
|
44
|
+
|
|
45
|
+
expect(item(wrapper, 'dashboard').exists()).toBe(true)
|
|
46
|
+
expect(item(wrapper, 'dashboard').text()).toContain('Dashboard')
|
|
47
|
+
expect(item(wrapper, 'projects').text()).toContain('Projects')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('marks the active tab with data-active="true"', () => {
|
|
51
|
+
const wrapper = mountMenu({ activeTab: 'projects' })
|
|
52
|
+
|
|
53
|
+
expect(item(wrapper, 'projects').attributes('data-active')).toBe('true')
|
|
54
|
+
expect(item(wrapper, 'dashboard').attributes('data-active')).toBe('false')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('emits tab-click with the activeKey when a flat item is clicked', async () => {
|
|
58
|
+
const wrapper = mountMenu()
|
|
59
|
+
|
|
60
|
+
await item(wrapper, 'projects').trigger('click')
|
|
61
|
+
|
|
62
|
+
expect(wrapper.emitted('tab-click')).toHaveLength(1)
|
|
63
|
+
expect(wrapper.emitted('tab-click')[0]).toEqual([{ activeKey: 'projects' }])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('hides nested children until the parent is expanded', async () => {
|
|
67
|
+
const wrapper = mountMenu()
|
|
68
|
+
|
|
69
|
+
expect(item(wrapper, 'general').exists()).toBe(false)
|
|
70
|
+
|
|
71
|
+
await item(wrapper, 'settings').trigger('click')
|
|
72
|
+
expect(item(wrapper, 'general').exists()).toBe(true)
|
|
73
|
+
expect(item(wrapper, 'billing').exists()).toBe(true)
|
|
74
|
+
|
|
75
|
+
// clicking the parent again collapses it
|
|
76
|
+
await item(wrapper, 'settings').trigger('click')
|
|
77
|
+
expect(item(wrapper, 'general').exists()).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('emits tab-click with activeKey and parentKey when a child is clicked', async () => {
|
|
81
|
+
const wrapper = mountMenu()
|
|
82
|
+
|
|
83
|
+
await item(wrapper, 'settings').trigger('click')
|
|
84
|
+
await item(wrapper, 'general').trigger('click')
|
|
85
|
+
|
|
86
|
+
expect(wrapper.emitted('tab-click')[0]).toEqual([
|
|
87
|
+
{ activeKey: 'general', parentKey: 'settings' },
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('renders the logout button and app version, and emits on-logout on click', async () => {
|
|
92
|
+
const wrapper = mountMenu({ hasLogout: true, appVersion: 'v1.2.3' })
|
|
93
|
+
|
|
94
|
+
const logout = wrapper.find('[data-id="button_settings_logout"]')
|
|
95
|
+
expect(logout.exists()).toBe(true)
|
|
96
|
+
expect(wrapper.text()).toContain('v1.2.3')
|
|
97
|
+
|
|
98
|
+
await logout.trigger('click')
|
|
99
|
+
expect(wrapper.emitted('on-logout')).toHaveLength(1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('hides the logout section when hasLogout is false', () => {
|
|
103
|
+
const wrapper = mountMenu({ hasLogout: false })
|
|
104
|
+
|
|
105
|
+
expect(wrapper.find('[data-id="button_settings_logout"]').exists()).toBe(
|
|
106
|
+
false
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -7,6 +7,19 @@ const flatMenu = [
|
|
|
7
7
|
{ key: 'settings', label: 'Settings', icon: 'House' },
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
+
const nestedMenu = [
|
|
11
|
+
{ key: 'dashboard', label: 'Dashboard', icon: 'House' },
|
|
12
|
+
{
|
|
13
|
+
key: 'settings',
|
|
14
|
+
label: 'Settings',
|
|
15
|
+
icon: 'House',
|
|
16
|
+
children: [
|
|
17
|
+
{ key: 'general', label: 'General' },
|
|
18
|
+
{ key: 'billing', label: 'Billing' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
10
23
|
export default {
|
|
11
24
|
title: 'Components/Navigation/SideMenu',
|
|
12
25
|
component: SideMenu,
|
|
@@ -51,3 +64,33 @@ SecondActive.args = {
|
|
|
51
64
|
...FlatItems.args,
|
|
52
65
|
activeTab: 'projects',
|
|
53
66
|
}
|
|
67
|
+
|
|
68
|
+
// A collapsible parent item — click "Settings" to expand its children.
|
|
69
|
+
export const NestedItems = Template.bind({})
|
|
70
|
+
NestedItems.args = {
|
|
71
|
+
tabsData: nestedMenu,
|
|
72
|
+
activeTab: 'general',
|
|
73
|
+
activeParentTab: 'settings',
|
|
74
|
+
hasLogout: false,
|
|
75
|
+
appVersion: 'Storybook',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Bottom section with the logout button and the app version.
|
|
79
|
+
export const WithLogout = Template.bind({})
|
|
80
|
+
WithLogout.args = {
|
|
81
|
+
tabsData: flatMenu,
|
|
82
|
+
activeTab: 'dashboard',
|
|
83
|
+
activeParentTab: null,
|
|
84
|
+
hasLogout: true,
|
|
85
|
+
appVersion: 'v1.2.3',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// While tabsData is empty the menu shows a loading spinner.
|
|
89
|
+
export const Loading = Template.bind({})
|
|
90
|
+
Loading.args = {
|
|
91
|
+
tabsData: [],
|
|
92
|
+
activeTab: 'dashboard',
|
|
93
|
+
activeParentTab: null,
|
|
94
|
+
hasLogout: false,
|
|
95
|
+
appVersion: 'Storybook',
|
|
96
|
+
}
|