Fix bug: Improve code to use ref and test units

This commit is contained in:
Michael H. Arieli 2018-11-29 17:47:29 -08:00
parent 4e6657f19a
commit b91a1f2f81
9 changed files with 166 additions and 60 deletions

View File

@ -4,7 +4,18 @@ export const createObjectURL = jest.fn()
.mockImplementation(object => 'blob://' + String(object)) .mockImplementation(object => 'blob://' + String(object))
export const revokeObjectURL = jest.fn() export const revokeObjectURL = jest.fn()
export class MediaStream {} export class MediaStream {
getVideoTracks () {
return [{
enabled: true
}]
}
getAudioTracks () {
return [{
enabled: true
}]
}
}
export function getUserMedia () { export function getUserMedia () {
return !getUserMedia.shouldFail return !getUserMedia.shouldFail
? Promise.resolve(getUserMedia.stream) ? Promise.resolve(getUserMedia.stream)

View File

@ -23,6 +23,12 @@ export default class App extends React.PureComponent {
streams: PropTypes.objectOf(StreamPropType).isRequired, streams: PropTypes.objectOf(StreamPropType).isRequired,
toggleActive: PropTypes.func.isRequired toggleActive: PropTypes.func.isRequired
} }
constructor () {
super()
this.state = {
videos: {}
}
}
componentDidMount () { componentDidMount () {
const { init } = this.props const { init } = this.props
init() init()
@ -41,17 +47,29 @@ export default class App extends React.PureComponent {
streams streams
} = this.props } = this.props
const { videos } = this.state
return ( return (
<div className="app"> <div className="app">
<Toolbar messages={messages} stream={streams[constants.ME]} /> <Toolbar
chatRef={this.chatRef}
messages={messages}
stream={streams[constants.ME]}
ref={node => { this.toolbarRef = node }}
/>
<Alerts alerts={alerts} dismiss={dismissAlert} /> <Alerts alerts={alerts} dismiss={dismissAlert} />
<Notifications notifications={notifications} /> <Notifications notifications={notifications} />
<div id="chat"> <div id="chat" ref={node => { this.chatRef = node }}>
<Chat messages={messages} /> <Chat toolbarRef={this.toolbarRef} messages={messages} />
<Input notify={notify} sendMessage={sendMessage} /> <Input
videos={videos}
notify={notify}
sendMessage={sendMessage}
/>
</div> </div>
<div className="videos"> <div className="videos">
<Video <Video
videos={videos}
active={active === constants.ME} active={active === constants.ME}
onClick={toggleActive} onClick={toggleActive}
stream={streams[constants.ME]} stream={streams[constants.ME]}

View File

@ -10,42 +10,15 @@ export const MessagePropTypes = PropTypes.shape({
export default class Chat extends React.PureComponent { export default class Chat extends React.PureComponent {
static propTypes = { static propTypes = {
toolbarRef: PropTypes.object.isRequired,
messages: PropTypes.arrayOf(MessagePropTypes).isRequired messages: PropTypes.arrayOf(MessagePropTypes).isRequired
} }
handleCloseChat = e => { handleCloseChat = e => {
document.getElementById('chat').classList.remove('show') const { toolbarRef } = this.props
document.querySelector('.toolbar .chat').classList.remove('on') toolbarRef.chatButton.click()
} }
scrollToBottom = () => { scrollToBottom = () => {
// this.chatScroll.scrollTop = this.chatScroll.scrollHeight this.chatScroll.scrollTop = this.chatScroll.scrollHeight
const duration = 300
const start = this.chatScroll.scrollTop
const end = this.chatScroll.scrollHeight
const change = end - start
const increment = 20
const easeInOut = (currentTime, start, change, duration) => {
currentTime /= duration / 2
if (currentTime < 1) {
return change / 2 * currentTime * currentTime + start
}
currentTime -= 1
return -change / 2 * (currentTime * (currentTime - 2) - 1) + start
}
const animate = elapsedTime => {
elapsedTime += increment
const position = easeInOut(elapsedTime, start, change, duration)
this.chatScroll.scrollTop = position
if (elapsedTime < duration) {
setTimeout(() => {
animate(elapsedTime)
}, increment)
}
}
animate(0)
} }
componentDidMount () { componentDidMount () {
this.scrollToBottom() this.scrollToBottom()

View File

@ -4,6 +4,7 @@ import socket from '../socket.js'
export default class Input extends React.PureComponent { export default class Input extends React.PureComponent {
static propTypes = { static propTypes = {
videos: PropTypes.object.isRequired,
notify: PropTypes.func.isRequired, notify: PropTypes.func.isRequired,
sendMessage: PropTypes.func.isRequired sendMessage: PropTypes.func.isRequired
} }
@ -29,7 +30,7 @@ export default class Input extends React.PureComponent {
} }
} }
submit = () => { submit = () => {
const { notify, sendMessage } = this.props const { videos, notify, sendMessage } = this.props
const { message } = this.state const { message } = this.state
if (message) { if (message) {
notify('You: ' + message) notify('You: ' + message)
@ -41,13 +42,15 @@ export default class Input extends React.PureComponent {
// take snapshoot // take snapshoot
try { try {
const video = document.getElementById(`video-${userId}`) const video = videos[userId]
const canvas = document.createElement('canvas') if (video) {
canvas.height = video.videoHeight const canvas = document.createElement('canvas')
canvas.width = video.videoWidth canvas.height = video.videoHeight
const avatar = canvas.getContext('2d') canvas.width = video.videoWidth
avatar.drawImage(video, 0, 0, canvas.width, canvas.height) const avatar = canvas.getContext('2d')
image = canvas.toDataURL() avatar.drawImage(video, 0, 0, canvas.width, canvas.height)
image = canvas.toDataURL()
}
} catch (e) {} } catch (e) {}
const payload = { userId, message, timestamp, image } const payload = { userId, message, timestamp, image }

View File

@ -6,6 +6,7 @@ import { StreamPropType } from './Video.js'
export default class Toolbar extends React.PureComponent { export default class Toolbar extends React.PureComponent {
static propTypes = { static propTypes = {
chatRef: PropTypes.object.isRequired,
messages: PropTypes.arrayOf(MessagePropTypes).isRequired, messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
stream: StreamPropType stream: StreamPropType
} }
@ -16,36 +17,36 @@ export default class Toolbar extends React.PureComponent {
totalMessages: 0 totalMessages: 0
} }
} }
handleChatClick = e => { handleChatClick = () => {
const { messages } = this.props const { chatRef, messages } = this.props
document.getElementById('chat').classList.toggle('show') chatRef.classList.toggle('show')
e.currentTarget.classList.toggle('on') this.chatButton.classList.toggle('on')
this.setState({ this.setState({
isChatOpen: document.getElementById('chat').classList.contains('show'), isChatOpen: chatRef.classList.contains('show'),
totalMessages: messages.length totalMessages: messages.length
}) })
} }
handleMicClick = e => { handleMicClick = () => {
const { stream } = this.props const { stream } = this.props
stream.mediaStream.getAudioTracks().forEach(track => { stream.mediaStream.getAudioTracks().forEach(track => {
track.enabled = !track.enabled track.enabled = !track.enabled
}) })
e.currentTarget.classList.toggle('on') this.mixButton.classList.toggle('on')
} }
handleCamClick = e => { handleCamClick = () => {
const { stream } = this.props const { stream } = this.props
stream.mediaStream.getVideoTracks().forEach(track => { stream.mediaStream.getVideoTracks().forEach(track => {
track.enabled = !track.enabled track.enabled = !track.enabled
}) })
e.currentTarget.classList.toggle('on') this.camButton.classList.toggle('on')
} }
handleFullscreenClick = e => { handleFullscreenClick = () => {
if (screenfull.enabled) { if (screenfull.enabled) {
screenfull.toggle(e.target) screenfull.toggle(this.fullscreenButton)
e.currentTarget.classList.toggle('on') this.fullscreenButton.classList.toggle('on')
} }
} }
handleHangoutClick = e => { handleHangoutClick = () => {
window.location.href = '/' window.location.href = '/'
} }
render () { render () {
@ -55,6 +56,7 @@ export default class Toolbar extends React.PureComponent {
return ( return (
<div className="toolbar active"> <div className="toolbar active">
<div onClick={this.handleChatClick} <div onClick={this.handleChatClick}
ref={node => { this.chatButton = node }}
className="button chat" className="button chat"
data-blink={messages.length !== totalMessages && !isChatOpen} data-blink={messages.length !== totalMessages && !isChatOpen}
title="Chat" title="Chat"
@ -65,6 +67,7 @@ export default class Toolbar extends React.PureComponent {
{stream && ( {stream && (
<div> <div>
<div onClick={this.handleMicClick} <div onClick={this.handleMicClick}
ref={node => { this.mixButton = node }}
className="button mute-audio" className="button mute-audio"
title="Mute audio" title="Mute audio"
> >
@ -72,6 +75,7 @@ export default class Toolbar extends React.PureComponent {
<span className="off icon icon-mic" /> <span className="off icon icon-mic" />
</div> </div>
<div onClick={this.handleCamClick} <div onClick={this.handleCamClick}
ref={node => { this.camButton = node }}
className="button mute-video" className="button mute-video"
title="Mute video" title="Mute video"
> >
@ -82,6 +86,7 @@ export default class Toolbar extends React.PureComponent {
)} )}
<div onClick={this.handleFullscreenClick} <div onClick={this.handleFullscreenClick}
ref={node => { this.fullscreenButton = node }}
className="button fullscreen" className="button fullscreen"
title="Enter fullscreen" title="Enter fullscreen"
> >

View File

@ -11,6 +11,7 @@ export const StreamPropType = PropTypes.shape({
export default class Video extends React.PureComponent { export default class Video extends React.PureComponent {
static propTypes = { static propTypes = {
videos: PropTypes.object.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
active: PropTypes.bool.isRequired, active: PropTypes.bool.isRequired,
stream: StreamPropType, stream: StreamPropType,
@ -29,7 +30,7 @@ export default class Video extends React.PureComponent {
this.componentDidUpdate() this.componentDidUpdate()
} }
componentDidUpdate () { componentDidUpdate () {
const { stream } = this.props const { videos, stream } = this.props
const { video } = this.refs const { video } = this.refs
const mediaStream = stream && stream.mediaStream const mediaStream = stream && stream.mediaStream
const url = stream && stream.url const url = stream && stream.url
@ -40,6 +41,9 @@ export default class Video extends React.PureComponent {
} else if (video.src !== url) { } else if (video.src !== url) {
video.src = url video.src = url
} }
if (socket.id) {
videos[socket.id] = video
}
} }
render () { render () {
const { active } = this.props const { active } = this.props

View File

@ -5,12 +5,14 @@ import TestUtils from 'react-dom/test-utils'
describe('components/Input', () => { describe('components/Input', () => {
let component, node, notify, sendMessage let component, node, videos, notify, sendMessage
function render () { function render () {
videos = {}
notify = jest.fn() notify = jest.fn()
sendMessage = jest.fn() sendMessage = jest.fn()
component = TestUtils.renderIntoDocument( component = TestUtils.renderIntoDocument(
<Input <Input
videos={videos}
sendMessage={sendMessage} sendMessage={sendMessage}
notify={notify} notify={notify}
/> />

View File

@ -0,0 +1,87 @@
jest.mock('../../window.js')
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import Toolbar from '../Toolbar.js'
import { MediaStream } from '../../window.js'
describe('components/Video', () => {
class ToolbarWrapper extends React.PureComponent {
static propTypes = Toolbar.propTypes
constructor () {
super()
this.state = {}
}
render () {
return <Toolbar
chatRef={this.props.chatRef}
messages={this.props.messages}
stream={this.state.stream || this.props.stream}
/>
}
}
let component, node, chatRef, mediaStream, url
function render () {
mediaStream = new MediaStream()
chatRef = ReactDOM.findDOMNode(
TestUtils.renderIntoDocument(<div />)
)
component = TestUtils.renderIntoDocument(
<ToolbarWrapper
chatRef={chatRef}
messages={[]}
stream={{ mediaStream, url }}
/>
)
node = ReactDOM.findDOMNode(component)
}
describe('render', () => {
it('should not fail', () => {
render()
})
})
describe('handleChatClick', () => {
it('toggle chat', () => {
const button = node.querySelector('.chat')
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(true)
})
})
describe('handleMicClick', () => {
it('toggle mic', () => {
const button = node.querySelector('.mute-audio')
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(true)
})
})
describe('handleCamClick', () => {
it('toggle cam', () => {
const button = node.querySelector('.mute-video')
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(true)
})
})
describe('handleFullscreenClick', () => {
it('toggle fullscreen', () => {
const button = node.querySelector('.fullscreen')
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(false)
})
})
describe('handleHangoutClick', () => {
it('hangout', () => {
const button = node.querySelector('.hangup')
TestUtils.Simulate.click(button)
expect(window.location.href).toBe('http://localhost/')
})
})
})

View File

@ -14,6 +14,7 @@ describe('components/Video', () => {
} }
render () { render () {
return <Video return <Video
videos={this.props.videos}
active={this.props.active} active={this.props.active}
stream={this.state.stream || this.props.stream} stream={this.state.stream || this.props.stream}
onClick={this.props.onClick} onClick={this.props.onClick}
@ -22,12 +23,14 @@ describe('components/Video', () => {
} }
} }
let component, video, onClick, mediaStream, url let component, videos, video, onClick, mediaStream, url
function render () { function render () {
videos = {}
onClick = jest.fn() onClick = jest.fn()
mediaStream = new MediaStream() mediaStream = new MediaStream()
component = TestUtils.renderIntoDocument( component = TestUtils.renderIntoDocument(
<VideoWrapper <VideoWrapper
videos={videos}
active active
stream={{ mediaStream, url }} stream={{ mediaStream, url }}
onClick={onClick} onClick={onClick}