@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,127 +1,131 @@
1
+ import { ref, watch } from 'vue'
1
2
  import TextAreaInput from './index.vue'
2
3
 
4
+ // To use:
5
+ // import TextAreaInput from '@eturnity/eturnity_reusable_components/src/components/inputs/textAreaInput'
6
+ // <text-area-input
7
+ // placeholder="Comments"
8
+ // :value="form.note"
9
+ // label="Question 5"
10
+ // row-height="4"
11
+ // :is-error="false"
12
+ // error-text="This field is required"
13
+ // @input-change="onInputChange($event)"
14
+ // />
15
+
3
16
  export default {
4
17
  title: 'Components/Inputs/TextAreaInput',
5
18
  component: TextAreaInput,
6
- // argTypes: {},
19
+ tags: ['autodocs'],
20
+ argTypes: {
21
+ value: { control: 'text' },
22
+ placeholder: { control: 'text' },
23
+ label: { control: 'text' },
24
+ rowHeight: { control: 'text' },
25
+ fontSize: { control: 'text' },
26
+ inputWidth: { control: 'text' },
27
+ resize: {
28
+ control: 'inline-radio',
29
+ options: ['none', 'both', 'horizontal', 'vertical'],
30
+ },
31
+ alignItems: {
32
+ control: 'inline-radio',
33
+ options: ['horizontal', 'vertical'],
34
+ },
35
+ appTheme: { control: 'inline-radio', options: ['light', 'dark'] },
36
+ isDisabled: { control: 'boolean' },
37
+ isError: { control: 'boolean' },
38
+ errorText: { control: 'text' },
39
+ infoTextMessage: { control: 'text' },
40
+ isRequiredLabel: { control: 'boolean' },
41
+ },
7
42
  }
8
43
 
