@nexxtmove/ui 2.5.0 → 2.7.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/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +12 -0
- package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
- package/dist/index.js +1094 -1135
- package/dist/nuxt.js +0 -1
- package/package.json +38 -36
- package/src/attribute-contract.test.ts +151 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
- package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
- package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
- package/src/components/atoms/Button/Button.stories.ts +290 -0
- package/src/components/atoms/Button/Button.test.ts +406 -0
- package/src/components/atoms/Card/Card.stories.ts +45 -0
- package/src/components/atoms/Card/Card.test.ts +46 -0
- package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/atoms/Chip/Chip.stories.ts +274 -0
- package/src/components/atoms/Chip/Chip.test.ts +230 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
- package/src/components/atoms/Icon/Icon.stories.ts +63 -0
- package/src/components/atoms/Icon/Icon.test.ts +89 -0
- package/src/components/atoms/Loader/Loader.stories.ts +36 -0
- package/src/components/atoms/Loader/Loader.test.ts +40 -0
- package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
- package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
- package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
- package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
- package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
- package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
- package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
- package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
- package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
- package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
- package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
- package/src/components/atoms/Slider/Slider.stories.ts +151 -0
- package/src/components/atoms/Slider/Slider.test.ts +191 -0
- package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
- package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
- package/src/components/atoms/Switch/Switch.stories.ts +91 -0
- package/src/components/atoms/Switch/Switch.test.ts +183 -0
- package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
- package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
- package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +58 -2
- package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +7 -29
- package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
- package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
- package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
- package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
- package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/molecules/InputField/InputField.stories.ts +117 -0
- package/src/components/molecules/InputField/InputField.test.ts +138 -0
- package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
- package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
- package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
- package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
- package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
- package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
- package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
- package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
- package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
- package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
- package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
- package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
- package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
- package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
- package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
- package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
- package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
- package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
- package/src/components/molecules/Select/Select.stories.ts +264 -0
- package/src/components/molecules/Select/Select.test.ts +315 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
- package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
- package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/molecules/Source/Source.stories.ts +35 -0
- package/src/components/molecules/Source/Source.test.ts +45 -0
- package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
- package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
- package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
- package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
- package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
- package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
- package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
- package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
- package/src/components/molecules/Toast/Toast.stories.ts +110 -0
- package/src/components/molecules/Toast/Toast.test.ts +103 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
- package/src/components/molecules/Tree/Tree.stories.ts +884 -0
- package/src/components/molecules/Tree/Tree.test.ts +1247 -0
- package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
- package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
- package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
- package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
- package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
- package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
- package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
- package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
- package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
- package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
- package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
- package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
- package/src/components/organisms/Combobox/Combobox.vue +1 -1
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
- package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
- package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
- package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
- package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
- package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
- package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
- package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
- package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
- package/src/components/organisms/Header/Header.stories.ts +75 -0
- package/src/components/organisms/Header/Header.test.ts +233 -0
- package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
- package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
- package/src/components/organisms/Modal/Modal.stories.ts +350 -0
- package/src/components/organisms/Modal/Modal.test.ts +275 -0
- package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
- package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
- package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
- package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
- package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
- package/src/components/organisms/Table/Table.stories.ts +332 -0
- package/src/components/organisms/Table/Table.test.ts +320 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
- package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
- package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
- package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
- package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent, h, nextTick, ref } from 'vue'
|
|
4
|
+
import VideoControls from './VideoControls.vue'
|
|
5
|
+
import type { NexxtVideoControlsProps } from './VideoControls.vue'
|
|
6
|
+
|
|
7
|
+
interface FakeVideoState {
|
|
8
|
+
duration?: number
|
|
9
|
+
currentTime?: number
|
|
10
|
+
paused?: boolean
|
|
11
|
+
volume?: number
|
|
12
|
+
muted?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A real <video> with its media state as plain, writable fields; play() and pause() flip
|
|
17
|
+
* `paused` and fire the media event, as a browser would. `seeks` records every currentTime
|
|
18
|
+
* write, so a test can count them.
|
|
19
|
+
*/
|
|
20
|
+
const fakeVideo = (state: FakeVideoState = {}) => {
|
|
21
|
+
const video = document.createElement('video')
|
|
22
|
+
const fields = {
|
|
23
|
+
duration: state.duration ?? 100,
|
|
24
|
+
currentTime: state.currentTime ?? 0,
|
|
25
|
+
paused: state.paused ?? true,
|
|
26
|
+
ended: false,
|
|
27
|
+
volume: state.volume ?? 1,
|
|
28
|
+
muted: state.muted ?? false,
|
|
29
|
+
}
|
|
30
|
+
const seeks: number[] = []
|
|
31
|
+
|
|
32
|
+
Object.defineProperties(video, {
|
|
33
|
+
duration: { get: () => fields.duration },
|
|
34
|
+
paused: { get: () => fields.paused },
|
|
35
|
+
ended: { get: () => fields.ended },
|
|
36
|
+
currentTime: {
|
|
37
|
+
get: () => fields.currentTime,
|
|
38
|
+
set: (value: number) => {
|
|
39
|
+
seeks.push(value)
|
|
40
|
+
fields.currentTime = value
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
volume: {
|
|
44
|
+
get: () => fields.volume,
|
|
45
|
+
set: (value: number) => {
|
|
46
|
+
fields.volume = value
|
|
47
|
+
video.dispatchEvent(new Event('volumechange'))
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
muted: {
|
|
51
|
+
get: () => fields.muted,
|
|
52
|
+
set: (value: boolean) => {
|
|
53
|
+
fields.muted = value
|
|
54
|
+
video.dispatchEvent(new Event('volumechange'))
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
buffered: { get: () => ({ length: 0, start: () => 0, end: () => 0 }) },
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const play = vi.fn(() => {
|
|
61
|
+
fields.paused = false
|
|
62
|
+
video.dispatchEvent(new Event('play'))
|
|
63
|
+
return Promise.resolve()
|
|
64
|
+
})
|
|
65
|
+
const pause = vi.fn(() => {
|
|
66
|
+
fields.paused = true
|
|
67
|
+
video.dispatchEvent(new Event('pause'))
|
|
68
|
+
})
|
|
69
|
+
video.play = play
|
|
70
|
+
video.pause = pause
|
|
71
|
+
|
|
72
|
+
return { video, fields, seeks, play, pause }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let host: HTMLDivElement
|
|
76
|
+
let wrapper: VueWrapper | undefined
|
|
77
|
+
|
|
78
|
+
/** The video and the controls side by side in one host, as a player lays them out. */
|
|
79
|
+
const mountControls = (
|
|
80
|
+
props: Partial<NexxtVideoControlsProps> = {},
|
|
81
|
+
state: FakeVideoState = {},
|
|
82
|
+
hostHeight?: number,
|
|
83
|
+
) => {
|
|
84
|
+
const fake = fakeVideo(state)
|
|
85
|
+
host = document.createElement('div')
|
|
86
|
+
// jsdom lays nothing out; a height stands in for the player's measured size
|
|
87
|
+
if (hostHeight !== undefined) Object.defineProperty(host, 'offsetHeight', { value: hostHeight })
|
|
88
|
+
const outlet = document.createElement('div')
|
|
89
|
+
host.append(fake.video, outlet)
|
|
90
|
+
document.body.append(host)
|
|
91
|
+
|
|
92
|
+
wrapper = mount(VideoControls, {
|
|
93
|
+
props: { target: fake.video, ...props },
|
|
94
|
+
attachTo: outlet,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return { ...fake, wrapper }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Keyboard focus, as Tab gives it: the element takes the focus and a key goes down in it. jsdom's
|
|
102
|
+
* `:focus-visible` does not tell keyboard from mouse, so the key is what says it here.
|
|
103
|
+
*/
|
|
104
|
+
const tabTo = (element: Element) => {
|
|
105
|
+
;(element as HTMLElement).focus()
|
|
106
|
+
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A mouse click: the press comes first, then the focus, then the click. */
|
|
110
|
+
const mouseClick = (element: Element) => {
|
|
111
|
+
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerType: 'mouse' }))
|
|
112
|
+
;(element as HTMLElement).focus()
|
|
113
|
+
;(element as HTMLElement).click()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const slider = (w: VueWrapper, name: string) => w.get(`[role="slider"][aria-label="${name}"]`)
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
vi.useRealTimers()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
wrapper?.unmount()
|
|
124
|
+
wrapper = undefined
|
|
125
|
+
host?.remove()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('VideoControls', () => {
|
|
129
|
+
describe('messages', () => {
|
|
130
|
+
it('defaults to Dutch copy', async () => {
|
|
131
|
+
const { wrapper } = mountControls({ name: 'Rondleiding' })
|
|
132
|
+
await nextTick()
|
|
133
|
+
|
|
134
|
+
expect(wrapper.get('[role="group"]').attributes('aria-label')).toBe(
|
|
135
|
+
'Videobediening: Rondleiding',
|
|
136
|
+
)
|
|
137
|
+
expect(wrapper.find('button[aria-label="Afspelen"]').exists()).toBe(true)
|
|
138
|
+
expect(wrapper.find('button[aria-label="Geluid uit"]').exists()).toBe(true)
|
|
139
|
+
expect(slider(wrapper, 'Tijdlijn').attributes('aria-valuetext')).toBe('0:00 van 1:40')
|
|
140
|
+
expect(slider(wrapper, 'Volume').attributes('aria-valuetext')).toBe('100 procent')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('merges overrides per key and keeps the defaults for the rest', async () => {
|
|
144
|
+
const { wrapper } = mountControls({
|
|
145
|
+
messages: {
|
|
146
|
+
play: 'Play',
|
|
147
|
+
group: (name) => `Controls ${name}`.trim(),
|
|
148
|
+
timeValue: (current, duration) => `${current} of ${duration}`,
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
await nextTick()
|
|
152
|
+
|
|
153
|
+
expect(wrapper.get('[role="group"]').attributes('aria-label')).toBe('Controls')
|
|
154
|
+
expect(wrapper.find('button[aria-label="Play"]').exists()).toBe(true)
|
|
155
|
+
expect(wrapper.find('button[aria-label="Geluid uit"]').exists()).toBe(true)
|
|
156
|
+
expect(slider(wrapper, 'Tijdlijn').attributes('aria-valuetext')).toBe('0:00 of 1:40')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('state from the video', () => {
|
|
161
|
+
it('follows the media events, without play or pause emits', async () => {
|
|
162
|
+
const { wrapper, video, fields } = mountControls()
|
|
163
|
+
await nextTick()
|
|
164
|
+
|
|
165
|
+
fields.paused = false
|
|
166
|
+
video.dispatchEvent(new Event('play'))
|
|
167
|
+
await nextTick()
|
|
168
|
+
|
|
169
|
+
expect(wrapper.find('button[aria-label="Pauzeren"]').exists()).toBe(true)
|
|
170
|
+
expect(Object.keys(wrapper.emitted())).not.toContain('play')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('offers a replay after the end', async () => {
|
|
174
|
+
const { wrapper, video, fields } = mountControls()
|
|
175
|
+
fields.ended = true
|
|
176
|
+
video.dispatchEvent(new Event('ended'))
|
|
177
|
+
await nextTick()
|
|
178
|
+
|
|
179
|
+
expect(wrapper.find('button[aria-label="Opnieuw afspelen"]').exists()).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('timeline', () => {
|
|
184
|
+
it('steps 5 s with the arrows, 10% with Page Up/Down, and jumps with Home/End', async () => {
|
|
185
|
+
const { wrapper, fields } = mountControls({}, { currentTime: 40 })
|
|
186
|
+
await nextTick()
|
|
187
|
+
const timeline = slider(wrapper, 'Tijdlijn')
|
|
188
|
+
|
|
189
|
+
await timeline.trigger('keydown', { key: 'ArrowRight' })
|
|
190
|
+
expect(fields.currentTime).toBe(45)
|
|
191
|
+
await timeline.trigger('keydown', { key: 'ArrowDown' })
|
|
192
|
+
expect(fields.currentTime).toBe(40)
|
|
193
|
+
await timeline.trigger('keydown', { key: 'PageUp' })
|
|
194
|
+
expect(fields.currentTime).toBe(50)
|
|
195
|
+
await timeline.trigger('keydown', { key: 'PageDown' })
|
|
196
|
+
expect(fields.currentTime).toBe(40)
|
|
197
|
+
await timeline.trigger('keydown', { key: 'End' })
|
|
198
|
+
expect(fields.currentTime).toBe(100)
|
|
199
|
+
await timeline.trigger('keydown', { key: 'Home' })
|
|
200
|
+
expect(fields.currentTime).toBe(0)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('says the time as "0:12 van 0:42" and reports whole seconds only', async () => {
|
|
204
|
+
const { wrapper, video, fields } = mountControls({}, { duration: 42, currentTime: 12.2 })
|
|
205
|
+
await nextTick()
|
|
206
|
+
const timeline = slider(wrapper, 'Tijdlijn')
|
|
207
|
+
|
|
208
|
+
expect(timeline.attributes('aria-valuetext')).toBe('0:12 van 0:42')
|
|
209
|
+
expect(timeline.attributes('aria-valuenow')).toBe('12')
|
|
210
|
+
expect(timeline.attributes('aria-valuemax')).toBe('42')
|
|
211
|
+
|
|
212
|
+
fields.currentTime = 12.9
|
|
213
|
+
video.dispatchEvent(new Event('timeupdate'))
|
|
214
|
+
await nextTick()
|
|
215
|
+
expect(timeline.attributes('aria-valuenow')).toBe('12')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('stays focusable but disabled, with an explanation, while the duration is unknown', async () => {
|
|
219
|
+
const { wrapper, seeks } = mountControls({}, { duration: Number.NaN })
|
|
220
|
+
await nextTick()
|
|
221
|
+
const timeline = slider(wrapper, 'Tijdlijn')
|
|
222
|
+
|
|
223
|
+
expect(timeline.attributes('aria-disabled')).toBe('true')
|
|
224
|
+
expect(timeline.attributes('tabindex')).toBe('0')
|
|
225
|
+
expect(timeline.attributes('aria-valuetext')).toBe('Duur nog onbekend')
|
|
226
|
+
|
|
227
|
+
await timeline.trigger('keydown', { key: 'ArrowRight' })
|
|
228
|
+
expect(seeks).toEqual([])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('seeks once, on letting go, however far it is dragged', async () => {
|
|
232
|
+
const { wrapper, seeks } = mountControls()
|
|
233
|
+
await nextTick()
|
|
234
|
+
const timeline = slider(wrapper, 'Tijdlijn')
|
|
235
|
+
vi.spyOn(timeline.element, 'getBoundingClientRect').mockReturnValue(
|
|
236
|
+
DOMRect.fromRect({ x: 0, y: 0, width: 200, height: 32 }),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const pointer = (type: string, clientX: number) =>
|
|
240
|
+
timeline.element.dispatchEvent(new PointerEvent(type, { clientX, pointerId: 1 }))
|
|
241
|
+
|
|
242
|
+
pointer('pointerdown', 20)
|
|
243
|
+
pointer('pointermove', 60)
|
|
244
|
+
pointer('pointermove', 140)
|
|
245
|
+
await nextTick()
|
|
246
|
+
|
|
247
|
+
expect(seeks).toEqual([])
|
|
248
|
+
expect(timeline.attributes('aria-valuetext')).toBe('1:10 van 1:40')
|
|
249
|
+
|
|
250
|
+
pointer('pointerup', 140)
|
|
251
|
+
expect(seeks).toEqual([70])
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('drops the scrub on a cancelled pointer: no seek, back to the real time', async () => {
|
|
255
|
+
const { wrapper, seeks } = mountControls({}, { currentTime: 10 })
|
|
256
|
+
await nextTick()
|
|
257
|
+
const timeline = slider(wrapper, 'Tijdlijn')
|
|
258
|
+
vi.spyOn(timeline.element, 'getBoundingClientRect').mockReturnValue(
|
|
259
|
+
DOMRect.fromRect({ x: 0, y: 0, width: 200, height: 32 }),
|
|
260
|
+
)
|
|
261
|
+
const pointer = (type: string, clientX: number) =>
|
|
262
|
+
timeline.element.dispatchEvent(new PointerEvent(type, { clientX, pointerId: 1 }))
|
|
263
|
+
|
|
264
|
+
pointer('pointerdown', 20)
|
|
265
|
+
pointer('pointermove', 140)
|
|
266
|
+
await nextTick()
|
|
267
|
+
expect(timeline.attributes('aria-valuetext')).toBe('1:10 van 1:40')
|
|
268
|
+
|
|
269
|
+
pointer('pointercancel', 140)
|
|
270
|
+
await nextTick()
|
|
271
|
+
expect(seeks).toEqual([])
|
|
272
|
+
expect(timeline.attributes('aria-valuetext')).toBe('0:10 van 1:40')
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
describe('volume', () => {
|
|
277
|
+
it('steps 10% with the arrows and goes to 0 or 100% with Home/End', async () => {
|
|
278
|
+
const { wrapper, fields } = mountControls({}, { volume: 0.5 })
|
|
279
|
+
await nextTick()
|
|
280
|
+
const volume = slider(wrapper, 'Volume')
|
|
281
|
+
|
|
282
|
+
await volume.trigger('keydown', { key: 'ArrowUp' })
|
|
283
|
+
expect(fields.volume).toBeCloseTo(0.6)
|
|
284
|
+
await volume.trigger('keydown', { key: 'ArrowLeft' })
|
|
285
|
+
expect(fields.volume).toBeCloseTo(0.5)
|
|
286
|
+
await volume.trigger('keydown', { key: 'Home' })
|
|
287
|
+
expect(fields.volume).toBe(0)
|
|
288
|
+
expect(fields.muted).toBe(true)
|
|
289
|
+
expect(volume.attributes('aria-valuetext')).toBe('Gedempt')
|
|
290
|
+
await volume.trigger('keydown', { key: 'End' })
|
|
291
|
+
expect(fields.volume).toBe(1)
|
|
292
|
+
expect(fields.muted).toBe(false)
|
|
293
|
+
expect(volume.attributes('aria-valuetext')).toBe('100 procent')
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('volume popover', () => {
|
|
298
|
+
const popover = (w: VueWrapper) => w.find('[data-volume-popover]')
|
|
299
|
+
const isOpen = (w: VueWrapper) => popover(w).classes('opacity-100')
|
|
300
|
+
const soundWrapper = (w: VueWrapper) =>
|
|
301
|
+
w.get('button[aria-label="Geluid uit"]').element.parentElement!
|
|
302
|
+
|
|
303
|
+
it('is a vertical slider with the volume copy', async () => {
|
|
304
|
+
const { wrapper } = mountControls({}, { volume: 0.8 })
|
|
305
|
+
await nextTick()
|
|
306
|
+
const volume = slider(wrapper, 'Volume')
|
|
307
|
+
|
|
308
|
+
expect(volume.attributes('aria-orientation')).toBe('vertical')
|
|
309
|
+
expect(volume.attributes('aria-valuetext')).toBe('80 procent')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('opens on hover, and closes 300 ms after the pointer leaves', async () => {
|
|
313
|
+
vi.useFakeTimers()
|
|
314
|
+
const { wrapper } = mountControls()
|
|
315
|
+
await nextTick()
|
|
316
|
+
expect(isOpen(wrapper)).toBe(false)
|
|
317
|
+
|
|
318
|
+
soundWrapper(wrapper).dispatchEvent(new PointerEvent('pointerenter'))
|
|
319
|
+
await nextTick()
|
|
320
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
321
|
+
|
|
322
|
+
soundWrapper(wrapper).dispatchEvent(new PointerEvent('pointerleave'))
|
|
323
|
+
await nextTick()
|
|
324
|
+
vi.advanceTimersByTime(299)
|
|
325
|
+
await nextTick()
|
|
326
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
327
|
+
vi.advanceTimersByTime(1)
|
|
328
|
+
await nextTick()
|
|
329
|
+
expect(isOpen(wrapper)).toBe(false)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('opens with the focus on the sound button, and keeps the bar up while open', async () => {
|
|
333
|
+
vi.useFakeTimers()
|
|
334
|
+
const { wrapper } = mountControls({}, { paused: false })
|
|
335
|
+
await nextTick()
|
|
336
|
+
|
|
337
|
+
const sound = wrapper.get('button[aria-label="Geluid uit"]')
|
|
338
|
+
tabTo(sound.element)
|
|
339
|
+
await nextTick()
|
|
340
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
341
|
+
|
|
342
|
+
// Focus leaves the bar for the page: the bar would hide, but not while the popover is up
|
|
343
|
+
const group = wrapper.get('[role="group"]').element
|
|
344
|
+
group.dispatchEvent(new FocusEvent('focusout', { relatedTarget: null }))
|
|
345
|
+
vi.advanceTimersByTime(5000)
|
|
346
|
+
await nextTick()
|
|
347
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-100')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('Escape on the slider closes it and puts the focus on the sound button', async () => {
|
|
351
|
+
const { wrapper } = mountControls()
|
|
352
|
+
await nextTick()
|
|
353
|
+
const volume = slider(wrapper, 'Volume')
|
|
354
|
+
|
|
355
|
+
tabTo(volume.element)
|
|
356
|
+
await nextTick()
|
|
357
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
358
|
+
|
|
359
|
+
await volume.trigger('keydown', { key: 'Escape' })
|
|
360
|
+
expect(isOpen(wrapper)).toBe(false)
|
|
361
|
+
expect(document.activeElement?.getAttribute('aria-label')).toBe('Geluid uit')
|
|
362
|
+
|
|
363
|
+
// Back to the slider reopens it
|
|
364
|
+
tabTo(volume.element)
|
|
365
|
+
await nextTick()
|
|
366
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('is not there on a touch screen: the sound button is all', async () => {
|
|
370
|
+
vi.stubGlobal('matchMedia', (query: string) => ({
|
|
371
|
+
matches: query.includes('coarse'),
|
|
372
|
+
media: query,
|
|
373
|
+
addEventListener: () => {},
|
|
374
|
+
removeEventListener: () => {},
|
|
375
|
+
}))
|
|
376
|
+
const { wrapper } = mountControls()
|
|
377
|
+
await nextTick()
|
|
378
|
+
|
|
379
|
+
expect(popover(wrapper).exists()).toBe(false)
|
|
380
|
+
expect(wrapper.find('button[aria-label="Geluid uit"]').exists()).toBe(true)
|
|
381
|
+
vi.unstubAllGlobals()
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('is not there when the player is too low to hold it', async () => {
|
|
385
|
+
const low = mountControls({}, {}, 120)
|
|
386
|
+
await nextTick()
|
|
387
|
+
expect(popover(low.wrapper).exists()).toBe(false)
|
|
388
|
+
low.wrapper.unmount()
|
|
389
|
+
host.remove()
|
|
390
|
+
|
|
391
|
+
const tall = mountControls({}, {}, 270)
|
|
392
|
+
await nextTick()
|
|
393
|
+
expect(popover(tall.wrapper).exists()).toBe(true)
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
describe('volume popover, dismissing and losing it', () => {
|
|
398
|
+
const isOpen = (w: VueWrapper) => w.find('[data-volume-popover]').classes('opacity-100')
|
|
399
|
+
|
|
400
|
+
it('Escape closes a popover opened by hover, without moving the focus, until a new hover', async () => {
|
|
401
|
+
const { wrapper } = mountControls()
|
|
402
|
+
await nextTick()
|
|
403
|
+
const wrapperEl = wrapper.get('button[aria-label="Geluid uit"]').element.parentElement!
|
|
404
|
+
const before = document.activeElement
|
|
405
|
+
|
|
406
|
+
wrapperEl.dispatchEvent(new PointerEvent('pointerenter'))
|
|
407
|
+
await nextTick()
|
|
408
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
409
|
+
|
|
410
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
411
|
+
await nextTick()
|
|
412
|
+
expect(isOpen(wrapper)).toBe(false)
|
|
413
|
+
expect(document.activeElement).toBe(before)
|
|
414
|
+
|
|
415
|
+
wrapperEl.dispatchEvent(new PointerEvent('pointerleave'))
|
|
416
|
+
wrapperEl.dispatchEvent(new PointerEvent('pointerenter'))
|
|
417
|
+
await nextTick()
|
|
418
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('Escape with the focus on the sound button closes it and leaves the focus there', async () => {
|
|
422
|
+
const { wrapper } = mountControls()
|
|
423
|
+
await nextTick()
|
|
424
|
+
const sound = wrapper.get('button[aria-label="Geluid uit"]').element
|
|
425
|
+
tabTo(sound)
|
|
426
|
+
await nextTick()
|
|
427
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
428
|
+
|
|
429
|
+
sound.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
430
|
+
await nextTick()
|
|
431
|
+
expect(isOpen(wrapper)).toBe(false)
|
|
432
|
+
expect(document.activeElement).toBe(sound)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('forgets its focus when the slider goes away (a touch screen, a smaller player)', async () => {
|
|
436
|
+
let coarse = false
|
|
437
|
+
const listeners: ((event: { matches: boolean }) => void)[] = []
|
|
438
|
+
vi.stubGlobal('matchMedia', (query: string) => ({
|
|
439
|
+
get matches() {
|
|
440
|
+
return query.includes('coarse') && coarse
|
|
441
|
+
},
|
|
442
|
+
media: query,
|
|
443
|
+
addEventListener: (_: string, listener: (event: { matches: boolean }) => void) => {
|
|
444
|
+
if (query.includes('coarse')) listeners.push(listener)
|
|
445
|
+
},
|
|
446
|
+
removeEventListener: () => {},
|
|
447
|
+
}))
|
|
448
|
+
const { wrapper } = mountControls()
|
|
449
|
+
// Vue skips a bubbling event in a handler attached in the same millisecond (see Space)
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
451
|
+
tabTo(slider(wrapper, 'Volume').element)
|
|
452
|
+
await nextTick()
|
|
453
|
+
expect(isOpen(wrapper)).toBe(true)
|
|
454
|
+
|
|
455
|
+
coarse = true
|
|
456
|
+
listeners.forEach((listener) => listener({ matches: true }))
|
|
457
|
+
await nextTick()
|
|
458
|
+
expect(wrapper.find('[data-volume-popover]').exists()).toBe(false)
|
|
459
|
+
|
|
460
|
+
coarse = false
|
|
461
|
+
listeners.forEach((listener) => listener({ matches: false }))
|
|
462
|
+
await nextTick()
|
|
463
|
+
expect(isOpen(wrapper)).toBe(false)
|
|
464
|
+
vi.unstubAllGlobals()
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
describe('Escape with the popover open', () => {
|
|
469
|
+
/** The app around the player: Escape handlers on window and document, as a page has. */
|
|
470
|
+
const appListeners = () => {
|
|
471
|
+
const onWindow = vi.fn()
|
|
472
|
+
const onDocument = vi.fn()
|
|
473
|
+
window.addEventListener('keydown', onWindow)
|
|
474
|
+
document.addEventListener('keydown', onDocument)
|
|
475
|
+
return {
|
|
476
|
+
onWindow,
|
|
477
|
+
onDocument,
|
|
478
|
+
remove: () => {
|
|
479
|
+
window.removeEventListener('keydown', onWindow)
|
|
480
|
+
document.removeEventListener('keydown', onDocument)
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** The controls in a modal `<dialog>`, as in the Lightbox. */
|
|
486
|
+
const mountInDialog = () => {
|
|
487
|
+
const fake = fakeVideo()
|
|
488
|
+
const dialog = document.createElement('dialog')
|
|
489
|
+
host = document.createElement('div')
|
|
490
|
+
host.append(fake.video)
|
|
491
|
+
dialog.append(host)
|
|
492
|
+
document.body.append(dialog)
|
|
493
|
+
if (typeof dialog.showModal === 'function') dialog.showModal()
|
|
494
|
+
else dialog.setAttribute('open', '')
|
|
495
|
+
const onCancel = vi.fn()
|
|
496
|
+
dialog.addEventListener('cancel', onCancel)
|
|
497
|
+
wrapper = mount(VideoControls, {
|
|
498
|
+
props: { target: fake.video },
|
|
499
|
+
attachTo: host.appendChild(document.createElement('div')),
|
|
500
|
+
})
|
|
501
|
+
return { dialog, onCancel }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const escape = (target: EventTarget = document.body) => {
|
|
505
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
506
|
+
target.dispatchEvent(event)
|
|
507
|
+
return event
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
it('closes only the popover: the dialog stays open and the app never sees the key', async () => {
|
|
511
|
+
const { dialog, onCancel } = mountInDialog()
|
|
512
|
+
await nextTick()
|
|
513
|
+
const app = appListeners()
|
|
514
|
+
const sound = wrapper!.get('button[aria-label="Geluid uit"]').element
|
|
515
|
+
sound.parentElement!.dispatchEvent(new PointerEvent('pointerenter'))
|
|
516
|
+
await nextTick()
|
|
517
|
+
expect(wrapper!.get('[data-volume-popover]').classes()).toContain('opacity-100')
|
|
518
|
+
|
|
519
|
+
const event = escape(sound)
|
|
520
|
+
await nextTick()
|
|
521
|
+
|
|
522
|
+
expect(wrapper!.get('[data-volume-popover]').classes()).toContain('opacity-0')
|
|
523
|
+
// preventDefault stops a dialog's cancel, the key's default action
|
|
524
|
+
expect(event.defaultPrevented).toBe(true)
|
|
525
|
+
expect(dialog.open).toBe(true)
|
|
526
|
+
expect(onCancel).not.toHaveBeenCalled()
|
|
527
|
+
expect(app.onWindow).not.toHaveBeenCalled()
|
|
528
|
+
expect(app.onDocument).not.toHaveBeenCalled()
|
|
529
|
+
app.remove()
|
|
530
|
+
dialog.remove()
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('from the slider, also hands the focus back to the sound button', async () => {
|
|
534
|
+
const { wrapper } = mountControls()
|
|
535
|
+
// Vue skips a bubbling event in a handler attached in the same millisecond (see Space)
|
|
536
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
537
|
+
const volume = slider(wrapper, 'Volume').element
|
|
538
|
+
tabTo(volume)
|
|
539
|
+
await nextTick()
|
|
540
|
+
|
|
541
|
+
escape(volume)
|
|
542
|
+
await nextTick()
|
|
543
|
+
expect(document.activeElement?.getAttribute('aria-label')).toBe('Geluid uit')
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('passes untouched while the popover is closed', async () => {
|
|
547
|
+
mountControls()
|
|
548
|
+
await nextTick()
|
|
549
|
+
const app = appListeners()
|
|
550
|
+
|
|
551
|
+
const event = escape()
|
|
552
|
+
expect(event.defaultPrevented).toBe(false)
|
|
553
|
+
expect(app.onWindow).toHaveBeenCalledOnce()
|
|
554
|
+
expect(app.onDocument).toHaveBeenCalledOnce()
|
|
555
|
+
app.remove()
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('lets go of window when unmounted', async () => {
|
|
559
|
+
const { wrapper } = mountControls()
|
|
560
|
+
await nextTick()
|
|
561
|
+
wrapper
|
|
562
|
+
.get('button[aria-label="Geluid uit"]')
|
|
563
|
+
.element.parentElement!.dispatchEvent(new PointerEvent('pointerenter'))
|
|
564
|
+
await nextTick()
|
|
565
|
+
wrapper.unmount()
|
|
566
|
+
|
|
567
|
+
const app = appListeners()
|
|
568
|
+
const event = escape()
|
|
569
|
+
expect(event.defaultPrevented).toBe(false)
|
|
570
|
+
expect(app.onDocument).toHaveBeenCalledOnce()
|
|
571
|
+
app.remove()
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
describe('focus({ focusVisible: true })', () => {
|
|
576
|
+
it('only keeps the bar up when the focus really lands', async () => {
|
|
577
|
+
vi.useFakeTimers()
|
|
578
|
+
const controls = ref<InstanceType<typeof VideoControls> | null>(null)
|
|
579
|
+
const fake = fakeVideo({ paused: false })
|
|
580
|
+
host = document.createElement('div')
|
|
581
|
+
host.append(fake.video)
|
|
582
|
+
document.body.append(host)
|
|
583
|
+
wrapper = mount(
|
|
584
|
+
defineComponent({
|
|
585
|
+
setup: () => () => h(VideoControls, { ref: controls, target: fake.video }),
|
|
586
|
+
}),
|
|
587
|
+
{ attachTo: host.appendChild(document.createElement('div')) },
|
|
588
|
+
)
|
|
589
|
+
await nextTick()
|
|
590
|
+
// A button that takes no focus, as on an inert bar
|
|
591
|
+
const play = wrapper.get('button[aria-label="Pauzeren"]').element as HTMLButtonElement
|
|
592
|
+
vi.spyOn(play, 'focus').mockImplementation(() => {})
|
|
593
|
+
|
|
594
|
+
controls.value?.focus({ focusVisible: true })
|
|
595
|
+
vi.advanceTimersByTime(2500)
|
|
596
|
+
await nextTick()
|
|
597
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-0')
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
describe('shortcuts', () => {
|
|
602
|
+
it('Space on a slider plays and pauses, without scrolling', async () => {
|
|
603
|
+
const { wrapper, play } = mountControls()
|
|
604
|
+
// Vue skips a bubbling event in a handler attached in the same millisecond the event was
|
|
605
|
+
// first handled (the slider's own keydown). A real key press never comes that soon.
|
|
606
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
607
|
+
const event = new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true })
|
|
608
|
+
|
|
609
|
+
slider(wrapper, 'Tijdlijn').element.dispatchEvent(event)
|
|
610
|
+
|
|
611
|
+
expect(play).toHaveBeenCalledOnce()
|
|
612
|
+
expect(event.defaultPrevented).toBe(true)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('leaves Space on a button to that button', async () => {
|
|
616
|
+
const { wrapper, play, fields } = mountControls()
|
|
617
|
+
await nextTick()
|
|
618
|
+
const event = new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true })
|
|
619
|
+
|
|
620
|
+
wrapper.get('button[aria-label="Geluid uit"]').element.dispatchEvent(event)
|
|
621
|
+
|
|
622
|
+
expect(play).not.toHaveBeenCalled()
|
|
623
|
+
expect(fields.muted).toBe(false)
|
|
624
|
+
expect(event.defaultPrevented).toBe(false)
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it('M toggles the sound with the focus anywhere in the bar', async () => {
|
|
628
|
+
const { wrapper, fields } = mountControls()
|
|
629
|
+
await nextTick()
|
|
630
|
+
|
|
631
|
+
await wrapper.get('button[aria-label="Afspelen"]').trigger('keydown', { key: 'm' })
|
|
632
|
+
expect(fields.muted).toBe(true)
|
|
633
|
+
await slider(wrapper, 'Volume').trigger('keydown', { key: 'M' })
|
|
634
|
+
expect(fields.muted).toBe(false)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('does nothing outside the bar', async () => {
|
|
638
|
+
const { play, fields, video } = mountControls()
|
|
639
|
+
await nextTick()
|
|
640
|
+
|
|
641
|
+
for (const key of [' ', 'm', 'f']) {
|
|
642
|
+
video.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
|
|
643
|
+
document.body.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
expect(play).not.toHaveBeenCalled()
|
|
647
|
+
expect(fields.muted).toBe(false)
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
describe('auto-hide', () => {
|
|
652
|
+
const capsuleShown = (w: VueWrapper) => w.get('[role="group"]').classes('opacity-100')
|
|
653
|
+
|
|
654
|
+
it('hides 2500 ms after the last movement while playing, and comes back on movement', async () => {
|
|
655
|
+
vi.useFakeTimers()
|
|
656
|
+
const { wrapper } = mountControls({}, { paused: false })
|
|
657
|
+
await nextTick()
|
|
658
|
+
expect(capsuleShown(wrapper)).toBe(true)
|
|
659
|
+
|
|
660
|
+
vi.advanceTimersByTime(2499)
|
|
661
|
+
await nextTick()
|
|
662
|
+
expect(capsuleShown(wrapper)).toBe(true)
|
|
663
|
+
|
|
664
|
+
vi.advanceTimersByTime(1)
|
|
665
|
+
await nextTick()
|
|
666
|
+
expect(capsuleShown(wrapper)).toBe(false)
|
|
667
|
+
expect(wrapper.emitted('visibility-change')?.at(-1)).toEqual([false])
|
|
668
|
+
|
|
669
|
+
host.dispatchEvent(new PointerEvent('pointermove', { pointerType: 'mouse' }))
|
|
670
|
+
await nextTick()
|
|
671
|
+
expect(capsuleShown(wrapper)).toBe(true)
|
|
672
|
+
expect(wrapper.emitted('visibility-change')?.at(-1)).toEqual([true])
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('hides 300 ms after the pointer leaves the video', async () => {
|
|
676
|
+
vi.useFakeTimers()
|
|
677
|
+
const { wrapper } = mountControls({}, { paused: false })
|
|
678
|
+
await nextTick()
|
|
679
|
+
|
|
680
|
+
host.dispatchEvent(new PointerEvent('pointerleave'))
|
|
681
|
+
vi.advanceTimersByTime(300)
|
|
682
|
+
await nextTick()
|
|
683
|
+
expect(capsuleShown(wrapper)).toBe(false)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('stays while paused, with focus inside, or with autoHide off', async () => {
|
|
687
|
+
vi.useFakeTimers()
|
|
688
|
+
const paused = mountControls()
|
|
689
|
+
vi.advanceTimersByTime(5000)
|
|
690
|
+
await nextTick()
|
|
691
|
+
expect(capsuleShown(paused.wrapper)).toBe(true)
|
|
692
|
+
paused.wrapper.unmount()
|
|
693
|
+
host.remove()
|
|
694
|
+
|
|
695
|
+
const focused = mountControls({}, { paused: false })
|
|
696
|
+
tabTo(focused.wrapper.get('[data-video-controls] button').element)
|
|
697
|
+
vi.advanceTimersByTime(5000)
|
|
698
|
+
await nextTick()
|
|
699
|
+
expect(capsuleShown(focused.wrapper)).toBe(true)
|
|
700
|
+
focused.wrapper.unmount()
|
|
701
|
+
host.remove()
|
|
702
|
+
|
|
703
|
+
const pinned = mountControls({ autoHide: false }, { paused: false })
|
|
704
|
+
vi.advanceTimersByTime(5000)
|
|
705
|
+
await nextTick()
|
|
706
|
+
expect(capsuleShown(pinned.wrapper)).toBe(true)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('stays hidden in the tab order: transparent, never removed', async () => {
|
|
710
|
+
vi.useFakeTimers()
|
|
711
|
+
const { wrapper } = mountControls({}, { paused: false })
|
|
712
|
+
vi.advanceTimersByTime(2500)
|
|
713
|
+
await nextTick()
|
|
714
|
+
|
|
715
|
+
expect(wrapper.find('button[aria-label="Pauzeren"]').isVisible()).toBe(true)
|
|
716
|
+
expect(wrapper.get('[role="group"]').classes()).not.toContain('invisible')
|
|
717
|
+
})
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
describe('focus after a click', () => {
|
|
721
|
+
it.each(['full', 'toggle'] as const)(
|
|
722
|
+
'%s: a mouse click keeps the focus but not the bar; a key brings it back',
|
|
723
|
+
async (variant) => {
|
|
724
|
+
vi.useFakeTimers()
|
|
725
|
+
const { wrapper } = mountControls({ variant }, { paused: false })
|
|
726
|
+
await nextTick()
|
|
727
|
+
const shownNow = () => wrapper.get('[data-video-controls]').classes('opacity-100')
|
|
728
|
+
// The sound button: a click on it leaves the video playing, so only the focus could hold the bar
|
|
729
|
+
const sound = wrapper.get('[data-video-controls] button[aria-label^="Geluid uit"]').element
|
|
730
|
+
|
|
731
|
+
mouseClick(sound)
|
|
732
|
+
await nextTick()
|
|
733
|
+
vi.advanceTimersByTime(2500)
|
|
734
|
+
await nextTick()
|
|
735
|
+
expect(document.activeElement).toBe(sound)
|
|
736
|
+
expect(shownNow()).toBe(false)
|
|
737
|
+
|
|
738
|
+
sound.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
739
|
+
await nextTick()
|
|
740
|
+
vi.advanceTimersByTime(5000)
|
|
741
|
+
await nextTick()
|
|
742
|
+
expect(shownNow()).toBe(true)
|
|
743
|
+
},
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
it('keyboard focus keeps the bar up', async () => {
|
|
747
|
+
vi.useFakeTimers()
|
|
748
|
+
const { wrapper } = mountControls({}, { paused: false })
|
|
749
|
+
await nextTick()
|
|
750
|
+
tabTo(wrapper.get('button[aria-label="Pauzeren"]').element)
|
|
751
|
+
vi.advanceTimersByTime(5000)
|
|
752
|
+
await nextTick()
|
|
753
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-100')
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
it('focus({ focusVisible: false }) leaves the bar to hide', async () => {
|
|
757
|
+
vi.useFakeTimers()
|
|
758
|
+
const controls = ref<InstanceType<typeof VideoControls> | null>(null)
|
|
759
|
+
const fake = fakeVideo({ paused: false })
|
|
760
|
+
host = document.createElement('div')
|
|
761
|
+
host.append(fake.video)
|
|
762
|
+
document.body.append(host)
|
|
763
|
+
wrapper = mount(
|
|
764
|
+
defineComponent({
|
|
765
|
+
setup: () => () => h(VideoControls, { ref: controls, target: fake.video }),
|
|
766
|
+
}),
|
|
767
|
+
{ attachTo: host.appendChild(document.createElement('div')) },
|
|
768
|
+
)
|
|
769
|
+
await nextTick()
|
|
770
|
+
|
|
771
|
+
controls.value?.focus({ focusVisible: false })
|
|
772
|
+
vi.advanceTimersByTime(2500)
|
|
773
|
+
await nextTick()
|
|
774
|
+
expect(document.activeElement?.getAttribute('aria-label')).toBe('Pauzeren')
|
|
775
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-0')
|
|
776
|
+
})
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
describe('clicks on the video', () => {
|
|
780
|
+
it('with a mouse, a click plays and pauses', async () => {
|
|
781
|
+
const { video, play } = mountControls()
|
|
782
|
+
await nextTick()
|
|
783
|
+
|
|
784
|
+
video.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'mouse' }))
|
|
785
|
+
video.click()
|
|
786
|
+
|
|
787
|
+
expect(play).toHaveBeenCalledOnce()
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
it('on touch, a tap shows the bar and never pauses', async () => {
|
|
791
|
+
vi.useFakeTimers()
|
|
792
|
+
const { wrapper, video, pause } = mountControls({}, { paused: false })
|
|
793
|
+
vi.advanceTimersByTime(2500)
|
|
794
|
+
await nextTick()
|
|
795
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-0')
|
|
796
|
+
|
|
797
|
+
video.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'touch' }))
|
|
798
|
+
video.click()
|
|
799
|
+
await nextTick()
|
|
800
|
+
|
|
801
|
+
expect(pause).not.toHaveBeenCalled()
|
|
802
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-100')
|
|
803
|
+
})
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
describe('toggle variant', () => {
|
|
807
|
+
const corner = (w: VueWrapper) => w.get('[data-video-controls]')
|
|
808
|
+
const shownCorner = (w: VueWrapper) => corner(w).classes('opacity-100')
|
|
809
|
+
const muteButton = (w: VueWrapper) => w.get('button[aria-label="Geluid uit: Rondleiding"]')
|
|
810
|
+
const muteShown = (w: VueWrapper) => muteButton(w).classes('opacity-100')
|
|
811
|
+
|
|
812
|
+
it('is mute and play/pause, in that order (as seen), and leaves mouse clicks on the video alone', async () => {
|
|
813
|
+
const { wrapper, video, play, fields } = mountControls({
|
|
814
|
+
variant: 'toggle',
|
|
815
|
+
name: 'Rondleiding',
|
|
816
|
+
})
|
|
817
|
+
await nextTick()
|
|
818
|
+
|
|
819
|
+
expect(wrapper.findAll('button').map((b) => b.attributes('aria-label'))).toEqual([
|
|
820
|
+
'Geluid uit: Rondleiding',
|
|
821
|
+
'Afspelen: Rondleiding',
|
|
822
|
+
])
|
|
823
|
+
expect(wrapper.find('[role="slider"]').exists()).toBe(false)
|
|
824
|
+
|
|
825
|
+
video.click()
|
|
826
|
+
expect(play).not.toHaveBeenCalled()
|
|
827
|
+
|
|
828
|
+
await wrapper.get('button[aria-label="Afspelen: Rondleiding"]').trigger('click')
|
|
829
|
+
expect(play).toHaveBeenCalledOnce()
|
|
830
|
+
await muteButton(wrapper).trigger('click')
|
|
831
|
+
expect(fields.muted).toBe(true)
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
it('hides while playing, like the bar, and comes back on movement and on pause', async () => {
|
|
835
|
+
vi.useFakeTimers()
|
|
836
|
+
const { wrapper, video, fields } = mountControls({ variant: 'toggle' }, { paused: false })
|
|
837
|
+
await nextTick()
|
|
838
|
+
expect(shownCorner(wrapper)).toBe(true)
|
|
839
|
+
|
|
840
|
+
vi.advanceTimersByTime(2500)
|
|
841
|
+
await nextTick()
|
|
842
|
+
expect(shownCorner(wrapper)).toBe(false)
|
|
843
|
+
expect(wrapper.emitted('visibility-change')?.at(-1)).toEqual([false])
|
|
844
|
+
|
|
845
|
+
host.dispatchEvent(new PointerEvent('pointermove', { pointerType: 'mouse' }))
|
|
846
|
+
await nextTick()
|
|
847
|
+
expect(shownCorner(wrapper)).toBe(true)
|
|
848
|
+
|
|
849
|
+
host.dispatchEvent(new PointerEvent('pointerleave'))
|
|
850
|
+
vi.advanceTimersByTime(300)
|
|
851
|
+
await nextTick()
|
|
852
|
+
expect(shownCorner(wrapper)).toBe(false)
|
|
853
|
+
|
|
854
|
+
fields.paused = true
|
|
855
|
+
video.dispatchEvent(new Event('pause'))
|
|
856
|
+
await nextTick()
|
|
857
|
+
expect(shownCorner(wrapper)).toBe(true)
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
it('stays with focus inside, and always with autoHide off', async () => {
|
|
861
|
+
vi.useFakeTimers()
|
|
862
|
+
const focused = mountControls({ variant: 'toggle' }, { paused: false })
|
|
863
|
+
tabTo(focused.wrapper.get('[data-video-controls] button').element)
|
|
864
|
+
vi.advanceTimersByTime(5000)
|
|
865
|
+
await nextTick()
|
|
866
|
+
expect(shownCorner(focused.wrapper)).toBe(true)
|
|
867
|
+
focused.wrapper.unmount()
|
|
868
|
+
host.remove()
|
|
869
|
+
|
|
870
|
+
const pinned = mountControls({ variant: 'toggle', autoHide: false }, { paused: false })
|
|
871
|
+
vi.advanceTimersByTime(5000)
|
|
872
|
+
await nextTick()
|
|
873
|
+
expect(shownCorner(pinned.wrapper)).toBe(true)
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
it('shows mute only with a pointer over the video, focus, or a tap', async () => {
|
|
877
|
+
const { wrapper, video } = mountControls({ variant: 'toggle', name: 'Rondleiding' })
|
|
878
|
+
await nextTick()
|
|
879
|
+
expect(muteShown(wrapper)).toBe(false)
|
|
880
|
+
// Transparent, never removed: still in the tab order
|
|
881
|
+
expect(muteButton(wrapper).attributes('tabindex')).toBeUndefined()
|
|
882
|
+
|
|
883
|
+
host.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }))
|
|
884
|
+
await nextTick()
|
|
885
|
+
expect(muteShown(wrapper)).toBe(true)
|
|
886
|
+
host.dispatchEvent(new PointerEvent('pointerleave'))
|
|
887
|
+
await nextTick()
|
|
888
|
+
expect(muteShown(wrapper)).toBe(false)
|
|
889
|
+
|
|
890
|
+
tabTo(muteButton(wrapper).element)
|
|
891
|
+
await nextTick()
|
|
892
|
+
expect(muteShown(wrapper)).toBe(true)
|
|
893
|
+
;(muteButton(wrapper).element as HTMLButtonElement).blur()
|
|
894
|
+
await nextTick()
|
|
895
|
+
expect(muteShown(wrapper)).toBe(false)
|
|
896
|
+
|
|
897
|
+
video.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'touch' }))
|
|
898
|
+
video.click()
|
|
899
|
+
await nextTick()
|
|
900
|
+
expect(muteShown(wrapper)).toBe(true)
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
it('without showMute is play/pause alone: hover, focus and taps work on it and nothing else', async () => {
|
|
904
|
+
vi.useFakeTimers()
|
|
905
|
+
const { wrapper, video, play, fields } = mountControls(
|
|
906
|
+
{ variant: 'toggle', name: 'Rondleiding', showMute: false },
|
|
907
|
+
{ paused: false },
|
|
908
|
+
)
|
|
909
|
+
await nextTick()
|
|
910
|
+
const toggle = wrapper.get('button[aria-label="Pauzeren: Rondleiding"]')
|
|
911
|
+
expect(wrapper.findAll('button')).toHaveLength(1)
|
|
912
|
+
expect(corner(wrapper).element.children).toHaveLength(1)
|
|
913
|
+
|
|
914
|
+
// Hover and focus bring no mute button and leave play/pause as it was
|
|
915
|
+
host.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }))
|
|
916
|
+
tabTo(toggle.element)
|
|
917
|
+
await nextTick()
|
|
918
|
+
expect(toggle.element).toBe(document.activeElement)
|
|
919
|
+
expect(wrapper.findAll('button')).toHaveLength(1)
|
|
920
|
+
;(toggle.element as HTMLButtonElement).blur()
|
|
921
|
+
host.dispatchEvent(new PointerEvent('pointerleave'))
|
|
922
|
+
vi.advanceTimersByTime(2500)
|
|
923
|
+
await nextTick()
|
|
924
|
+
expect(shownCorner(wrapper)).toBe(false)
|
|
925
|
+
|
|
926
|
+
// A tap brings the corner back while playing and a second one hides it: no dead state
|
|
927
|
+
const tap = () => {
|
|
928
|
+
video.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'touch' }))
|
|
929
|
+
video.click()
|
|
930
|
+
}
|
|
931
|
+
tap()
|
|
932
|
+
await nextTick()
|
|
933
|
+
expect(shownCorner(wrapper)).toBe(true)
|
|
934
|
+
expect(wrapper.findAll('button')).toHaveLength(1)
|
|
935
|
+
tap()
|
|
936
|
+
await nextTick()
|
|
937
|
+
expect(shownCorner(wrapper)).toBe(false)
|
|
938
|
+
tap()
|
|
939
|
+
vi.advanceTimersByTime(2500)
|
|
940
|
+
await nextTick()
|
|
941
|
+
expect(shownCorner(wrapper)).toBe(false)
|
|
942
|
+
|
|
943
|
+
expect(play).not.toHaveBeenCalled()
|
|
944
|
+
await toggle.trigger('click')
|
|
945
|
+
expect(fields.paused).toBe(true)
|
|
946
|
+
expect(fields.muted).toBe(false)
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
it('with showMute off leaves the full bar as it is', async () => {
|
|
950
|
+
const { wrapper } = mountControls({ name: 'Rondleiding', showMute: false })
|
|
951
|
+
await nextTick()
|
|
952
|
+
expect(wrapper.find('button[aria-label="Geluid uit"]').exists()).toBe(true)
|
|
953
|
+
})
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
describe('full screen', () => {
|
|
957
|
+
it('has no button without a full-screen target', async () => {
|
|
958
|
+
const { wrapper } = mountControls()
|
|
959
|
+
await nextTick()
|
|
960
|
+
expect(wrapper.find('button[aria-label="Volledig scherm"]').exists()).toBe(false)
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
it('falls back to the native player where the root cannot go full screen', async () => {
|
|
964
|
+
const fake = fakeVideo()
|
|
965
|
+
const webkitEnterFullscreen = vi.fn()
|
|
966
|
+
Object.assign(fake.video, { webkitEnterFullscreen })
|
|
967
|
+
const root = document.createElement('div')
|
|
968
|
+
root.append(fake.video)
|
|
969
|
+
document.body.append(root)
|
|
970
|
+
host = root
|
|
971
|
+
|
|
972
|
+
wrapper = mount(VideoControls, {
|
|
973
|
+
props: { target: fake.video, fullscreenTarget: root },
|
|
974
|
+
attachTo: root.appendChild(document.createElement('div')),
|
|
975
|
+
})
|
|
976
|
+
await nextTick()
|
|
977
|
+
|
|
978
|
+
await wrapper.get('button[aria-label="Volledig scherm"]').trigger('click')
|
|
979
|
+
expect(webkitEnterFullscreen).toHaveBeenCalledOnce()
|
|
980
|
+
})
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
describe('native full screen (iOS)', () => {
|
|
984
|
+
/** A video whose native player refuses until the metadata is in, as iOS does. */
|
|
985
|
+
const mountRefusing = (refuseAlways = false) => {
|
|
986
|
+
const fake = fakeVideo()
|
|
987
|
+
let readyState = 0
|
|
988
|
+
Object.defineProperty(fake.video, 'readyState', { get: () => readyState })
|
|
989
|
+
const webkitEnterFullscreen = vi.fn(() => {
|
|
990
|
+
if (refuseAlways || readyState < 1)
|
|
991
|
+
throw new DOMException('No metadata', 'InvalidStateError')
|
|
992
|
+
})
|
|
993
|
+
const load = vi.fn(() => {
|
|
994
|
+
readyState = 1
|
|
995
|
+
fake.video.dispatchEvent(new Event('loadedmetadata'))
|
|
996
|
+
})
|
|
997
|
+
Object.assign(fake.video, { webkitEnterFullscreen, load })
|
|
998
|
+
const root = document.createElement('div')
|
|
999
|
+
root.append(fake.video)
|
|
1000
|
+
document.body.append(root)
|
|
1001
|
+
host = root
|
|
1002
|
+
wrapper = mount(VideoControls, {
|
|
1003
|
+
props: { target: fake.video, fullscreenTarget: root },
|
|
1004
|
+
attachTo: root.appendChild(document.createElement('div')),
|
|
1005
|
+
})
|
|
1006
|
+
return { webkitEnterFullscreen, load }
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
it('loads the metadata after a refusal and tries once more', async () => {
|
|
1010
|
+
const { webkitEnterFullscreen, load } = mountRefusing()
|
|
1011
|
+
await nextTick()
|
|
1012
|
+
|
|
1013
|
+
await wrapper!.get('button[aria-label="Volledig scherm"]').trigger('click')
|
|
1014
|
+
await flushPromises()
|
|
1015
|
+
|
|
1016
|
+
expect(load).toHaveBeenCalledOnce()
|
|
1017
|
+
expect(webkitEnterFullscreen).toHaveBeenCalledTimes(2)
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
it('swallows a second refusal', async () => {
|
|
1021
|
+
const { webkitEnterFullscreen } = mountRefusing(true)
|
|
1022
|
+
await nextTick()
|
|
1023
|
+
|
|
1024
|
+
await wrapper!.get('button[aria-label="Volledig scherm"]').trigger('click')
|
|
1025
|
+
await flushPromises()
|
|
1026
|
+
|
|
1027
|
+
expect(webkitEnterFullscreen).toHaveBeenCalledTimes(2)
|
|
1028
|
+
})
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
describe('expose', () => {
|
|
1032
|
+
it('focus() moves the focus to the play button; show() brings the bar back', async () => {
|
|
1033
|
+
vi.useFakeTimers()
|
|
1034
|
+
const fake = fakeVideo({ paused: false })
|
|
1035
|
+
host = document.createElement('div')
|
|
1036
|
+
host.append(fake.video)
|
|
1037
|
+
document.body.append(host)
|
|
1038
|
+
|
|
1039
|
+
const controls = ref<InstanceType<typeof VideoControls> | null>(null)
|
|
1040
|
+
const Parent = defineComponent({
|
|
1041
|
+
setup: () => () => h(VideoControls, { ref: controls, target: fake.video }),
|
|
1042
|
+
})
|
|
1043
|
+
wrapper = mount(Parent, { attachTo: host.appendChild(document.createElement('div')) })
|
|
1044
|
+
vi.advanceTimersByTime(2500)
|
|
1045
|
+
await nextTick()
|
|
1046
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-0')
|
|
1047
|
+
|
|
1048
|
+
controls.value?.show()
|
|
1049
|
+
await nextTick()
|
|
1050
|
+
expect(wrapper.get('[role="group"]').classes()).toContain('opacity-100')
|
|
1051
|
+
|
|
1052
|
+
controls.value?.focus()
|
|
1053
|
+
expect(document.activeElement?.getAttribute('aria-label')).toBe('Pauzeren')
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
})
|