diff --git a/src/client/components/Media.test.tsx b/src/client/components/Media.test.tsx new file mode 100644 index 0000000..7b6b3af --- /dev/null +++ b/src/client/components/Media.test.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import TestUtils from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { createStore, Store } from '../store' +import { Media } from './Media' +import { MEDIA_ENUMERATE } from '../constants' + +describe('Media', () => { + + const onSave = jest.fn() + beforeEach(() => { + jest.resetAllMocks() + store = createStore() + store.dispatch({ + type: MEDIA_ENUMERATE, + status: 'resolved', + payload: [{ + id: '123', + name: 'Audio Input', + type: 'audioinput', + }, { + id: '456', + label: 'Video Input', + name: 'videoinput', + }], + }) + }) + + let store: Store + async function render() { + const div = document.createElement('div') + const node = await new Promise(resolve => { + ReactDOM.render( +
resolve(div!)}> + + + +
, + div, + ) + }) + return node.children[0] + } + + describe('submit', () => { + it('calls onSave', async () => { + const node = await render() + expect(node.tagName).toBe('FORM') + TestUtils.Simulate.submit(node) + expect(onSave.mock.calls.length).toBe(1) + }) + }) + + describe('onVideoChange', () => { + it('calls onSetVideoConstraint', async () => { + const node = await render() + const select = node.querySelector('select.media-video')! + TestUtils.Simulate.change(select, { + target: { + value: '{"deviceId":123}', + } as any, + }) + expect(store.getState().media.video).toEqual({ deviceId: 123 }) + }) + }) + + describe('onAudioChange', () => { + it('calls onSetAudioConstraint', async () => { + const node = await render() + const select = node.querySelector('select.media-audio')! + TestUtils.Simulate.change(select, { + target: { + value: '{"deviceId":456}', + } as any, + }) + expect(store.getState().media.audio).toEqual({ deviceId: 456 }) + }) + }) + +}) diff --git a/src/client/components/Media.tsx b/src/client/components/Media.tsx new file mode 100644 index 0000000..1967745 --- /dev/null +++ b/src/client/components/Media.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import { connect } from 'react-redux' +import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, VideoConstraint } from '../actions/MediaActions' +import { MediaState } from '../reducers/media' +import { State } from '../store' + +export type MediaProps = MediaState & { + onSetVideoConstraint: typeof setVideoConstraint + onSetAudioConstraint: typeof setAudioConstraint + onSave: () => void +} + +function getId(constraint: VideoConstraint | AudioConstraint) { + return typeof constraint === 'object' && 'deviceId' in constraint + ? constraint.deviceId + : '' +} + +function mapStateToProps(state: State) { + return { + ...state.media, + } +} + +const mapDispatchToProps = { + onSetVideoConstraint: setVideoConstraint, + onSetAudioConstraint: setAudioConstraint, +} + +const c = connect(mapStateToProps, mapDispatchToProps) + +export const Media = c(React.memo(function Media(props: MediaProps) { + + function onSave(event: React.FormEvent) { + event.preventDefault() + props.onSave() + } + + function onVideoChange(event: React.ChangeEvent) { + const constraint: VideoConstraint = JSON.parse(event.target.value) + props.onSetVideoConstraint(constraint) + } + + function onAudioChange(event: React.ChangeEvent) { + const constraint: AudioConstraint = JSON.parse(event.target.value) + props.onSetAudioConstraint(constraint) + } + + const videoId = getId(props.video) + const audioId = getId(props.audio) + + return ( +
+ + + + + +
+ ) +})) + +interface OptionsProps { + devices: MediaDevice[] + type: 'audioinput' | 'videoinput' + default: string +} + +function Options(props: OptionsProps) { + return ( + + + + { + props.devices + .filter(device => device.type === props.type) + .map(device => + , + ) + } + + ) +}