@nexxtmove/ui 2.6.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.
Files changed (229) hide show
  1. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +7 -5
  2. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
  3. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
  4. package/dist/index.js +95 -172
  5. package/dist/nuxt.js +0 -1
  6. package/package.json +38 -36
  7. package/src/attribute-contract.test.ts +151 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  9. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  10. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  11. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  12. package/src/components/atoms/Button/Button.stories.ts +290 -0
  13. package/src/components/atoms/Button/Button.test.ts +406 -0
  14. package/src/components/atoms/Card/Card.stories.ts +45 -0
  15. package/src/components/atoms/Card/Card.test.ts +46 -0
  16. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  17. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  18. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  19. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  21. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  23. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  24. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  25. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  26. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  27. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  28. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  29. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  31. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  32. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  34. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  36. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  37. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  38. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  39. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  40. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  41. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  42. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  43. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  44. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  45. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  46. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  48. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  50. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  52. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  53. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  55. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  57. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  59. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  61. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  63. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +633 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +776 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
  69. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +832 -0
  70. package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
  71. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  72. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  73. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  74. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  75. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  76. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  77. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  78. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  79. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  80. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  81. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  82. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  83. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  84. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  85. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  86. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  87. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  88. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  89. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  90. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  91. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  92. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  93. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  94. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  95. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  96. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  97. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  98. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  99. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  100. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  101. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  102. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  103. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  104. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  105. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  106. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  107. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  108. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  109. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  110. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  111. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  112. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  113. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  114. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  115. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  116. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  117. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  118. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  119. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  120. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  121. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  122. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  123. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  124. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  125. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  126. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  127. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  128. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  129. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  130. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  131. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  132. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  133. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  134. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  135. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  136. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  137. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  138. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  139. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  140. package/src/components/molecules/Select/Select.stories.ts +264 -0
  141. package/src/components/molecules/Select/Select.test.ts +315 -0
  142. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  143. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  144. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  145. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  146. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  147. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  148. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  149. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  150. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  151. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  152. package/src/components/molecules/Source/Source.stories.ts +35 -0
  153. package/src/components/molecules/Source/Source.test.ts +45 -0
  154. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  155. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  156. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  157. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  158. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  159. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  160. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  161. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  162. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  163. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  164. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  165. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  166. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  167. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  168. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  169. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  170. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  171. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  172. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  173. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  174. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  175. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  176. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  177. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  178. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  179. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  180. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  181. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +696 -0
  182. package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
  183. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  184. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  185. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  186. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  187. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  190. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  191. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  192. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  193. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  194. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  196. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  197. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  198. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  199. package/src/components/organisms/Header/Header.stories.ts +75 -0
  200. package/src/components/organisms/Header/Header.test.ts +233 -0
  201. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  202. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  203. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  204. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  205. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  206. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  207. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  209. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  211. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  213. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  215. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  217. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  218. package/src/components/organisms/Table/Table.stories.ts +332 -0
  219. package/src/components/organisms/Table/Table.test.ts +320 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  221. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  223. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  224. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  225. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  226. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  227. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  229. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { mount, type VueWrapper } from '@vue/test-utils'
