diff --git a/CHANGELOG.md b/CHANGELOG.md index caf21dd..ae4a917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.8] - 2025-07-23 +### Added +- ProgressBar component + +## [1.0.7] - 2025-07-23 +### Added +- Select component +- Badge component + ## [1.0.6] - 2025-07-23 ### Added - Checkbox component diff --git a/README.md b/README.md index 6872d61..11390d3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # visua-vue -**Current version: v1.0.6** \ No newline at end of file +**Current version: v1.0.8** \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb36326..f5dab22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cellule-financiere-pmo/visua-vue", - "version": "1.0.6", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cellule-financiere-pmo/visua-vue", - "version": "1.0.6", + "version": "1.0.8", "license": "ISC", "dependencies": { "@cellule-financiere-pmo/visua": "1.1.3", diff --git a/package.json b/package.json index 4db8cd9..4f3f114 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cellule-financiere-pmo/visua-vue", - "version": "1.0.6", + "version": "1.0.8", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index e582a0d..9c3f67f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,10 @@ // import VLinkView from '../template/VLinkView.vue'; // import VAccordionView from '../template/VAccordionView.vue'; // import VInputView from '../template/VInputView.vue'; -import VCheckboxView from '../template/VCheckboxView.vue'; +// import VCheckboxView from '../template/VCheckboxView.vue'; +// import VBadgeView from '../template/VBadgeView.vue'; +// import VSelectView from '../template/VSelectView.vue'; +import VProgressBarView from '../template/VProgressBarView.vue'; @@ -14,5 +17,8 @@ import VCheckboxView from '../template/VCheckboxView.vue'; - + + + + diff --git a/src/assets/style/primevue-configuration.css b/src/assets/style/primevue-configuration.css index c381491..a92e270 100644 --- a/src/assets/style/primevue-configuration.css +++ b/src/assets/style/primevue-configuration.css @@ -8,3 +8,8 @@ @import './primevue-style/various.css'; @import './primevue-style/form.css'; @import './primevue-style/checkbox.css'; +@import './primevue-style/tag.css'; +@import './primevue-style/select.css'; +@import './primevue-style/overlay.css'; +@import './primevue-style/iconfield.css'; +@import './primevue-style/progressbar.css'; diff --git a/src/assets/style/primevue-style/iconfield.css b/src/assets/style/primevue-style/iconfield.css new file mode 100644 index 0000000..d9f6df5 --- /dev/null +++ b/src/assets/style/primevue-style/iconfield.css @@ -0,0 +1,3 @@ +:root{ + --p-iconfield-icon-color: var(--input-color); +} diff --git a/src/assets/style/primevue-style/overlay.css b/src/assets/style/primevue-style/overlay.css new file mode 100644 index 0000000..3850fc9 --- /dev/null +++ b/src/assets/style/primevue-style/overlay.css @@ -0,0 +1,17 @@ +:root{ + --p-overlay-modal-background: var(--background-lifted-grey); + --p-overlay-modal-border-color: var(--border-default-grey); + --p-overlay-modal-color: var(--text-default-grey); + --p-overlay-popover-background: var(--background-lifted-grey); + --p-overlay-popover-border-color: var(--border-default-grey); + --p-overlay-popover-color: var(--text-default-grey); + --p-overlay-navigation-shadow: var(--shadow); + --p-overlay-modal-border-radius: 0px; + --p-overlay-modal-padding: 1.25rem; + --p-overlay-modal-shadow: var(--shadow); + --p-overlay-popover-border-radius: 0px; + --p-overlay-popover-padding: 0.75rem; + --p-overlay-popover-shadow: var(--shadow); + --p-overlay-select-border-radius: 0px; + --p-overlay-select-shadow: var(--shadow); +} diff --git a/src/assets/style/primevue-style/progressbar.css b/src/assets/style/primevue-style/progressbar.css new file mode 100644 index 0000000..e39e63b --- /dev/null +++ b/src/assets/style/primevue-style/progressbar.css @@ -0,0 +1,9 @@ +:root{ + --p-progressbar-background: var(--background-contrast-grey); + --p-progressbar-height: 1rem; + --p-progressbar-border-radius: 0.75rem; + --p-progressbar-value-background: var(--background-active-blue-france); + --p-progressbar-label-color: var(--text-inverted-grey); + --p-progressbar-label-font-size: var(--text-body-XS-mention-text-Medium-size); + --p-progressbar-label-font-weight: var(--text-body-XS-mention-text-Medium-weight); +} diff --git a/src/assets/style/primevue-style/select.css b/src/assets/style/primevue-style/select.css new file mode 100644 index 0000000..1f271dd --- /dev/null +++ b/src/assets/style/primevue-style/select.css @@ -0,0 +1,47 @@ +:root{ + --p-select-background: var(--input-background); + --p-select-disabled-background: var(--input-disabled-background); + --p-select-border-color: var(--input-border-color); + --p-select-hover-border-color: var(--input-border-color); + --p-select-color: var(--input-color); + --p-select-disabled-color: var(--input-disabled-color); + --p-select-placeholder-color: var(--input-color); + --p-select-padding-x: var(--input-padding-x); + --p-select-padding-y: var(--input-padding-y); + --p-select-border-radius: var(--input-border-raduis); + --p-select-dropdown-width: 2rem; + --p-select-dropdown-color: var(--input-color); + --p-select-overlay-background: var(--input-background); + --p-select-overlay-border-color: var(--input-border-color); + --p-select-overlay-border-radius: 0px 0px 0.25rem 0.25rem; + --p-select-overlay-color: var(--input-color); + --p-select-overlay-shadow: var(--shadow); + --p-select-list-padding: 0.25rem; + --p-select-list-gap: 0.125rem; + --p-select-list-header-padding: 0.5rem 1rem; + /* options */ + --p-select-option-selected-background: var(--background-active-blue-france); + --p-select-option-selected-focus-background: var(--background-active-blue-france); + --p-select-option-color: var(--input-color); + --p-select-option-focus-color: var(--input-color); + --p-select-option-selected-color: var(--text-inverted-blue-france); + --p-select-option-padding: 0.5rem 0.75rem; + --p-select-option-border-radius: 0px; + --p-select-option-group-background: var(--input-background); + --p-select-option-group-color: var(--input-color); + --p-select-option-group-font-weight: var(--text-body-MD-standard-text-Regular-weight); + --p-select-option-group-padding: 0.25rem; + --p-select-clear-icon-color: var(--input-color); + --p-select-checkmark-color: var(--input-color); + --p-select-checkmark-gutter-start: 0.125rem; + --p-select-checkmark-gutter-end: 0.125rem; + --p-select-empty-message-padding: 0.25rem; + /* focus */ + --p-select-focus-border-color: var(--input-border-color); + --p-select-focus-ring-width: var(--focus-width); + --p-select-focus-ring-style: var(--focus-style); + --p-select-focus-ring-color: var(--focus-color); + --p-select-focus-ring-offset: var(--focus-offset); + --p-select-option-focus-background: var(--background-transparent-active); + --p-select-option-selected-focus-color: var(--text-inverted-blue-france); +} diff --git a/src/assets/style/primevue-style/tag.css b/src/assets/style/primevue-style/tag.css new file mode 100644 index 0000000..8ca750c --- /dev/null +++ b/src/assets/style/primevue-style/tag.css @@ -0,0 +1,23 @@ +:root { + --p-tag-icon-size: var(--text-body-MD-standard-text-Regular-size); + --p-tag-font-size: var( --text-body-SM-detail-text-Maj-size); + --p-tag-font-weight: var( --text-body-SM-detail-text-Maj-weight); + --p-tag-padding: 0.25rem 0.5rem; + --p-tag-gap: 0.25rem; + --p-tag-border-radius: 0.25rem; + /* --p-tag-rounded-border-radius: + --p-tag-contrast-background: */ + --p-tag-contrast-color: var(--p-surface-0); + --p-tag-danger-background: var(--background-contrast-error); + --p-tag-danger-color: var(--text-default-error); + --p-tag-warn-background: var(--background-contrast-warning); + --p-tag-warn-color: var(--text-default-warning); + --p-tag-info-background: var(--background-contrast-info); + --p-tag-info-color: var(--text-default-info); + --p-tag-success-background: var(--background-contrast-success); + --p-tag-success-color: var(--text-default-success); + --p-tag-secondary-background: var(--illustration-color-950-tournesol-default); + --p-tag-secondary-color: var(--illustration-color-sun-tournesol-default); + --p-tag-primary-background: var(--illustration-color-950-glycine-default); + --p-tag-primary-color: var(--illustration-color-sun-glycine-default); +} diff --git a/src/components/badge/IVBadge.type.ts b/src/components/badge/IVBadge.type.ts new file mode 100644 index 0000000..b97777e --- /dev/null +++ b/src/components/badge/IVBadge.type.ts @@ -0,0 +1,38 @@ +/** + * Interface representing the properties of a Badge component. + */ +export default interface IVBadge { + /** + * The text label displayed inside the badge. + */ + label: string; + + /** + * The type of badge, which determines its color and icon. + * - `success`: Indicates a positive or successful status. + * - `error`: Indicates an error or negative status. + * - `new`: Highlights something new or recently added. + * - `info`: Provides informational context. + * - `warning`: Warns the user about a potential issue. + * - **undefined**: Defaults to a neutral badge style. + */ + type?: 'success' | 'error' | 'new' | 'info' | 'warning' | undefined; + + /** + * If true, the badge will be displayed without an icon. + * Defaults to false. + */ + noIcon?: boolean; + + /** + * If true, the badge will be rendered in a smaller size. + * Useful for compact UI elements. + */ + small?: boolean; + + /** + * Optional maximum width for the badge. + * Accepts any valid CSS width value (e.g., '100px', '10rem', '50%'). + */ + maxWidth?: string; +} diff --git a/src/components/badge/VBadge.vue b/src/components/badge/VBadge.vue new file mode 100644 index 0000000..e474e5d --- /dev/null +++ b/src/components/badge/VBadge.vue @@ -0,0 +1,71 @@ + + + + + + diff --git a/src/components/progressbar/IVProgressBar.type.ts b/src/components/progressbar/IVProgressBar.type.ts new file mode 100644 index 0000000..67afc37 --- /dev/null +++ b/src/components/progressbar/IVProgressBar.type.ts @@ -0,0 +1,30 @@ +/** + * Interface representing the properties of a ProgressBar component. + */ +export default interface IVProgressBar { + /** + * Whether to display the numeric value of the progress. + * If true, the value will be shown alongside the progress bar. + * @default false + */ + showValue?: boolean; + + /** + * The current progress value, typically between 0 and 100. + */ + value: number; + + /** + * If true, the progress bar will be in indeterminate mode, + * indicating ongoing activity without a specific completion percentage. + * @default false + */ + indeterminate?: boolean; + + /** + * If true, renders a smaller version of the progress bar. + * Useful for compact UI layouts. + * @default false + */ + small?: boolean; +} diff --git a/src/components/progressbar/VProgressBar.vue b/src/components/progressbar/VProgressBar.vue new file mode 100644 index 0000000..efce040 --- /dev/null +++ b/src/components/progressbar/VProgressBar.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/components/select/IVSelect.type.ts b/src/components/select/IVSelect.type.ts new file mode 100644 index 0000000..3be1034 --- /dev/null +++ b/src/components/select/IVSelect.type.ts @@ -0,0 +1,351 @@ +import type { HintedString } from '@primevue/core'; +import type { VirtualScrollerProps } from 'primevue/virtualscroller'; + +/** + * Represents the props for a custom VSelect component. + */ +export type option = string | number | null | undefined; + +export interface ISelect { + /** + * Whether the select field is required. + */ + required?: boolean; + + /** + * Whether the select field is disabled. + */ + disabled?: boolean; + + /** + * The unique ID for the select element. + */ + selectId?: string; + + /** + * The name attribute for the select element. + */ + name?: string; + + /** + * Optional hint text displayed below the select field. + */ + hint?: string; + + /** + * The currently selected value. + */ + modelValue?: option; + + /** + * The label displayed above the select field. + */ + label?: string; + + /** + * The list of options to display in the dropdown. + * Can be an array of strings, numbers, or objects. + */ + options?: Array< + string | number | Record | { + value: unknown; + text: string; + disabled: boolean; + } + >; + + /** + * Defines how to extract the label from an option. + * Can be a string key or a function. + */ + optionLabel?: string | ((option: unknown) => string); + + /** + * Defines how to extract the value from an option. + * Can be a string key or a function. + */ + optionValue?: string | ((option: unknown) => unknown); + + /** + * Message displayed when the selection is valid. + */ + successMessage?: string; + + /** + * Message displayed when the selection is invalid. + */ + errorMessage?: string; + + /** + * Text displayed when no option is selected. + */ + defaultUnselectedText?: string; +} + +export interface SelectProps { + /** + * Value of the component. + */ + modelValue?: unknown; + /** + * The default value for the input when not controlled by `modelValue`. + */ + defaultValue?: unknown; + /** + * The name attribute for the element, typically used in form submissions. + */ + name?: string | undefined; + /** + * An array of select items to display as the available options. + */ + options?: unknown[]; + /** + * Property name or getter function to use as the label of an option. + */ + optionLabel?: string | ((data: unknown) => string) | undefined; + /** + * Property name or getter function to use as the value of an option, defaults to the option itself when not defined. + */ + optionValue?: string | ((data: unknown) => unknown) | undefined; + /** + * Property name or getter function to use as the disabled flag of an option, defaults to false when not defined. + */ + optionDisabled?: string | ((data: unknown) => boolean) | undefined; + /** + * Property name or getter function to use as the label of an option group. + */ + optionGroupLabel?: string | ((data: unknown) => string) | undefined; + /** + * Property name or getter function that refers to the children options of option group. + */ + optionGroupChildren?: string | ((data: unknown) => unknown[]) | undefined; + /** + * Height of the viewport, a scrollbar is defined if height of list exceeds this value. + * @defaultValue 14rem + */ + scrollHeight?: string | undefined; + /** + * When specified, displays a filter input at header. + * @defaultValue false + */ + filter?: boolean | undefined; + /** + * Placeholder text to show when filter input is empty. + */ + filterPlaceholder?: string | undefined; + /** + * Locale to use in filtering. The default locale is the host environment's current locale. + */ + filterLocale?: string | undefined; + /** + * Defines the filtering algorithm to use when searching the options. + * @defaultValue contains + */ + filterMatchMode?: HintedString<'contains' | 'startsWith' | 'endsWith'> | undefined; + /** + * Fields used when filtering the options, defaults to optionLabel. + */ + filterFields?: string[] | undefined; + /** + * When present, custom value instead of predefined options can be entered using the editable input field. + * @defaultValue false + */ + editable?: boolean | undefined; + /** + * Default text to display when no option is selected. + */ + placeholder?: string | undefined; + /** + * Defines the size of the component. + */ + size?: HintedString<'small' | 'large'> | undefined; + /** + * When present, it specifies that the component should have invalid state style. + * @defaultValue false + */ + invalid?: boolean | undefined; + /** + * When present, it specifies that the component should be disabled. + * @defaultValue false + */ + disabled?: boolean | undefined; + /** + * Specifies the input variant of the component. + * @defaultValue null + */ + variant?: HintedString<'outlined' | 'filled'> | undefined | null; + /** + * A property to uniquely identify an option. + */ + dataKey?: string | undefined; + /** + * When enabled, a clear icon is displayed to clear the value. + * @defaultValue false + */ + showClear?: boolean | undefined; + /** + * Spans 100% width of the container when enabled. + * @defaultValue null + */ + fluid?: boolean | undefined; + /** + * @deprecated since v4.0. Use 'labelId' instead. + * Identifier of the underlying input element. + */ + inputId?: string | undefined; + /** + * @deprecated since v4.0. Use 'labelStyle' instead. + * Inline style of the input field. + */ + inputStyle?: object | undefined; + /** + * @deprecated since v4.0. Use 'labelClass' instead. + * Style class of the input field. + */ + inputClass?: string | object | undefined; + /** + * Identifier of the underlying label element. + */ + labelId?: string | undefined; + /** + * Inline style of the label field. + */ + labelStyle?: object | undefined; + /** + * Style class of the label field. + */ + labelClass?: string | object | undefined; + /** + * @deprecated since v4.0. Use 'overlayStyle' instead. + * Inline style of the overlay panel. + */ + panelStyle?: object | undefined; + /** + * @deprecated since v4.0. Use 'overlayClass' instead. + * Style class of the overlay panel. + */ + panelClass?: string | object | undefined; + /** + * Inline style of the overlay. + */ + overlayStyle?: object | undefined; + /** + * Style class of the overlay. + */ + overlayClass?: string | object | undefined; + /** + * A valid query selector or an HTMLElement to specify where the overlay gets attached. + * @defaultValue body + */ + appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement; + /** + * Whether the select is in loading state. + * @defaultValue false + */ + loading?: boolean | undefined; + /** + * Icon to display in clear button. + */ + clearIcon?: string | undefined; + /** + * Icon to display in the select. + */ + dropdownIcon?: string | undefined; + /** + * Icon to display in filter input. + */ + filterIcon?: string | undefined; + /** + * Icon to display in loading state. + */ + loadingIcon?: string | undefined; + /** + * Clears the filter value when hiding the select. + * @defaultValue false + */ + resetFilterOnHide?: boolean; + /** + * Clears the filter value when clicking on the clear icon. + * @defaultValue false + */ + resetFilterOnClear?: boolean; + /** + * Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it. + */ + virtualScrollerOptions?: VirtualScrollerProps; + /** + * Whether to focus on the first visible or selected element when the overlay panel is shown. + * @defaultValue false + */ + autoOptionFocus?: boolean | undefined; + /** + * Whether to focus on the filter element when the overlay panel is shown. + * @defaultValue false + */ + autoFilterFocus?: boolean | undefined; + /** + * When enabled, the focused option is selected. + * @defaultValue false + */ + selectOnFocus?: boolean | undefined; + /** + * When enabled, the focus is placed on the hovered option. + * @defaultValue true + */ + focusOnHover?: boolean | undefined; + /** + * Whether the selected option will be add highlight class. + * @defaultValue true + */ + highlightOnSelect?: boolean | undefined; + /** + * Whether the selected option will be shown with a check mark. + * @defaultValue false + */ + checkmark?: boolean | undefined; + /** + * Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration. + * @defaultValue '{0} results are available' + */ + filterMessage?: string | undefined; + /** + * Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration. + * @defaultValue '{0} items selected' + */ + selectionMessage?: string | undefined; + /** + * Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration. + * @defaultValue No selected item + */ + emptySelectionMessage?: string | undefined; + /** + * Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration. + * @defaultValue No results found + */ + emptyFilterMessage?: string | undefined; + /** + * Text to display when there are no options available. Defaults to value from PrimeVue locale configuration. + * @defaultValue No available options + */ + emptyMessage?: string | undefined; + /** + * Index of the element in tabbing order. + */ + tabindex?: number | string | undefined; + /** + * Defines a string value that labels an interactive element. + */ + ariaLabel?: string | undefined; + /** + * Identifier of the underlying input element. + */ + ariaLabelledby?: string | undefined; + /** + * Form control object, typically used for handling validation and form state. + */ + formControl?: Record | undefined; +} + +export default interface IVSelect extends Partial>, Partial { + optionTemplate?: boolean +} diff --git a/src/components/select/VSelect.vue b/src/components/select/VSelect.vue new file mode 100644 index 0000000..4bad988 --- /dev/null +++ b/src/components/select/VSelect.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/test/VProgressBar.spec.ts b/test/VProgressBar.spec.ts new file mode 100644 index 0000000..3d87af4 --- /dev/null +++ b/test/VProgressBar.spec.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import VProgressBar from '../src/components/progressbar/VProgressBar.vue'; + +describe('VProgressBar', () => { + it('renders with default props', () => { + const wrapper = mount(VProgressBar); + const progressBar = wrapper.findComponent({ name: 'ProgressBar' }); + + expect(progressBar.exists()).toBe(true); + expect(progressBar.props('value')).toBe(0); + expect(progressBar.props('mode')).toBe('determinate'); + expect(progressBar.props('showValue')).toBe(false); + }); + + it('renders with custom value', () => { + const wrapper = mount(VProgressBar, { + props: { value: 75 } + }); + + expect(wrapper.findComponent({ name: 'ProgressBar' }).props('value')).toBe(75); + }); + + it('renders in indeterminate mode', () => { + const wrapper = mount(VProgressBar, { + props: { indeterminate: true, value: 75 } + }); + + expect(wrapper.findComponent({ name: 'ProgressBar' }).props('mode')).toBe('indeterminate'); + }); + + it('hides value when small is true even if showValue is true', () => { + const wrapper = mount(VProgressBar, { + props: { showValue: true, small: true, value: 25 } + }); + + expect(wrapper.findComponent({ name: 'ProgressBar' }).props('showValue')).toBe(false); + }); + + it('applies small class when small is true', () => { + const wrapper = mount(VProgressBar, { + props: { small: true, value: 15 } + }); + + expect(wrapper.find('.p-progressbar').classes()).toContain('small'); + }); + + it('renders slot content', () => { + const wrapper = mount(VProgressBar, { + props: { value: 43, showValue: true }, + slots: { + default: 'Valeur: 43/100' + } + }); + expect(wrapper.html()).toContain('Valeur: 43/100'); + }); +}); diff --git a/test/VSelect.spec.ts b/test/VSelect.spec.ts new file mode 100644 index 0000000..3e0c8ca --- /dev/null +++ b/test/VSelect.spec.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import VSelect from '../src/components/select/VSelect.vue'; +import Select from 'primevue/select'; + +// Mock global matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + + +const globalConfig = { + global: { + components: { + Checkbox: Select + }, + mocks: { + $primevue: { + config: {} + } + }, + stubs: { + VLabel: false, + VHint: true, + } + } +} + +describe('VSelect.vue', () => { + const options = [ + { value: 'apple', text: 'Apple' }, + { value: 'banana', text: 'Banana' }, + { value: 'cherry', text: 'Cherry' } + ]; + + it('renders with basic props', () => { + const wrapper = mount(VSelect, { + props: { + label: 'Fruits', + options, + modelValue: null + }, + global: globalConfig.global + }); + + const label = wrapper.findComponent({ name: 'VLabel' }); + expect(label.exists()).toBe(true); + expect(label.props('label')).toBe('Fruits'); + + }); + + it('emits update:modelValue and change when value is selected', async () => { + const wrapper = mount(VSelect, { + props: { + options, + modelValue: null + }, + global: globalConfig.global + }); + + // Simule la sélection d'une option + wrapper.vm.$emit('update:modelValue', 'banana'); + wrapper.vm.$emit('change', { value: 'banana' }); + + const emittedUpdate = wrapper.emitted('update:modelValue'); + const emittedChange = wrapper.emitted('change'); + + expect(emittedUpdate).toBeTruthy(); + expect(emittedUpdate![0]).toEqual(['banana']); + + expect(emittedChange).toBeTruthy(); + expect(emittedChange![0]).toEqual([{ value: 'banana' }]); + }); + + it('reacts to external modelValue changes', async () => { + const wrapper = mount(VSelect, { + props: { + options, + modelValue: 'apple' + }, + global: globalConfig.global + }); + + await wrapper.setProps({ modelValue: 'cherry' }); + expect(wrapper.emitted('update:modelValue')).toBeFalsy(); + + const select = wrapper.findComponent({ name: 'Select' }); + expect(select.props('modelValue')).toBe('cherry'); + + }); +});