Skip to content

Commit 7cf76ef

Browse files
author
Dobromir Hristov
authored
refactor: add ability to disable muting on ReplayableVideoAsset. (#406)
closes rdar://97715398
1 parent 787c2fb commit 7cf76ef

File tree

7 files changed

+164
-68
lines changed

7 files changed

+164
-68
lines changed

src/components/Asset.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export default {
5151
type: Boolean,
5252
default: () => true,
5353
},
54+
videoMuted: {
55+
type: Boolean,
56+
default: true,
57+
},
5458
},
5559
computed: {
5660
rawAsset() {
@@ -96,6 +100,7 @@ export default {
96100
return {
97101
variants: this.asset.variants,
98102
showsControls: this.showsVideoControls,
103+
muted: this.videoMuted,
99104
autoplays: this.prefersReducedMotion ? false : this.videoAutoplays,
100105
posterVariants: this.videoPoster ? this.videoPoster.variants : [],
101106
};

src/components/ReplayableVideoAsset.vue

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313
<VideoAsset
1414
ref="asset"
1515
:variants="variants"
16-
@ended="onVideoEnd"
17-
:showsControls="showsControls"
1816
:autoplays="autoplays"
17+
:showsControls="showsControls"
18+
:muted="muted"
19+
:posterVariants="posterVariants"
20+
@pause="onPause"
21+
@playing="onVideoPlaying"
22+
@ended="onVideoEnd"
1923
/>
2024
<a
2125
class="replay-button"
2226
href="#"
2327
:class="{ visible: this.showsReplayButton }"
2428
@click.prevent="replay"
2529
>
26-
Replay
30+
{{ text }}
2731
<InlineReplayIcon class="replay-icon icon-inline" />
2832
</a>
2933
</div>
@@ -52,10 +56,22 @@ export default {
5256
type: Boolean,
5357
default: () => true,
5458
},
59+
muted: {
60+
type: Boolean,
61+
default: true,
62+
},
63+
posterVariants: {
64+
type: Array,
65+
default: () => [],
66+
},
67+
},
68+
computed: {
69+
text: ({ played }) => (played ? 'Replay' : 'Play'),
5570
},
5671
data() {
5772
return {
58-
showsReplayButton: false,
73+
showsReplayButton: !(this.autoplays && this.muted),
74+
played: false,
5975
};
6076
},
6177
methods: {
@@ -68,6 +84,16 @@ export default {
6884
},
6985
onVideoEnd() {
7086
this.showsReplayButton = true;
87+
this.played = true;
88+
},
89+
onVideoPlaying() {
90+
this.showsReplayButton = false;
91+
},
92+
onPause() {
93+
// if the video pauses, and we are hiding the controls, show the replay button
94+
if (!this.showsControls && !this.showsReplayButton) {
95+
this.showsReplayButton = true;
96+
}
7197
},
7298
},
7399
};

src/components/VideoAsset.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
:controls="showsControls"
1414
:autoplay="autoplays"
1515
:poster="normalizeAssetUrl(defaultPosterAttributes.url)"
16-
muted
16+
:muted="muted"
1717
playsinline
1818
@playing="$emit('playing')"
19+
@pause="$emit('pause')"
1920
@ended="$emit('ended')"
2021
>
2122
<!--
@@ -53,6 +54,10 @@ export default {
5354
required: false,
5455
default: () => [],
5556
},
57+
muted: {
58+
type: Boolean,
59+
default: true,
60+
},
5661
},
5762
data: () => ({ appState: AppStore.state }),
5863
computed: {

tests/unit/components/Asset.spec.js

Lines changed: 58 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ import ImageAsset from 'docc-render/components/ImageAsset.vue';
1414
import VideoAsset from 'docc-render/components/VideoAsset.vue';
1515
import ReplayableVideoAsset from 'docc-render/components/ReplayableVideoAsset.vue';
1616

17+
const video = {
18+
type: 'video',
19+
poster: 'image',
20+
variants: [
21+
{
22+
url: 'foo.mp4',
23+
traits: ['2x'],
24+
size: {
25+
width: 42,
26+
height: 42,
27+
},
28+
},
29+
],
30+
};
31+
1732
describe('Asset', () => {
1833
beforeAll(() => {
1934
Object.defineProperty(window, 'matchMedia', {
@@ -60,20 +75,6 @@ describe('Asset', () => {
6075
});
6176

6277
it('renders a `VideoAsset` for video', () => {
63-
const foo = {
64-
type: 'video',
65-
poster: 'image',
66-
variants: [
67-
{
68-
url: 'foo.mp4',
69-
traits: ['2x'],
70-
size: {
71-
width: 42,
72-
height: 42,
73-
},
74-
},
75-
],
76-
};
7778
const image = {
7879
type: 'image',
7980
variants: [
@@ -83,10 +84,10 @@ describe('Asset', () => {
8384
},
8485
],
8586
};
86-
const wrapper = mountAsset('foo', { foo, image });
87+
const wrapper = mountAsset('video', { video, image });
8788

8889
const videoAsset = wrapper.find(VideoAsset);
89-
expect(videoAsset.props('variants')).toBe(foo.variants);
90+
expect(videoAsset.props('variants')).toBe(video.variants);
9091
expect(videoAsset.props('posterVariants')).toBe(image.variants);
9192
expect(videoAsset.props('showsControls')).toBe(true);
9293
expect(videoAsset.props('autoplays')).toBe(true);
@@ -97,68 +98,63 @@ describe('Asset', () => {
9798
});
9899

99100
it('renders a `VideoAsset` without poster variants', () => {
100-
const foo = {
101-
type: 'video',
102-
poster: 'image',
103-
variants: [
104-
{
105-
url: 'foo.mp4',
106-
traits: ['2x'],
107-
size: {
108-
width: 42,
109-
height: 42,
110-
},
111-
},
112-
],
113-
};
114-
const videoAsset = mountAsset('foo', { foo }).find(VideoAsset);
115-
expect(videoAsset.props('variants')).toBe(foo.variants);
101+
const videoAsset = mountAsset('video', { video }).find(VideoAsset);
102+
expect(videoAsset.props('variants')).toBe(video.variants);
116103
expect(videoAsset.props('posterVariants')).toEqual([]);
117104
});
118105

119106
it('renders a `ReplayableVideoAsset` for video with `showsReplayButton=true`', () => {
120-
const foo = {
121-
type: 'video',
122-
variants: [
123-
{
124-
url: 'foo.mp4',
125-
traits: ['2x'],
126-
size: {
127-
width: 42,
128-
height: 42,
129-
},
130-
},
131-
],
132-
};
133107
const wrapper = shallowMount(Asset, {
134108
propsData: {
135-
identifier: 'foo',
109+
identifier: 'video',
136110
showsReplayButton: true,
137111
showsVideoControls: true,
138112
},
139-
provide: { references: { foo } },
113+
provide: { references: { video } },
140114
});
141115

142116
const videoAsset = wrapper.find(ReplayableVideoAsset);
143-
expect(videoAsset.props('variants')).toBe(foo.variants);
117+
expect(videoAsset.props('variants')).toBe(video.variants);
144118
expect(videoAsset.props('showsControls')).toBe(true);
119+
expect(videoAsset.props('muted')).toBe(true);
120+
});
121+
122+
it('renders a `VideoAsset`, without muting it', () => {
123+
const wrapper = shallowMount(Asset, {
124+
propsData: {
125+
identifier: 'video',
126+
showsVideoControls: true,
127+
videoAutoplays: true,
128+
videoMuted: false,
129+
},
130+
provide: { references: { video } },
131+
});
132+
133+
expect(wrapper.find(ReplayableVideoAsset).exists()).toBe(false);
134+
const videoAsset = wrapper.find(VideoAsset);
135+
expect(videoAsset.props('variants')).toBe(video.variants);
136+
expect(videoAsset.props('showsControls')).toBe(true);
137+
expect(videoAsset.props('muted')).toBe(false);
138+
expect(videoAsset.props('autoplays')).toBe(true);
139+
});
140+
141+
it('renders a `ReplayableVideoAsset` without it being muted', () => {
142+
const wrapper = shallowMount(Asset, {
143+
propsData: {
144+
identifier: 'video',
145+
showsReplayButton: true,
146+
showsVideoControls: true,
147+
videoMuted: false,
148+
},
149+
provide: { references: { video } },
150+
});
151+
152+
const videoAsset = wrapper.find(ReplayableVideoAsset);
153+
expect(videoAsset.props('showsControls')).toBe(true);
154+
expect(videoAsset.props('muted')).toBe(false);
145155
});
146156

147157
describe('ReduceMotion aware', () => {
148-
const video = {
149-
type: 'video',
150-
poster: 'image',
151-
variants: [
152-
{
153-
url: 'foo.mp4',
154-
traits: ['2x'],
155-
size: {
156-
width: 42,
157-
height: 42,
158-
},
159-
},
160-
],
161-
};
162158
const image = {
163159
type: 'image',
164160
alt: 'blah',

tests/unit/components/ReplayableVideoAsset.spec.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import InlineReplayIcon from 'theme/components/Icons/InlineReplayIcon.vue';
1515
import { flushPromises } from '../../../test-utils';
1616

1717
const variants = [{ traits: ['dark', '1x'], url: 'https://www.example.com/myvideo.mp4' }];
18+
const posterVariants = [{ traits: ['dark', '1x'], url: 'https://www.example.com/image.jpg' }];
1819

1920
const propsData = {
2021
variants,
22+
posterVariants,
2123
};
2224
describe('ReplayableVideoAsset', () => {
2325
const mountWithProps = props => shallowMount(ReplayableVideoAsset, {
@@ -46,6 +48,7 @@ describe('ReplayableVideoAsset', () => {
4648

4749
const video = wrapper.find(VideoAsset);
4850
expect(video.props('variants')).toBe(variants);
51+
expect(video.props('posterVariants')).toBe(posterVariants);
4952
expect(video.props('showsControls')).toBe(true);
5053
expect(video.props('autoplays')).toBe(true);
5154
});
@@ -79,4 +82,53 @@ describe('ReplayableVideoAsset', () => {
7982
await flushPromises();
8083
expect(playMock).toHaveBeenCalledTimes(1);
8184
});
85+
86+
it('shows the Replay on first mount, if not set to autoplay', async () => {
87+
const wrapper = mountWithProps({
88+
autoplays: false,
89+
});
90+
const replay = wrapper.find('.replay-button');
91+
expect(replay.text()).toBe('Play');
92+
expect(replay.classes()).toContain('visible');
93+
replay.trigger('click');
94+
await flushPromises();
95+
// text is not changed, but its invisible
96+
expect(replay.text()).toBe('Play');
97+
expect(replay.classes()).not.toContain('visible');
98+
// now end the video
99+
wrapper.find({ ref: 'asset' }).trigger('ended');
100+
// assert text changed and its visible
101+
expect(replay.text()).toBe('Replay');
102+
expect(replay.classes()).toContain('visible');
103+
});
104+
105+
it('shows the Replay on `pause` if no `showsControls` is false', async () => {
106+
const wrapper = mountWithProps({
107+
autoplays: false,
108+
showsControls: false,
109+
});
110+
const replay = wrapper.find('.replay-button');
111+
expect(replay.classes()).toContain('visible');
112+
// play
113+
replay.trigger('click');
114+
await flushPromises();
115+
// assert button is hidden
116+
expect(replay.classes()).not.toContain('visible');
117+
// pause
118+
wrapper.find({ ref: 'asset' }).trigger('pause');
119+
// assert button is visible again
120+
expect(replay.classes()).toContain('visible');
121+
});
122+
123+
it('hides the button when @playing fired', () => {
124+
const wrapper = mountWithProps({
125+
autoplays: false,
126+
});
127+
const replay = wrapper.find('.replay-button');
128+
expect(replay.classes()).toContain('visible');
129+
// Simulate browser starts playing
130+
wrapper.find({ ref: 'asset' }).trigger('playing');
131+
// assert button is hidden
132+
expect(replay.classes()).not.toContain('visible');
133+
});
82134
});

tests/unit/components/Tutorial/SectionIntro.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ describe('SectionIntro', () => {
108108
showsReplayButton: true,
109109
showsVideoControls: false,
110110
videoAutoplays: true,
111+
videoMuted: true,
111112
});
112113
});
113114

tests/unit/components/VideoAsset.spec.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('VideoAsset', () => {
4040

4141
it('renders a video', () => {
4242
expect(wrapper.is('video')).toBe(true);
43+
expect(wrapper.element.muted).toBe(true);
4344
});
4445

4546
it('adds a poster to the `video`, using light by default', () => {
@@ -86,12 +87,15 @@ describe('VideoAsset', () => {
8687
expect(source.attributes('controls')).toBe(undefined);
8788
});
8889

89-
it('forwards `playing` and `ended` events', () => {
90+
it('forwards `playing`, `pause` and `ended` events', () => {
9091
const video = wrapper.find('video');
9192

9293
video.trigger('playing');
9394
expect(wrapper.emitted().playing.length).toBe(1);
9495

96+
video.trigger('pause');
97+
expect(wrapper.emitted().pause.length).toBe(1);
98+
9599
video.trigger('ended');
96100
expect(wrapper.emitted().ended.length).toBe(1);
97101
});
@@ -143,4 +147,11 @@ describe('VideoAsset', () => {
143147
expect(source.exists()).toBe(true);
144148
expect(source.attributes('src')).toBe(propsData.variants[1].url);
145149
});
150+
151+
it('renders a video as none-muted', () => {
152+
wrapper.setProps({
153+
muted: false,
154+
});
155+
expect(wrapper.element.muted).toBeFalsy();
156+
});
146157
});

0 commit comments

Comments
 (0)