Skip to content

Commit

Permalink
✨ Preact VideoIframe (#30359)
Browse files Browse the repository at this point in the history
* ✨ Preact VideoIframe

* Remove need to run gulp along storybook

* Address review, add loading=lazy

* JSON.stringify

* Fix overridden

* missing dep

* makeMethodMessage

* @template T

* type

* base tests

* test message from imperative handle

* test unmount unlisten

* use handlemessage ref

* ref test

* no effect

* type typo

* unused

* CustomEvent

* createCustomEvent util
  • Loading branch information
alanorozco committed Nov 5, 2020
1 parent 7cf8a4a commit bd78eea
Show file tree
Hide file tree
Showing 4 changed files with 495 additions and 0 deletions.
108 changes: 108 additions & 0 deletions extensions/amp-video/1.0/storybook/VideoIframe.js
@@ -0,0 +1,108 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Preact from '../../../../src/preact';
import {VideoIframe} from '../video-iframe';
import {VideoWrapper} from '../video-wrapper';
import {boolean, text, withKnobs} from '@storybook/addon-knobs';
import {createCustomEvent} from '../../../../src/event-helper';
import {useCallback} from '../../../../src/preact';
import {withA11y} from '@storybook/addon-a11y';

export default {
title: 'VideoIframe',
component: VideoIframe,
decorators: [withA11y, withKnobs],
};

const AmpVideoIframeLike = (props) => {
const onMessage = useCallback((e) => {
// Expect HTMLMediaElement events from document in `src` as
// `{event: 'playing'}`
// (video-iframe-integration-v0.js talks similarly to HTMLMediaElement,
// so amp-video-iframe samples already mostly work).
if (e.data?.event) {
e.currentTarget.dispatchEvent(
createCustomEvent(window, e.data.event, /* detail */ null, {
bubbles: true,
cancelable: true,
})
);
}
}, []);

const makeMethodMessage = useCallback(
(method) =>
JSON.stringify({
// Like amp-video-iframe
'event': 'method',
'method': method.toLowerCase(),
}),
[]
);

return (
<VideoWrapper
{...props}
component={VideoIframe}
allow="autoplay" // this is not safe for a generic frame
onMessage={onMessage}
makeMethodMessage={makeMethodMessage}
/>
);
};

export const UsingVideoIframe = () => {
const width = text('width', '640px');
const height = text('height', '360px');

const ariaLabel = text('aria-label', 'Video Player');
const autoplay = boolean('autoplay', true);
const controls = boolean('controls', true);
const mediasession = boolean('mediasession', true);
const noaudio = boolean('noaudio', false);
const loop = boolean('loop', false);
const poster = text(
'poster',
'https://amp.dev/static/samples/img/amp-video-iframe-sample-placeholder.jpg'
);

const artist = text('artist', '');
const album = text('album', '');
const artwork = text('artwork', '');
const title = text('title', '');

const src = text(
'src',
'https://amp.dev/static/samples/files/amp-video-iframe-videojs.html'
);
return (
<AmpVideoIframeLike
ariaLabel={ariaLabel}
autoplay={autoplay}
controls={controls}
mediasession={mediasession}
noaudio={noaudio}
loop={loop}
poster={poster}
artist={artist}
album={album}
artwork={artwork}
title={title}
style={{width, height}}
src={src}
/>
);
};
190 changes: 190 additions & 0 deletions extensions/amp-video/1.0/test/test-video-iframe.js
@@ -0,0 +1,190 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Preact from '../../../../src/preact';
import {VideoIframe} from '../video-iframe';
import {mount} from 'enzyme';

function dispatchMessage(window, opt_event) {
const event = window.document.createEvent('Event');
event.initEvent('message', /* bubbles */ true, /* cancelable */ true);
window.dispatchEvent(Object.assign(event, opt_event));
}

describes.sandboxed('VideoIframe Preact component', {}, (env) => {
beforeEach(() => {});

it('unmutes per lack of `muted` prop', async () => {
const makeMethodMessage = env.sandbox.spy();
const videoIframe = mount(
<VideoIframe src="about:blank" makeMethodMessage={makeMethodMessage} />,
{attachTo: document.body}
);

await videoIframe.find('iframe').invoke('onCanPlay')();

expect(makeMethodMessage.withArgs('unmute')).to.have.been.calledOnce;
});

it('mutes per `muted` prop', async () => {
const makeMethodMessage = env.sandbox.spy();
const videoIframe = mount(
<VideoIframe
src="about:blank"
makeMethodMessage={makeMethodMessage}
muted
/>,
{attachTo: document.body}
);

await videoIframe.find('iframe').invoke('onCanPlay')();

expect(makeMethodMessage.withArgs('mute')).to.have.been.calledOnce;
});

it('hides controls per lack of `controls` prop', async () => {
const makeMethodMessage = env.sandbox.spy();
const videoIframe = mount(
<VideoIframe src="about:blank" makeMethodMessage={makeMethodMessage} />,
{attachTo: document.body}
);

await videoIframe.find('iframe').invoke('onCanPlay')();

expect(makeMethodMessage.withArgs('hideControls')).to.have.been.calledOnce;
});

it('shows controls per `controls` prop', async () => {
const makeMethodMessage = env.sandbox.spy();
const videoIframe = mount(
<VideoIframe
src="about:blank"
makeMethodMessage={makeMethodMessage}
controls
/>,
{attachTo: document.body}
);

await videoIframe.find('iframe').invoke('onCanPlay')();

expect(makeMethodMessage.withArgs('showControls')).to.have.been.calledOnce;
});

it('passes messages to onMessage', async () => {
const onMessage = env.sandbox.spy();
const videoIframe = mount(
<VideoIframe src="about:blank" onMessage={onMessage} controls />,
{attachTo: document.body}
);

const iframe = videoIframe.getDOMNode();

const data = {foo: 'bar'};
dispatchMessage(window, {source: iframe.contentWindow, data});

expect(
onMessage.withArgs(
env.sandbox.match({
currentTarget: iframe,
target: iframe,
data,
})
)
).to.have.been.calledOnce;
});

it("ignores messages if source doesn't match iframe", async () => {
const onMessage = env.sandbox.spy();
mount(<VideoIframe src="about:blank" onMessage={onMessage} controls />, {
attachTo: document.body,
});
dispatchMessage(window, {source: null, data: 'whatever'});
expect(onMessage).to.not.have.been.called;
});

it('stops listening to messages on unmount', async () => {
const onMessage = env.sandbox.spy();
const videoIframe = mount(
<VideoIframe src="about:blank" onMessage={onMessage} />,
{attachTo: document.body}
);
const iframe = videoIframe.getDOMNode();
videoIframe.unmount();
dispatchMessage(window, {source: iframe.contentWindow, data: 'whatever'});
expect(onMessage).to.not.have.been.called;
});

it('unlistens only when unmounted', async () => {
const addEventListener = env.sandbox.stub(window, 'addEventListener');
const removeEventListener = env.sandbox.stub(window, 'removeEventListener');

const videoIframe = mount(
<VideoIframe src="about:blank" onMessage={() => {}} />,
{attachTo: document.body}
);

expect(addEventListener.withArgs('message')).to.have.been.calledOnce;
expect(removeEventListener.withArgs('message')).to.not.have.been.called;

videoIframe.setProps({
onMessage: () => {
// An unstable onMessage prop should not cause unlisten
},
});
videoIframe.update();
expect(removeEventListener.withArgs('message')).to.not.have.been.called;

videoIframe.unmount();
expect(removeEventListener.withArgs('message')).to.have.been.calledOnce;
});

describe('uses makeMethodMessage to posts imperative handle methods', () => {
['play', 'pause'].forEach((method) => {
it(`with \`${method}\``, async () => {
let videoIframeRef;

const makeMethodMessage = (method) => ({makeMethodMessageFor: method});

const makeMethodMessageSpy = env.sandbox.spy(makeMethodMessage);

const videoIframe = mount(
<VideoIframe
ref={(ref) => (videoIframeRef = ref)}
src="about:blank"
makeMethodMessage={makeMethodMessageSpy}
/>,
{attachTo: document.body}
);

const postMessage = env.sandbox.stub(
videoIframe.getDOMNode().contentWindow,
'postMessage'
);

videoIframeRef[method]();
await videoIframe.find('iframe').invoke('onCanPlay')();

expect(makeMethodMessageSpy.withArgs(method)).to.have.been.calledOnce;
expect(
postMessage.withArgs(
env.sandbox.match(makeMethodMessage(method)),
'*'
)
).to.have.been.calledOnce;
});
});
});
});

0 comments on commit bd78eea

Please sign in to comment.