3
+ import EditorFlexDirection, { EDITOR_FLEX_DIRECTION_OPTIONS } from './EditorFlexDirection.vue'
4
+
5
+ // ToggleGroup awaits document.fonts.ready on mount, which jsdom does not ship.
6
+ beforeEach(() => {
7
+ Object.defineProperty(document, 'fonts', {
8
+ value: { ready: Promise.resolve() },
9
+ configurable: true,
10
+ })
11
+ })
12
+
13
+ /** Option labels, read from the interactive layer rather than the overlay copy. */
14
+ const optionLabels = (wrapper: VueWrapper) =>
15
+ wrapper.findAll('button').map((button) => button.text())
16
+
17
+ describe('EditorFlexDirection', () => {
18
+ // The options sit in a ToggleGroup toolbar; its name is the block label, so
19
+ // a screen reader announces which setting the pressed option belongs to.
20
+ it('names the options toolbar with the block label', async () => {
21
+ const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
22
+ expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Richting')
23
+
24
+ await wrapper.setProps({ label: 'Custom' })
25
+ expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Custom')
26
+ })
27
+
28
+ // No assertions on typography utilities: labels are found by text or by
29
+ // structure, so restyling the block or the toggle group cannot turn these red.
30
+ it('renders the Dutch default label', () => {
31
+ const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
32
+
33
+ expect(wrapper.find('p').text()).toBe('Richting')
34
+ })
35
+
36
+ it('renders the Dutch default option labels', () => {
37
+ const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
38
+
39
+ expect(optionLabels(wrapper)).toEqual(['Horizontaal', 'Verticaal'])
40
+ })
41
+
42
+ it('renders a custom label', () => {
43
+ const wrapper = mount(EditorFlexDirection, {
44
+ props: { modelValue: 'flex-row', label: 'Direction' },
45
+ })
46
+
47
+ expect(wrapper.find('p').text()).toBe('Direction')
48
+ })
49
+
50
+ it('renders custom option labels', () => {
51
+ const wrapper = mount(EditorFlexDirection, {
52
+ props: {
53
+ modelValue: 'flex-row',
54
+ messages: { horizontal: 'Horizontal', vertical: 'Vertical' },
55
+ },
56
+ })
57
+
58
+ expect(optionLabels(wrapper)).toEqual(['Horizontal', 'Vertical'])
59
+ })
60
+
61
+ it('keeps the untouched default when only one message is overridden', () => {
62
+ const wrapper = mount(EditorFlexDirection, {
63
+ props: { modelValue: 'flex-row', messages: { vertical: 'Vertical' } },
64
+ })
65
+
66
+ expect(optionLabels(wrapper)).toEqual(['Horizontaal', 'Vertical'])
67
+ })
68
+
69
+ it('emits update:modelValue with the Tailwind class of the clicked option', async () => {
70
+ const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
71
+
72
+ await wrapper.findAll('button')[1].trigger('click')
73
+
74
+ expect(wrapper.emitted('update:modelValue')).toEqual([['flex-col']])
75
+ })
76
+
77
+ it('renders an icon for each option', () => {
78
+ const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
79
+
80
+ // Each icon is rendered twice: once per toggle group layer.
81
+ expect(wrapper.findAll('i')).toHaveLength(4)
82
+ })
83
+
84
+ it('is always visible, so it renders no switch', () => {
85
+ const wrapper = mount(EditorFlexDirection, { props: { modelValue: 'flex-row' } })
86
+
87
+ expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
88
+ })
89
+ })
90
+
91
+ // The consumer derives its class matchers from this list, so the list is a
92
+ // contract: any change here changes what the app recognises.
93
+ describe('EDITOR_FLEX_DIRECTION_OPTIONS', () => {
94
+ it('offers exactly the agreed options', () => {
95
+ expect(EDITOR_FLEX_DIRECTION_OPTIONS).toEqual([{ token: 'row' }, { token: 'col' }])
96
+ })
97
+ })
@@ -0,0 +1,248 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, fn, userEvent, within } from 'storybook/test'
3
+ import { computed, ref } from 'vue'
4
+ import EditorFontSize, { EDITOR_FONT_SIZE_STEPS } from './EditorFontSize.vue'
5
+ import type { EditorFontSizeValue } from './EditorFontSize.vue'
6
+
7
+ const NBSP = ' '
8
+
9
+ const TOKENS = EDITOR_FONT_SIZE_STEPS.map(({ token }) => token)
10
+
11
+ export default {
12
+ title: 'Components/Editor Components/Molecules/Editor Font Size',
13
+ component: EditorFontSize,
14
+ // 312px: the content width of the editor's settings panel
15
+ decorators: [() => ({ template: '<div class="mx-auto w-78"><story /></div>' })],
16
+ args: {
17
+ onPointerup: fn(),
18
+ onReset: fn(),
19
+ },
20
+ argTypes: {
21
+ label: { control: 'text' },
22
+ // moves the slider while the model is null, and is the sample's size then
23
+ defaultToken: { control: 'select', options: TOKENS },
24
+ },
25
+ } satisfies Meta<typeof EditorFontSize>
26
+
27
+ type Story = StoryObj<typeof EditorFontSize>
28
+
29
+ /**
30
+ * The control with a sample paragraph under it. The sample carries the class the app would put
31
+ * on the element: the model, or the default size while the model is `null`. It is the real
32
+ * class, so the sample only changes size when that class compiles.
33
+ */
34
+ const renderSize = (initial: EditorFontSizeValue | null) => (args: Story['args']) => ({
35
+ components: { EditorFontSize },
36
+ setup() {
37
+ const size = ref<EditorFontSizeValue | null>(initial)
38
+ const sampleClass = computed(() => size.value ?? args?.defaultToken ?? 'text-sm')
39
+ return { args, size, sampleClass }
40
+ },
41
+ template: `
42
+ <div>
43
+ <EditorFontSize v-bind="args" v-model="size" />
44
+ <p class="extra-small-normal text-gray-700">
45
+ model: <code data-testid="model">{{ size ?? 'null' }}</code>
46
+ </p>
47
+ <!-- \`wrap-break-word\`: at 9xl a single word is wider than the 312px column -->
48
+ <p data-testid="sample" class="mt-4 text-gray-900 wrap-break-word" :class="sampleClass">
49
+ Een licht appartement op loopafstand van het centrum, met een ruim balkon op het zuiden.
50
+ </p>
51
+ </div>
52
+ `,
53
+ })
54
+
55
+ const thumbOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('slider')
56
+
57
+ /** The font size the sample renders at, in px, as Chromium computes it. */
58
+ const sampleSize = (canvasElement: HTMLElement) =>
59
+ parseFloat(getComputedStyle(within(canvasElement).getByTestId('sample')).fontSize)
60
+
61
+ /**
62
+ * No size of its own: the slider sits on the default step, `text-sm`. The ends of the list are
63
+ * 12 and 128px; every step writes its class, the default one too.
64
+ */
65
+ export const Default: Story = {
66
+ render: renderSize(null),
67
+ play: async ({ canvasElement }) => {
68
+ const canvas = within(canvasElement)
69
+ const thumb = thumbOf(canvasElement)
70
+
71
+ await expect(thumb).toHaveAccessibleName('Grootte')
72
+ await expect(thumb).toHaveAttribute('aria-valuetext', `14${NBSP}px`)
73
+ await expect(sampleSize(canvasElement)).toBe(14)
74
+ // invisible, so out of the accessibility tree and its name: found by its label attribute
75
+ const reset = canvasElement.querySelector<HTMLElement>(
76
+ 'button[aria-label="Terug naar standaard"]',
77
+ )!
78
+ await expect(reset).not.toBeVisible()
79
+
80
+ thumb.focus()
81
+ await userEvent.keyboard('{End}')
82
+ await expect(thumb).toHaveAttribute('aria-valuetext', `128${NBSP}px`)
83
+ await expect(canvas.getByTestId('model')).toHaveTextContent('text-9xl')
84
+ await expect(sampleSize(canvasElement)).toBe(128)
85
+
86
+ // the sample wraps inside the column instead of running out of it
87
+ const sample = canvas.getByTestId('sample')
88
+ const column = canvasElement.querySelector('.w-78')!.getBoundingClientRect()
89
+ await expect(sample.scrollWidth).toBeLessThanOrEqual(sample.clientWidth)
90
+ await expect(sample.getBoundingClientRect().right).toBeLessThanOrEqual(column.right)
91
+
92
+ await userEvent.keyboard('{Home}')
93
+ await expect(sampleSize(canvasElement)).toBe(12)
94
+
95
+ // back onto the default step by keyboard: an explicit class, not null
96
+ await userEvent.keyboard('{ArrowRight}')
97
+ await expect(canvas.getByTestId('model')).toHaveTextContent('text-sm')
98
+ await expect(reset).toBeVisible()
99
+ },
100
+ }
101
+
102
+ /** A default other than `text-sm`: the slider and the sample follow it while the model is null. */
103
+ export const OtherDefault: Story = {
104
+ args: { defaultToken: 'text-base' },
105
+ render: renderSize(null),
106
+ play: async ({ canvasElement }) => {
107
+ await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', `16${NBSP}px`)
108
+ await expect(sampleSize(canvasElement)).toBe(16)
109
+ },
110
+ }
111
+
112
+ /**
113
+ * The reset button is invisible while the model is null, and so out of the tab order, while it
114
+ * keeps its place. Using it sets the model to `null`, emits `reset`, leaves focus on the thumb, and
115
+ * the sample falls back to the default size.
116
+ */
117
+ export const Reset: Story = {
118
+ render: renderSize('text-xl'),
119
+ play: async ({ canvasElement, args }) => {
120
+ const canvas = within(canvasElement)
121
+ const reset = canvas.getByRole('button', { name: 'Terug naar standaard' })
122
+ const thumb = thumbOf(canvasElement)
123
+
124
+ await expect(sampleSize(canvasElement)).toBe(20)
125
+ await expect(reset).toBeVisible()
126
+ const trackWidth = thumb.parentElement!.getBoundingClientRect().width
127
+
128
+ await userEvent.click(reset)
129
+ await expect(args.onReset).toHaveBeenCalledOnce()
130
+ await expect(canvas.getByTestId('model')).toHaveTextContent('null')
131
+ await expect(thumb).toHaveFocus()
132
+ await expect(sampleSize(canvasElement)).toBe(14)
133
+
134
+ // `visibility: hidden` keeps its room, so the slider keeps its width, and takes it out of the
135
+ // tab order
136
+ await expect(getComputedStyle(reset).visibility).toBe('hidden')
137
+ await expect(thumb.parentElement!.getBoundingClientRect().width).toBe(trackWidth)
138
+ },
139
+ }
140
+
141
+ /**
142
+ * One drag is one undo step: the app merges the changes while dragging and closes the step on
143
+ * `pointerup`. A reset always gets a step of its own. The log shows what the app would see.
144
+ */
145
+ export const UndoEvents: Story = {
146
+ render: (args) => ({
147
+ components: { EditorFontSize },
148
+ setup() {
149
+ const size = ref<EditorFontSizeValue | null>(null)
150
+ const log = ref<string[]>([])
151
+ const onUpdate = (value: EditorFontSizeValue | null) => {
152
+ size.value = value
153
+ log.value.push(`change ${value ?? 'null'}`)
154
+ }
155
+ const onPointerup = () => log.value.push('pointerup: end of step')
156
+ const onReset = () => log.value.push('reset: own step')
157
+ return { args, size, log, onUpdate, onPointerup, onReset }
158
+ },
159
+ template: `
160
+ <div>
161
+ <EditorFontSize
162
+ v-bind="args"
163
+ :model-value="size"
164
+ @update:model-value="onUpdate"
165
+ @pointerup="onPointerup"
166
+ @reset="onReset"
167
+ />
168
+ <ol data-testid="log" class="extra-small-normal text-gray-700">
169
+ <li v-for="(entry, index) in log" :key="index">{{ entry }}</li>
170
+ </ol>
171
+ </div>
172
+ `,
173
+ }),
174
+ // The drag itself is a real pointer gesture and is left to the unit test (pointerup) and to
175
+ // trying it here by hand; the play covers the keyboard step and the reset
176
+ play: async ({ canvasElement }) => {
177
+ const canvas = within(canvasElement)
178
+ const entries = () =>
179
+ Array.from(canvas.getByTestId('log').querySelectorAll('li')).map((li) => li.textContent)
180
+
181
+ thumbOf(canvasElement).focus()
182
+ await userEvent.keyboard('{ArrowRight}')
183
+ await expect(entries()).toEqual(['change text-base'])
184
+
185
+ await userEvent.click(canvas.getByRole('button', { name: 'Terug naar standaard' }))
186
+ await expect(entries()).toEqual(['change text-base', 'change null', 'reset: own step'])
187
+ },
188
+ }
189
+
190
+ /**
191
+ * The readout is as wide at "14 px" as at "128 px", so the slider beside it keeps its width over
192
+ * the whole list.
193
+ */
194
+ export const ReadoutWidth: Story = {
195
+ render: renderSize(null),
196
+ play: async ({ canvasElement }) => {
197
+ const thumb = thumbOf(canvasElement)
198
+ const readout = within(canvasElement).getByText(`14 px`)
199
+ // the slider root, which the thumb is placed in
200
+ const slider = thumb.parentElement!
201
+ const textWidth = () => {
202
+ const range = document.createRange()
203
+ range.selectNodeContents(readout)
204
+ return range.getBoundingClientRect().width
205
+ }
206
+ const measure = () => ({
207
+ readout: readout.getBoundingClientRect().width,
208
+ text: textWidth(),
209
+ slider: slider.getBoundingClientRect().width,
210
+ })
211
+
212
+ const narrow = measure()
213
+ thumb.focus()
214
+ await userEvent.keyboard('{End}')
215
+ await expect(readout).toHaveTextContent('128 px')
216
+ const wide = measure()
217
+
218
+ // the widest label still fits inside the minimum width, so nothing moved
219
+ // `min-w-14`: 56px, with at least 8px to spare around the widest label
220
+ await expect(wide.readout).toBe(56)
221
+ await expect(wide.readout - wide.text).toBeGreaterThanOrEqual(8)
222
+ await expect(wide.readout).toBe(narrow.readout)
223
+ await expect(wide.slider).toBe(narrow.slider)
224
+ },
225
+ }
226
+
227
+ /** Every step, one control each, so the whole readout range is in view. */
228
+ export const AllSteps: Story = {
229
+ render: () => ({
230
+ components: { EditorFontSize },
231
+ setup() {
232
+ return { tokens: TOKENS }
233
+ },
234
+ template: `
235
+ <div>
236
+ <EditorFontSize v-for="token in tokens" :key="token" :label="token" :model-value="token" />
237
+ </div>
238
+ `,
239
+ }),
240
+ }
241
+
242
+ export const CustomMessages: Story = {
243
+ args: {
244
+ label: 'Size',
245
+ messages: { unit: 'pt', reset: 'Back to default' },
246
+ },
247
+ render: renderSize('text-lg'),
248
+ }
@@ -0,0 +1,288 @@
1
+ import { afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { mount, type VueWrapper } from '@vue/test-utils'
3
+ import EditorFontSize, { EDITOR_FONT_SIZE_STEPS } from './EditorFontSize.vue'
4
+ import type { EditorFontSizeValue } from './EditorFontSize.vue'
5
+ import Slider from '../../../atoms/Slider/Slider.vue'
6
+
7
+ // jsdom ships no ResizeObserver, and reka-ui's slider observes its track to place the thumbs
8
+ vi.stubGlobal(
9
+ 'ResizeObserver',
10
+ class {
11
+ observe() {}
12
+ unobserve() {}
13
+ disconnect() {}
14
+ },
15
+ )
16
+
17
+ // jsdom implements no pointer capture either, and reka-ui captures the pointer on thumb pointerdown
18
+ Element.prototype.setPointerCapture ??= () => {}
19
+ Element.prototype.releasePointerCapture ??= () => {}
20
+ Element.prototype.hasPointerCapture ??= () => false
21
+
22
+ const NBSP = ' '
23
+
24
+ const mounted: VueWrapper[] = []
25
+
26
+ const mountSize = (props: Record<string, unknown> = {}) => {
27
+ const wrapper = mount(EditorFontSize, {
28
+ props: { modelValue: null, ...props },
29
+ attachTo: document.body,
30
+ })
31
+ mounted.push(wrapper)
32
+ return wrapper
33
+ }
34
+
35
+ /** The index the slider sits on, read from the model handed to the slider. */
36
+ const sliderIndex = (wrapper: VueWrapper) => wrapper.findComponent(Slider).props('modelValue')
37
+
38
+ const thumbOf = (wrapper: VueWrapper) => wrapper.find('[role="slider"]')
39
+ const resetOf = (wrapper: VueWrapper) => wrapper.find('button[type="button"]')
40
+
41
+ const indexOf = (token: EditorFontSizeValue) =>
42
+ EDITOR_FONT_SIZE_STEPS.findIndex((step) => step.token === token)
43
+
44
+ describe('EditorFontSize', () => {
45
+ afterEach(() => {
46
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount())
47
+ })
48
+
49
+ // No assertions on typography utilities: the label and the value are found by
50
+ // text or by structure, so restyling the block or the slider cannot turn
51
+ // these red.
52
+ it('renders the Dutch default label', () => {
53
+ expect(mountSize().find('p').text()).toBe('Grootte')
54
+ })
55
+
56
+ it('renders a custom label, also on the thumb', () => {
57
+ const wrapper = mountSize({ label: 'Lettergrootte' })
58
+
59
+ expect(wrapper.find('p').text()).toBe('Lettergrootte')
60
+ expect(thumbOf(wrapper).attributes('aria-label')).toBe('Lettergrootte')
61
+ })
62
+
63
+ // The consumer stores these classes and lists them in an `@source inline`
64
+ // rule, so the list is a contract: any change here is a breaking change.
65
+ it('offers exactly the agreed steps', () => {
66
+ expect(EDITOR_FONT_SIZE_STEPS).toEqual([
67
+ { token: 'text-xs', px: 12 },
68
+ { token: 'text-sm', px: 14 },
69
+ { token: 'text-base', px: 16 },
70
+ { token: 'text-lg', px: 18 },
71
+ { token: 'text-xl', px: 20 },
72
+ { token: 'text-2xl', px: 24 },
73
+ { token: 'text-3xl', px: 30 },
74
+ { token: 'text-4xl', px: 36 },
75
+ { token: 'text-5xl', px: 48 },
76
+ { token: 'text-6xl', px: 60 },
77
+ { token: 'text-7xl', px: 72 },
78
+ { token: 'text-8xl', px: 96 },
79
+ { token: 'text-9xl', px: 128 },
80
+ ])
81
+ })
82
+
83
+ it('spans the whole step list', () => {
84
+ const slider = mountSize().findComponent(Slider)
85
+
86
+ expect(slider.props('min')).toBe(0)
87
+ expect(slider.props('max')).toBe(12)
88
+ expect(slider.props('step')).toBe(1)
89
+ })
90
+
91
+ describe('default step', () => {
92
+ it('puts null on text-sm without a defaultToken', () => {
93
+ const wrapper = mountSize()
94
+
95
+ expect(sliderIndex(wrapper)).toEqual([indexOf('text-sm')])
96
+ expect(wrapper.text()).toContain(`14${NBSP}px`)
97
+ })
98
+
99
+ it('treats an unset model as null', () => {
100
+ const wrapper = mount(EditorFontSize)
101
+ mounted.push(wrapper)
102
+
103
+ expect(sliderIndex(wrapper)).toEqual([indexOf('text-sm')])
104
+ })
105
+
106
+ it('puts null on the step of defaultToken', () => {
107
+ const wrapper = mountSize({ defaultToken: 'text-lg' })
108
+
109
+ expect(sliderIndex(wrapper)).toEqual([indexOf('text-lg')])
110
+ expect(wrapper.text()).toContain(`18${NBSP}px`)
111
+ })
112
+
113
+ it('falls back to text-sm for a defaultToken that is not on the list', () => {
114
+ const wrapper = mountSize({ defaultToken: 'text-10xl' as unknown as EditorFontSizeValue })
115
+
116
+ expect(sliderIndex(wrapper)).toEqual([indexOf('text-sm')])
117
+ })
118
+
119
+ it.each([
120
+ ['an unknown value', 'text-10xl'],
121
+ ['a size from another scale', 'text-[15px]'],
122
+ ])('sits on the default step for %s', (_, value) => {
123
+ // The type rules these out, which is exactly why the runtime fallback is
124
+ // worth a test: a stored value gets past the types.
125
+ const wrapper = mountSize({
126
+ defaultToken: 'text-base',
127
+ modelValue: value as unknown as EditorFontSizeValue,
128
+ })
129
+
130
+ expect(sliderIndex(wrapper)).toEqual([indexOf('text-base')])
131
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
132
+ })
133
+ })
134
+
135
+ // The fallback is for faulty or old data; a development build says so, once per value
136
+ describe('development warnings', () => {
137
+ const spyWarn = () => vi.spyOn(console, 'warn').mockImplementation(() => undefined)
138
+
139
+ afterEach(() => {
140
+ vi.restoreAllMocks()
141
+ })
142
+
143
+ it('warns once for an unknown defaultToken and reads out text-sm', async () => {
144
+ const warn = spyWarn()
145
+ const wrapper = mountSize({ defaultToken: 'text-10xl' as unknown as EditorFontSizeValue })
146
+
147
+ expect(wrapper.text()).toContain(`14${NBSP}px`)
148
+ await wrapper.setProps({ label: 'Opnieuw renderen' })
149
+ expect(warn).toHaveBeenCalledOnce()
150
+ expect(warn.mock.calls[0][0]).toContain('text-10xl')
151
+ })
152
+
153
+ it('warns once for a model value off the list, reads out the default and keeps reset', async () => {
154
+ const warn = spyWarn()
155
+ const wrapper = mountSize({ modelValue: 'text-[15px]' as unknown as EditorFontSizeValue })
156
+
157
+ expect(wrapper.text()).toContain(`14${NBSP}px`)
158
+ expect(resetOf(wrapper).classes()).not.toContain('invisible')
159
+
160
+ await wrapper.setProps({ label: 'Opnieuw renderen' })
161
+ await wrapper.setProps({ modelValue: 'text-[15px]' as unknown as EditorFontSizeValue })
162
+ expect(warn).toHaveBeenCalledOnce()
163
+ expect(warn.mock.calls[0][0]).toContain('text-[15px]')
164
+ })
165
+
166
+ it('warns again for another value off the list', async () => {
167
+ const warn = spyWarn()
168
+ const wrapper = mountSize({ modelValue: 'text-[15px]' as unknown as EditorFontSizeValue })
169
+
170
+ await wrapper.setProps({ modelValue: 'text-[16px]' as unknown as EditorFontSizeValue })
171
+
172
+ expect(warn).toHaveBeenCalledTimes(2)
173
+ expect(warn.mock.calls[1][0]).toContain('text-[16px]')
174
+ })
175
+
176
+ it.each([[{ modelValue: null }], [{ modelValue: 'text-xl', defaultToken: 'text-base' }]])(
177
+ 'stays silent for valid values: %o',
178
+ (props) => {
179
+ const warn = spyWarn()
180
+
181
+ mountSize(props)
182
+
183
+ expect(warn).not.toHaveBeenCalled()
184
+ },
185
+ )
186
+ })
187
+
188
+ it.each(EDITOR_FONT_SIZE_STEPS.map((step) => [step]))(
189
+ 'puts the slider on %o and reads out its px',
190
+ (step) => {
191
+ const wrapper = mountSize({ modelValue: step.token })
192
+
193
+ expect(sliderIndex(wrapper)).toEqual([EDITOR_FONT_SIZE_STEPS.indexOf(step)])
194
+ expect(wrapper.text()).toContain(`${step.px}${NBSP}px`)
195
+ // aria-valuenow is the step index, which is meaningless read aloud
196
+ expect(thumbOf(wrapper).attributes('aria-valuetext')).toBe(`${step.px}${NBSP}px`)
197
+ },
198
+ )
199
+
200
+ it('renders custom copy for the unit and the reset button', () => {
201
+ const wrapper = mountSize({ modelValue: 'text-xl', messages: { unit: 'pt', reset: 'Reset' } })
202
+
203
+ expect(wrapper.text()).toContain(`20${NBSP}pt`)
204
+ expect(resetOf(wrapper).attributes('aria-label')).toBe('Reset')
205
+ })
206
+
207
+ // Only the reset button writes null: moving onto the default step is a choice of its own
208
+ it('emits the token of every step, the default step included', async () => {
209
+ const wrapper = mountSize({ modelValue: 'text-xl' })
210
+ const slider = wrapper.findComponent(Slider)
211
+
212
+ slider.vm.$emit('update:modelValue', [indexOf('text-sm')])
213
+ slider.vm.$emit('update:modelValue', [indexOf('text-xs')])
214
+ await wrapper.vm.$nextTick()
215
+
216
+ expect(wrapper.emitted('update:modelValue')).toEqual([['text-sm'], ['text-xs']])
217
+ })
218
+
219
+ it('passes the release of the slider on as pointerup', async () => {
220
+ const wrapper = mountSize({ modelValue: 'text-lg' })
221
+
222
+ await thumbOf(wrapper).trigger('pointerup')
223
+
224
+ expect(wrapper.emitted('pointerup')).toHaveLength(1)
225
+ })
226
+
227
+ describe('reset', () => {
228
+ // `invisible` is `visibility: hidden`, which takes the button out of the tab order
229
+ // while it keeps its width, so the slider does not move
230
+ it('is rendered but invisible while the model is null', () => {
231
+ const reset = resetOf(mountSize({ modelValue: null }))
232
+
233
+ expect(reset.exists()).toBe(true)
234
+ expect(reset.classes()).toContain('invisible')
235
+ expect(reset.attributes('aria-label')).toBe('Terug naar standaard')
236
+ })
237
+
238
+ it('is visible for any token, the default step included', () => {
239
+ expect(resetOf(mountSize({ modelValue: 'text-sm' })).classes()).not.toContain('invisible')
240
+ expect(resetOf(mountSize({ modelValue: 'text-xl' })).classes()).not.toContain('invisible')
241
+ })
242
+
243
+ it.each(['text-sm', 'text-xl'] as const)(
244
+ 'from %s: sets the model to null, then emits reset, then focuses the thumb',
245
+ async (token) => {
246
+ const order: string[] = []
247
+ const wrapper = mountSize({
248
+ modelValue: token,
249
+ 'onUpdate:modelValue': (value: unknown) => order.push(`model:${value}`),
250
+ onReset: () => order.push('reset'),
251
+ })
252
+ // reka shows the thumb (display: none until then) in the render after mount; a user
253
+ // cannot click before it, and jsdom 30 refuses focus to an element that is not rendered.
254
+ await wrapper.vm.$nextTick()
255
+
256
+ await resetOf(wrapper).trigger('click')
257
+
258
+ expect(order).toEqual(['model:null', 'reset'])
259
+ expect(document.activeElement).toBe(thumbOf(wrapper).element)
260
+ },
261
+ )
262
+ })
263
+
264
+ describe('keyboard', () => {
265
+ const press = async (wrapper: VueWrapper, key: string) => {
266
+ await thumbOf(wrapper).trigger('keydown', { key })
267
+ }
268
+
269
+ it('writes text-base on ArrowRight from null', async () => {
270
+ const wrapper = mountSize({ modelValue: null })
271
+
272
+ await press(wrapper, 'ArrowRight')
273
+
274
+ expect(wrapper.emitted('update:modelValue')).toEqual([['text-base']])
275
+ })
276
+
277
+ it.each([
278
+ ['Home', 'text-xs'],
279
+ ['End', 'text-9xl'],
280
+ ])('writes the end of the list on %s', async (key, token) => {
281
+ const wrapper = mountSize({ modelValue: 'text-lg' })
282
+
283
+ await press(wrapper, key)
284
+
285
+ expect(wrapper.emitted('update:modelValue')).toEqual([[token]])
286
+ })
287
+ })
288
+ })