@mozaic-ds/vue 2.7.0 → 2.8.0
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/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +165 -97
- package/dist/mozaic-vue.js +2920 -1011
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +5 -1
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +3 -2
- package/src/components/drawer/MDrawer.spec.ts +68 -1
- package/src/components/drawer/MDrawer.vue +30 -6
- package/src/components/drawer/README.md +1 -0
- package/src/components/iconbutton/MIconButton.vue +5 -0
- package/src/components/loadingoverlay/MLoadingOverlay.stories.ts +1 -1
- package/src/components/modal/MModal.spec.ts +36 -1
- package/src/components/modal/MModal.vue +11 -1
- package/src/components/modal/README.md +1 -0
- package/src/components/phonenumber/MPhoneNumber.spec.ts +294 -0
- package/src/components/phonenumber/MPhoneNumber.stories.ts +88 -0
- package/src/components/phonenumber/MPhoneNumber.vue +271 -0
- package/src/components/phonenumber/README.md +26 -0
- package/src/components/pincode/README.md +1 -1
- package/src/components/quantityselector/MQuantitySelector.stories.ts +0 -7
- package/src/components/togglegroup/MToggleGroup.vue +0 -2
- package/src/components/togglegroup/README.md +1 -1
- package/src/main.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mozaic-ds/vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mozaic-Vue is the Vue.js implementation of ADEO Design system",
|
|
6
6
|
"author": "ADEO - ADEO Design system",
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
"vite-plugin-dts": "^4.5.3",
|
|
83
83
|
"vitest": "^3.0.9",
|
|
84
84
|
"vue-component-meta": "^3.0.8",
|
|
85
|
-
"vue-eslint-parser": "^10.1.1"
|
|
85
|
+
"vue-eslint-parser": "^10.1.1",
|
|
86
|
+
"libphonenumber-js": "^1.12.23"
|
|
86
87
|
},
|
|
87
88
|
"bugs": {
|
|
88
89
|
"url": "https://github.com/adeo/mozaic-vue/issues"
|
|
@@ -2,6 +2,19 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { mount } from '@vue/test-utils';
|
|
3
3
|
import MDrawer from '@/components/drawer/MDrawer.vue';
|
|
4
4
|
|
|
5
|
+
// Stubs for child components
|
|
6
|
+
const stubs = {
|
|
7
|
+
ArrowBack24: { template: '<svg />' },
|
|
8
|
+
Cross24: { template: '<svg />' },
|
|
9
|
+
MIconButton: {
|
|
10
|
+
template: `<button @click="$emit('click')"><slot name="icon"/></button>`,
|
|
11
|
+
},
|
|
12
|
+
MOverlay: {
|
|
13
|
+
name: 'MOverlay',
|
|
14
|
+
template: `<div class="overlay" @click="$emit('click')"><slot/></div>`,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
5
18
|
describe('MDrawer component', () => {
|
|
6
19
|
it('renders title and contentTitle when provided', () => {
|
|
7
20
|
const wrapper = mount(MDrawer, {
|
|
@@ -13,6 +26,7 @@ describe('MDrawer component', () => {
|
|
|
13
26
|
slots: {
|
|
14
27
|
default: '<p>Drawer content</p>',
|
|
15
28
|
},
|
|
29
|
+
global: { stubs },
|
|
16
30
|
});
|
|
17
31
|
|
|
18
32
|
expect(wrapper.find('.mc-drawer__title').text()).toBe('Main Drawer Title');
|
|
@@ -30,6 +44,7 @@ describe('MDrawer component', () => {
|
|
|
30
44
|
open: true,
|
|
31
45
|
title: 'Test Title',
|
|
32
46
|
},
|
|
47
|
+
global: { stubs },
|
|
33
48
|
});
|
|
34
49
|
|
|
35
50
|
const closeButton = wrapper.find('.mc-drawer__close');
|
|
@@ -46,6 +61,7 @@ describe('MDrawer component', () => {
|
|
|
46
61
|
title: 'Test Title',
|
|
47
62
|
back: true,
|
|
48
63
|
},
|
|
64
|
+
global: { stubs },
|
|
49
65
|
});
|
|
50
66
|
|
|
51
67
|
const backButton = wrapper.find('.mc-drawer__back');
|
|
@@ -63,6 +79,7 @@ describe('MDrawer component', () => {
|
|
|
63
79
|
slots: {
|
|
64
80
|
footer: '<button>Footer Button</button>',
|
|
65
81
|
},
|
|
82
|
+
global: { stubs },
|
|
66
83
|
});
|
|
67
84
|
|
|
68
85
|
expect(wrapper.find('.mc-drawer__footer').exists()).toBe(true);
|
|
@@ -79,6 +96,7 @@ describe('MDrawer component', () => {
|
|
|
79
96
|
extended: true,
|
|
80
97
|
position: 'left',
|
|
81
98
|
},
|
|
99
|
+
global: { stubs },
|
|
82
100
|
});
|
|
83
101
|
|
|
84
102
|
const section = wrapper.find('section.mc-drawer');
|
|
@@ -93,6 +111,7 @@ describe('MDrawer component', () => {
|
|
|
93
111
|
open: true,
|
|
94
112
|
title: 'Test Title',
|
|
95
113
|
},
|
|
114
|
+
global: { stubs },
|
|
96
115
|
});
|
|
97
116
|
|
|
98
117
|
expect(wrapper.find('.mc-drawer__back').exists()).toBe(false);
|
|
@@ -104,6 +123,7 @@ describe('MDrawer component', () => {
|
|
|
104
123
|
title: 'Test Title',
|
|
105
124
|
},
|
|
106
125
|
attachTo: document.body,
|
|
126
|
+
global: { stubs },
|
|
107
127
|
});
|
|
108
128
|
|
|
109
129
|
const titleElement = wrapper.find('.mc-drawer__title').element;
|
|
@@ -116,13 +136,60 @@ describe('MDrawer component', () => {
|
|
|
116
136
|
const wrapper = mount(MDrawer, {
|
|
117
137
|
props: {
|
|
118
138
|
title: 'Test Title',
|
|
119
|
-
open: true
|
|
139
|
+
open: true,
|
|
120
140
|
},
|
|
121
141
|
attachTo: document.body,
|
|
142
|
+
global: { stubs },
|
|
122
143
|
});
|
|
123
144
|
|
|
124
145
|
const titleElement = wrapper.find('.mc-drawer__title').element;
|
|
125
146
|
await wrapper.setProps({ open: false });
|
|
126
147
|
expect(document.activeElement).not.toBe(titleElement);
|
|
127
148
|
});
|
|
149
|
+
|
|
150
|
+
// ✅ New tests for closeOnOverlay behavior
|
|
151
|
+
it('emits update:open false when overlay is clicked and closeOnOverlay is true', async () => {
|
|
152
|
+
const wrapper = mount(MDrawer, {
|
|
153
|
+
props: {
|
|
154
|
+
open: true,
|
|
155
|
+
title: 'Test Title',
|
|
156
|
+
closeOnOverlay: true,
|
|
157
|
+
},
|
|
158
|
+
global: { stubs },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await wrapper.find('.overlay').trigger('click');
|
|
162
|
+
|
|
163
|
+
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
164
|
+
expect(wrapper.emitted('update:open')![0]).toEqual([false]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('does not emit update:open when overlay is clicked and closeOnOverlay is false', async () => {
|
|
168
|
+
const wrapper = mount(MDrawer, {
|
|
169
|
+
props: {
|
|
170
|
+
open: true,
|
|
171
|
+
title: 'Test Title',
|
|
172
|
+
closeOnOverlay: false,
|
|
173
|
+
},
|
|
174
|
+
global: { stubs },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await wrapper.find('.overlay').trigger('click');
|
|
178
|
+
|
|
179
|
+
expect(wrapper.emitted('update:open')).toBeFalsy();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does not emit update:open when overlay is clicked and closeOnOverlay is not set', async () => {
|
|
183
|
+
const wrapper = mount(MDrawer, {
|
|
184
|
+
props: {
|
|
185
|
+
open: true,
|
|
186
|
+
title: 'Test Title',
|
|
187
|
+
},
|
|
188
|
+
global: { stubs },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await wrapper.find('.overlay').trigger('click');
|
|
192
|
+
|
|
193
|
+
expect(wrapper.emitted('update:open')).toBeFalsy();
|
|
194
|
+
});
|
|
128
195
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<MOverlay
|
|
2
|
+
<MOverlay
|
|
3
|
+
:is-visible="open"
|
|
4
|
+
dialogLabel="drawerTitle"
|
|
5
|
+
@click="onClickOverlay"
|
|
6
|
+
>
|
|
3
7
|
<section
|
|
4
8
|
class="mc-drawer"
|
|
5
9
|
:class="classObject"
|
|
@@ -24,7 +28,14 @@
|
|
|
24
28
|
<ArrowBack24 aria-hidden="true" />
|
|
25
29
|
</template>
|
|
26
30
|
</MIconButton>
|
|
27
|
-
<h2
|
|
31
|
+
<h2
|
|
32
|
+
class="mc-drawer__title"
|
|
33
|
+
tabindex="-1"
|
|
34
|
+
id="drawerTitle"
|
|
35
|
+
ref="titleRef"
|
|
36
|
+
>
|
|
37
|
+
{{ title }}
|
|
38
|
+
</h2>
|
|
28
39
|
<MIconButton
|
|
29
40
|
class="mc-drawer__close"
|
|
30
41
|
aria-label="Close"
|
|
@@ -86,6 +97,10 @@ const props = defineProps<{
|
|
|
86
97
|
* Title of the content of the drawer.
|
|
87
98
|
*/
|
|
88
99
|
contentTitle?: string;
|
|
100
|
+
/**
|
|
101
|
+
* if `true`, close the drawer when clicking the overlay.
|
|
102
|
+
*/
|
|
103
|
+
closeOnOverlay?: boolean;
|
|
89
104
|
}>();
|
|
90
105
|
|
|
91
106
|
defineSlots<{
|
|
@@ -116,11 +131,20 @@ watch(
|
|
|
116
131
|
);
|
|
117
132
|
|
|
118
133
|
const titleRef = ref<HTMLElement | null>(null);
|
|
119
|
-
watch(
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
watch(
|
|
135
|
+
() => props.open,
|
|
136
|
+
(newValue) => {
|
|
137
|
+
if (newValue && titleRef.value) {
|
|
138
|
+
titleRef.value.focus();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const onClickOverlay = () => {
|
|
144
|
+
if (props.closeOnOverlay) {
|
|
145
|
+
onClose();
|
|
122
146
|
}
|
|
123
|
-
}
|
|
147
|
+
};
|
|
124
148
|
|
|
125
149
|
const onClose = () => {
|
|
126
150
|
emit('update:open', false);
|
|
@@ -13,6 +13,7 @@ A drawer is a sliding panel that appears from the side of the screen, providing
|
|
|
13
13
|
| `back` | If `true`, display the back button. | `boolean` | - |
|
|
14
14
|
| `title*` | Title of the drawer. | `string` | - |
|
|
15
15
|
| `contentTitle` | Title of the content of the drawer. | `string` | - |
|
|
16
|
+
| `closeOnOverlay` | if `true`, close the drawer when clicking the overlay. | `boolean` | - |
|
|
16
17
|
|
|
17
18
|
## Slots
|
|
18
19
|
|
|
@@ -8,7 +8,8 @@ const stubs = {
|
|
|
8
8
|
template: `<button @click="$emit('click')"><slot name="icon"/></button>`,
|
|
9
9
|
},
|
|
10
10
|
MOverlay: {
|
|
11
|
-
|
|
11
|
+
name: 'MOverlay',
|
|
12
|
+
template: `<div class="overlay" @click="$emit('click')"><slot/></div>`,
|
|
12
13
|
},
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -100,4 +101,38 @@ describe('MModal component', () => {
|
|
|
100
101
|
|
|
101
102
|
expect(wrapper.find('.mc-modal').classes()).toContain('is-open');
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
it('emits update:open with false when overlay clicked and closeOnOverlay is true', async () => {
|
|
106
|
+
const wrapper = mount(MModal, {
|
|
107
|
+
props: { open: true, title: 'Title', closeOnOverlay: true },
|
|
108
|
+
global: { stubs },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await wrapper.findComponent({ name: 'MOverlay' }).trigger('click');
|
|
112
|
+
|
|
113
|
+
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
114
|
+
expect(wrapper.emitted('update:open')![0]).toEqual([false]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not emit update:open when overlay clicked and closeOnOverlay is false', async () => {
|
|
118
|
+
const wrapper = mount(MModal, {
|
|
119
|
+
props: { open: true, title: 'Title', closeOnOverlay: false },
|
|
120
|
+
global: { stubs },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await wrapper.findComponent({ name: 'MOverlay' }).trigger('click');
|
|
124
|
+
|
|
125
|
+
expect(wrapper.emitted('update:open')).toBeFalsy();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('does not emit update:open when overlay clicked and closeOnOverlay is not set', async () => {
|
|
129
|
+
const wrapper = mount(MModal, {
|
|
130
|
+
props: { open: true, title: 'Title' },
|
|
131
|
+
global: { stubs },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await wrapper.findComponent({ name: 'MOverlay' }).trigger('click');
|
|
135
|
+
|
|
136
|
+
expect(wrapper.emitted('update:open')).toBeFalsy();
|
|
137
|
+
});
|
|
103
138
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<MOverlay :is-visible="open" dialogLabel="modalTitle">
|
|
2
|
+
<MOverlay :is-visible="open" dialogLabel="modalTitle" @click="onClickOverlay">
|
|
3
3
|
<section
|
|
4
4
|
class="mc-modal"
|
|
5
5
|
:class="classObject"
|
|
@@ -72,6 +72,10 @@ const props = withDefaults(
|
|
|
72
72
|
* if `true`, display the close button.
|
|
73
73
|
*/
|
|
74
74
|
closable?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* if `true`, close the modal when clicking the overlay.
|
|
77
|
+
*/
|
|
78
|
+
closeOnOverlay?: boolean;
|
|
75
79
|
}>(),
|
|
76
80
|
{
|
|
77
81
|
closable: true,
|
|
@@ -110,6 +114,12 @@ watch(
|
|
|
110
114
|
},
|
|
111
115
|
);
|
|
112
116
|
|
|
117
|
+
const onClickOverlay = () => {
|
|
118
|
+
if (props.closeOnOverlay) {
|
|
119
|
+
onClose();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
113
123
|
const onClose = () => {
|
|
114
124
|
emit('update:open', false);
|
|
115
125
|
};
|
|
@@ -11,6 +11,7 @@ A modal is a dialog window that appears on top of the main content, requiring us
|
|
|
11
11
|
| `title*` | Title of the modal. | `string` | - |
|
|
12
12
|
| `description` | Description of the modal. | `string` | - |
|
|
13
13
|
| `closable` | if `true`, display the close button. | `boolean` | `true` |
|
|
14
|
+
| `closeOnOverlay` | if `true`, close the modal when clicking the overlay. | `boolean` | - |
|
|
14
15
|
|
|
15
16
|
## Slots
|
|
16
17
|
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { mount, VueWrapper } from '@vue/test-utils';
|
|
3
|
+
import { nextTick } from 'vue';
|
|
4
|
+
import MPhoneNumber from './MPhoneNumber.vue';
|
|
5
|
+
import { isValidPhoneNumber } from 'libphonenumber-js';
|
|
6
|
+
|
|
7
|
+
vi.mock('libphonenumber-js', () => ({
|
|
8
|
+
default: vi.fn(),
|
|
9
|
+
isValidPhoneNumber: vi.fn(),
|
|
10
|
+
getCountries: vi.fn(() => ['FR', 'US', 'PT', 'DE', 'ES']),
|
|
11
|
+
getCountryCallingCode: vi.fn((country) => {
|
|
12
|
+
const codes = { FR: '33', US: '1', PT: '351', DE: '49', ES: '34' };
|
|
13
|
+
return codes[country as keyof typeof codes] || '33';
|
|
14
|
+
}),
|
|
15
|
+
getExampleNumber: vi.fn(() => ({
|
|
16
|
+
formatNational: () => '01 23 45 67 89',
|
|
17
|
+
})),
|
|
18
|
+
parsePhoneNumberFromString: vi.fn((value) => {
|
|
19
|
+
if (!value) return null;
|
|
20
|
+
return {
|
|
21
|
+
formatNational: () => {
|
|
22
|
+
if (value === '+33123456789') return '01 23 45 67 89';
|
|
23
|
+
return value;
|
|
24
|
+
},
|
|
25
|
+
number: value.startsWith('+') ? value : `+${value}`,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock(
|
|
31
|
+
'@mozaic-ds/icons-vue/src/components/ChevronDown20/ChevronDown20.vue',
|
|
32
|
+
() => ({
|
|
33
|
+
default: {
|
|
34
|
+
name: 'ChevronDown20',
|
|
35
|
+
template: '<svg data-testid="chevron-icon" aria-hidden="true"></svg>',
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
describe('MPhoneNumber', () => {
|
|
41
|
+
let wrapper: VueWrapper<InstanceType<typeof MPhoneNumber>>;
|
|
42
|
+
|
|
43
|
+
const defaultProps = {
|
|
44
|
+
id: 'phone-input',
|
|
45
|
+
defaultCountry: 'FR' as const,
|
|
46
|
+
modelValue: '',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
wrapper = mount(MPhoneNumber, {
|
|
51
|
+
props: defaultProps,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
wrapper.unmount();
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Rendering', () => {
|
|
61
|
+
it('should render the component', () => {
|
|
62
|
+
expect(wrapper.find('.mc-phone-number-input').exists()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Country Selection', () => {
|
|
67
|
+
it('should render country selector and flag by default', () => {
|
|
68
|
+
expect(
|
|
69
|
+
wrapper.find('select.mc-phone-number-input__select').exists(),
|
|
70
|
+
).toBe(true);
|
|
71
|
+
expect(wrapper.find('.mc-phone-number-input__flag').exists()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should hide country selector when flag prop is false', () => {
|
|
75
|
+
wrapper = mount(MPhoneNumber, {
|
|
76
|
+
props: { ...defaultProps, flag: false },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
wrapper.find('.mc-phone-number-input__select-wrapper').classes(),
|
|
81
|
+
).toContain('mc-phone-number-input__select-wrapper--hidden');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should render all country options', () => {
|
|
85
|
+
const options = wrapper.findAll('option');
|
|
86
|
+
expect(options.length).toBeGreaterThan(1);
|
|
87
|
+
expect(options.some((opt) => opt.text().includes('+33'))).toBe(true);
|
|
88
|
+
expect(options.some((opt) => opt.text().includes('+1'))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should display correct flag image and alt attribute', () => {
|
|
92
|
+
const flagImage = wrapper.find('img.mc-phone-number-input__flag-image');
|
|
93
|
+
expect(flagImage.exists()).toBe(true);
|
|
94
|
+
expect(flagImage.attributes('src')).toContain('flagcdn.com/fr.svg');
|
|
95
|
+
expect(flagImage.attributes('alt')).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should render chevron icon with aria-hidden true', () => {
|
|
99
|
+
const chevron = wrapper.find('[data-testid="chevron-icon"]');
|
|
100
|
+
expect(chevron.exists()).toBe(true);
|
|
101
|
+
expect(chevron.attributes('aria-hidden')).toBe('true');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Phone Input', () => {
|
|
106
|
+
it('should render phone input field with correct id', () => {
|
|
107
|
+
const input = wrapper.find('input[type="tel"]');
|
|
108
|
+
expect(input.exists()).toBe(true);
|
|
109
|
+
expect(input.attributes('id')).toBe(defaultProps.id);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should display country calling code prefix by default', () => {
|
|
113
|
+
expect(
|
|
114
|
+
wrapper.find('.mc-phone-number-input__country-code').exists(),
|
|
115
|
+
).toBe(true);
|
|
116
|
+
expect(wrapper.find('.mc-phone-number-input__country-code').text()).toBe(
|
|
117
|
+
'+33',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should hide prefix when prefix prop is false', () => {
|
|
122
|
+
wrapper = mount(MPhoneNumber, {
|
|
123
|
+
props: { ...defaultProps, prefix: false },
|
|
124
|
+
});
|
|
125
|
+
expect(
|
|
126
|
+
wrapper.find('.mc-phone-number-input__country-code').exists(),
|
|
127
|
+
).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should set custom placeholder if provided', () => {
|
|
131
|
+
const placeholder = 'Enter your phone number';
|
|
132
|
+
wrapper = mount(MPhoneNumber, {
|
|
133
|
+
props: { ...defaultProps, placeholder },
|
|
134
|
+
});
|
|
135
|
+
expect(wrapper.find('input[type="tel"]').attributes('placeholder')).toBe(
|
|
136
|
+
placeholder,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should use dynamic placeholder when no custom placeholder provided', () => {
|
|
141
|
+
expect(wrapper.find('input[type="tel"]').attributes('placeholder')).toBe(
|
|
142
|
+
'01 23 45 67 89',
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('States', () => {
|
|
148
|
+
it('should be enabled by default', () => {
|
|
149
|
+
const input = wrapper.find('input[type="tel"]');
|
|
150
|
+
const select = wrapper.find('select');
|
|
151
|
+
expect(input.attributes('disabled')).toBeUndefined();
|
|
152
|
+
expect(input.attributes('readonly')).toBeUndefined();
|
|
153
|
+
expect(select.attributes('disabled')).toBeUndefined();
|
|
154
|
+
expect(select.attributes('readonly')).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should be disabled when disabled prop is true', () => {
|
|
158
|
+
wrapper = mount(MPhoneNumber, {
|
|
159
|
+
props: { ...defaultProps, disabled: true },
|
|
160
|
+
});
|
|
161
|
+
expect(
|
|
162
|
+
wrapper.find('input[type="tel"]').attributes('disabled'),
|
|
163
|
+
).toBeDefined();
|
|
164
|
+
expect(wrapper.find('select').attributes('disabled')).toBeDefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should be readonly when readonly prop is true', () => {
|
|
168
|
+
wrapper = mount(MPhoneNumber, {
|
|
169
|
+
props: { ...defaultProps, readonly: true },
|
|
170
|
+
});
|
|
171
|
+
expect(
|
|
172
|
+
wrapper.find('input[type="tel"]').attributes('readonly'),
|
|
173
|
+
).toBeDefined();
|
|
174
|
+
expect(wrapper.find('select').attributes('readonly')).toBeDefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should apply invalid class when isInvalid is true', () => {
|
|
178
|
+
wrapper = mount(MPhoneNumber, {
|
|
179
|
+
props: { ...defaultProps, isInvalid: true },
|
|
180
|
+
});
|
|
181
|
+
expect(wrapper.find('.mc-phone-number-input__input').classes()).toContain(
|
|
182
|
+
'is-invalid',
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Sizes', () => {
|
|
188
|
+
it('should have medium size by default', () => {
|
|
189
|
+
expect(wrapper.find('select').classes()).not.toContain('mc-select--s');
|
|
190
|
+
expect(
|
|
191
|
+
wrapper.find('.mc-phone-number-input__input').classes(),
|
|
192
|
+
).not.toContain('mc-text-input--s');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should apply small size when size prop is s', () => {
|
|
196
|
+
wrapper = mount(MPhoneNumber, {
|
|
197
|
+
props: { ...defaultProps, size: 's' },
|
|
198
|
+
});
|
|
199
|
+
expect(wrapper.find('select').classes()).toContain('mc-select--s');
|
|
200
|
+
expect(wrapper.find('.mc-phone-number-input__input').classes()).toContain(
|
|
201
|
+
'mc-text-input--s',
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Events', () => {
|
|
207
|
+
it('should emit update:modelValue with international format on input', async () => {
|
|
208
|
+
const input = wrapper.find('input[type="tel"]');
|
|
209
|
+
|
|
210
|
+
await input.setValue('+33123456789');
|
|
211
|
+
await nextTick();
|
|
212
|
+
|
|
213
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
|
|
214
|
+
|
|
215
|
+
const emittedVal = wrapper.emitted('update:modelValue')?.[0][0] as string;
|
|
216
|
+
expect(emittedVal.startsWith('+')).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should emit valid event on input', async () => {
|
|
220
|
+
const input = wrapper.find('input[type="tel"]');
|
|
221
|
+
await input.setValue('+33123456789');
|
|
222
|
+
await nextTick();
|
|
223
|
+
expect(wrapper.emitted('valid')).toBeTruthy();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should sanitize input removing invalid characters', async () => {
|
|
227
|
+
const input = wrapper.find('input[type="tel"]');
|
|
228
|
+
await input.setValue('123abc456def+() -');
|
|
229
|
+
|
|
230
|
+
expect((input.element as HTMLInputElement).value).toBe('123456+() -');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Validation', () => {
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
vi.mocked(isValidPhoneNumber).mockImplementation((number: string) => {
|
|
237
|
+
const digits = number.replace(/\D/g, '');
|
|
238
|
+
return digits.length >= 10;
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should mark input invalid if phone number is invalid', async () => {
|
|
243
|
+
wrapper = mount(MPhoneNumber, {
|
|
244
|
+
props: { ...defaultProps, modelValue: '123' },
|
|
245
|
+
});
|
|
246
|
+
await nextTick();
|
|
247
|
+
expect(wrapper.find('.mc-phone-number-input__input').classes()).toContain(
|
|
248
|
+
'is-invalid',
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should not mark input invalid if empty', async () => {
|
|
253
|
+
wrapper = mount(MPhoneNumber, {
|
|
254
|
+
props: { ...defaultProps, modelValue: '' },
|
|
255
|
+
});
|
|
256
|
+
await nextTick();
|
|
257
|
+
expect(
|
|
258
|
+
wrapper.find('.mc-phone-number-input__input').classes(),
|
|
259
|
+
).not.toContain('is-invalid');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Accessibility', () => {
|
|
264
|
+
it('should set aria-invalid attribute correctly', async () => {
|
|
265
|
+
await wrapper.setProps({ isInvalid: true });
|
|
266
|
+
const input = wrapper.find('input[type="tel"]');
|
|
267
|
+
expect(input.attributes('aria-invalid')).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should use provided id on input', () => {
|
|
271
|
+
const input = wrapper.find('input[type="tel"]');
|
|
272
|
+
expect(input.attributes('id')).toBe(defaultProps.id);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should set alt attribute on flag image', () => {
|
|
276
|
+
const flagImg = wrapper.find('img.mc-phone-number-input__flag-image');
|
|
277
|
+
expect(flagImg.attributes('alt')).toBeTruthy();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should mark chevron icon aria-hidden as true', () => {
|
|
281
|
+
const chevron = wrapper.find('[data-testid="chevron-icon"]');
|
|
282
|
+
expect(chevron.attributes('aria-hidden')).toBe('true');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('Props reactivity', () => {
|
|
287
|
+
it('should update input when modelValue changes', async () => {
|
|
288
|
+
await wrapper.setProps({ modelValue: '+33123456789' });
|
|
289
|
+
await nextTick();
|
|
290
|
+
const input = wrapper.find('input[type="tel"]');
|
|
291
|
+
expect((input.element as HTMLInputElement).value).toBe('+33123456789');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|