9
- const Template = (args, { argTypes }) => ({
10
- // Components used in your story `template` are defined in the `components` object
44
+ const Template = (args) => ({
11
45
  components: { TextAreaInput },
12
- // The story's `args` need to be mapped into the template through the `setup()` method
13
- props: Object.keys(argTypes),
14
- template: '<text-area-input v-bind="$props" />',
15
-
16
- // import TextAreaInput from "@eturnity/eturnity_reusable_components/src/components/inputs/textAreaInput"
17
- // To use:
18
- // <text-area-input
19
- // placeholder="Comments"
20
- // @input-change="onInputChange({ value: $event, type: 'note' })"
21
- // :value="form.note"
22
- // rowHeight="4" //optional
23
- // :isError="false"
24
- // :errorText="$gettext('field_required')"
25
- // infoTextMessage="My info message"
26
- // label="Question 5"
27
- // alignItems="horizontal" // horizontal, vertical
28
- // :isDisabled="true"
29
- // />
46
+ setup() {
47
+ const model = ref(args.value)
48
+ watch(
49
+ () => args.value,
50
+ (v) => {
51
+ model.value = v
52
+ }
53
+ )
54
+ return { args, model }
55
+ },
56
+ template: `
57
+ <div style="padding: 16px;">
58
+ <TextAreaInput v-bind="args" :value="model" @input-change="model = $event" />
59
+ </div>
60
+ `,
30
61
  })
31
62
 
32
- export const Default = Template.bind({})
33
- Default.args = {
63
+ const baseArgs = {
34
64
  placeholder: 'Enter a comment',
35
- isDisabled: false,
36
65
  rowHeight: '2',
37
- isError: false,
38
- errorText: 'This field is required',
39
- infoTextMessage: '',
40
- label: '',
41
66
  value: '',
42
67
  alignItems: 'vertical',
43
68
  inputWidth: '350px',
44
69
  }
45
70
 
71
+ export const Default = Template.bind({})
72
+ Default.args = {
73
+ ...baseArgs,
74
+ }
75
+
46
76
  export const Disabled = Template.bind({})
47
77
  Disabled.args = {
48
- placeholder: 'Enter a comment',
78
+ ...baseArgs,
49
79
  isDisabled: true,
50
- rowHeight: '2',
51
- isError: false,
52
- errorText: 'This field is required',
53
- infoTextMessage: '',
54
- label: '',
55
- value: '',
56
- alignItems: 'vertical',
57
- inputWidth: '350px',
80
+ value: 'Cannot edit this',
58
81
  }
59
82
 
60
83
  export const Error = Template.bind({})
61
84
  Error.args = {
62
- placeholder: 'Enter a comment',
63
- isDisabled: false,
64
- rowHeight: '2',
85
+ ...baseArgs,
65
86
  isError: true,
66
87
  errorText: 'This field is required',
67
- infoTextMessage: '',
68
- label: '',
69
- value: '',
70
- alignItems: 'vertical',
71
- inputWidth: '350px',
72
88
  }
73
89
 
74
90
  export const WithLabel = Template.bind({})
75
91
  WithLabel.args = {
76
- placeholder: 'Enter a comment',
77
- isDisabled: false,
78
- rowHeight: '2',
79
- isError: false,
80
- errorText: 'This field is required',
81
- infoTextMessage: 'Here is some information',
92
+ ...baseArgs,
82
93
  label: 'Description',
94
+ infoTextMessage: 'Here is some information',
83
95
  value: 'Here is my description!',
84
- alignItems: 'vertical',
85
- inputWidth: '350px',
86
96
  }
87
97
 
88
98
  export const HorizontalLabel = Template.bind({})
89
99
  HorizontalLabel.args = {
90
- placeholder: 'Enter a comment',
91
- isDisabled: false,
92
- rowHeight: '2',
93
- isError: false,
94
- errorText: 'This field is required',
95
- infoTextMessage: 'Here is some information',
100
+ ...baseArgs,
96
101
  label: 'Description',
102
+ infoTextMessage: 'Here is some information',
97
103
  value: 'Here is my description!',
98
104
  alignItems: 'horizontal',
99
- inputWidth: '350px',
105
+ }
106
+
107
+ export const RequiredLabel = Template.bind({})
108
+ RequiredLabel.args = {
109
+ ...baseArgs,
110
+ label: 'Description',
111
+ isRequiredLabel: true,
100
112
  }
101
113
 
102
114
  export const LargerTextArea = Template.bind({})
103
115
  LargerTextArea.args = {
104
- placeholder: 'Enter a comment',
105
- isDisabled: false,
106
- rowHeight: '5',
107
- isError: false,
108
- errorText: 'This field is required',
109
- infoTextMessage: 'Here is some information',
116
+ ...baseArgs,
110
117
  label: 'Description',
118
+ rowHeight: '5',
111
119
  value: 'Here is my description!',
112
- alignItems: 'vertical',
120
+ inputWidth: null,
113
121
  }
114
122
 
115
123
  export const LargerFontSize = Template.bind({})
116
124
  LargerFontSize.args = {
117
- placeholder: 'Enter a comment',
118
- isDisabled: false,
125
+ ...baseArgs,
126
+ label: 'Description',
119
127
  rowHeight: '5',
120
128
  fontSize: '24px',
121
- isError: false,
122
- errorText: 'This field is required',
123
- infoTextMessage: 'Here is some information',
124
- label: 'Description',
125
129
  value: 'Here is my description!',
126
- alignItems: 'vertical',
130
+ inputWidth: null,
127
131
  }
@@ -0,0 +1,90 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import TextAreaInput from '@/components/inputs/textAreaInput'
4
+ import InfoText from '@/components/infoText'
5
+
6
+ // InfoText renders an icon; the real iconCache.mjs uses import.meta.
7
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
8
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
9
+ }))
10
+
11
+ describe('TextAreaInput', () => {
12
+ const mountTextArea = (props = {}) =>
13
+ mount(TextAreaInput, { props: { value: '', ...props } })
14
+
15
+ it('renders a textarea with the value, placeholder and row height', () => {
16
+ const textarea = mountTextArea({
17
+ value: 'Hello',
18
+ placeholder: 'Enter a comment',
19
+ rowHeight: '5',
20
+ }).find('textarea')
21
+
22
+ expect(textarea.element.value).toBe('Hello')
23
+ expect(textarea.attributes('placeholder')).toBe('Enter a comment')
24
+ expect(textarea.attributes('rows')).toBe('5')
25
+ })
26
+
27
+ it('emits input-change with the new value when typing', async () => {
28
+ const wrapper = mountTextArea()
29
+
30
+ await wrapper.find('textarea').setValue('new text')
31
+
32
+ expect(wrapper.emitted('input-change')).toHaveLength(1)
33
+ expect(wrapper.emitted('input-change')[0]).toEqual(['new text'])
34
+ })
35
+
36
+ it('does not emit input-change when disabled', async () => {
37
+ const wrapper = mountTextArea({ isDisabled: true })
38
+ const textarea = wrapper.find('textarea')
39
+
40
+ expect(textarea.attributes('disabled')).toBeDefined()
41
+ await textarea.setValue('blocked')
42
+
43
+ expect(wrapper.emitted('input-change')).toBeUndefined()
44
+ })
45
+
46
+ it('emits input-blur and input-focus with the current value', async () => {
47
+ const wrapper = mountTextArea({ value: 'abc' })
48
+ const textarea = wrapper.find('textarea')
49
+
50
+ await textarea.trigger('focus')
51
+ await textarea.trigger('blur')
52
+
53
+ expect(wrapper.emitted('input-focus')[0]).toEqual(['abc'])
54
+ expect(wrapper.emitted('input-blur')[0]).toEqual(['abc'])
55
+ })
56
+
57
+ it('renders the label with the required star', () => {
58
+ const wrapper = mountTextArea({ label: 'Description', isRequiredLabel: true })
59
+ expect(wrapper.text()).toContain('Description')
60
+ expect(wrapper.text()).toContain('*')
61
+ })
62
+
63
+ it('renders the info text only when a message is provided', () => {
64
+ expect(
65
+ mountTextArea({ label: 'Description' }).findComponent(InfoText).exists()
66
+ ).toBe(false)
67
+ expect(
68
+ mountTextArea({ label: 'Description', infoTextMessage: 'Some hint' })
69
+ .findComponent(InfoText)
70
+ .exists()
71
+ ).toBe(true)
72
+ })
73
+
74
+ it('shows the error text only when isError is set', () => {
75
+ expect(mountTextArea({ errorText: 'Required' }).text()).not.toContain(
76
+ 'Required'
77
+ )
78
+ expect(
79
+ mountTextArea({ isError: true, errorText: 'Required' }).text()
80
+ ).toContain('Required')
81
+ })
82
+
83
+ it('prefers inputDataId over dataId for the textarea data-id', () => {
84
+ const textarea = mountTextArea({
85
+ inputDataId: 'note_input',
86
+ dataId: 'fallback',
87
+ }).find('textarea')
88
+ expect(textarea.attributes('data-id')).toBe('note_input')
89
+ })
90
+ })
@@ -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
+ }