Skip to content

Commit 22ebecf

Browse files
charliesantostimmydoza
andauthored
AHOYAPPS-649-650 | Audio device tests (#36)
* AHOYAPPS-649-650 | Audio device tests * Fixing tests * Adding tests * Adding more tests * Update readme * Update usage of clsx utility Co-authored-by: timmydoza <tmendoza@twilio.com> * Move ProgressBar to common * Moving styles inside AudioDevice component * Use warning when Low audio levels are detected * Make test more readable * Update test names to make it more clear. Co-authored-by: timmydoza <tmendoza@twilio.com> * Fix test * Adding margin bottom for mobile * Adding onDeviceChange tests * Fixing lint * Update test * Fixing lint * Cleanup Co-authored-by: timmydoza <tmendoza@twilio.com>
1 parent 31a0618 commit 22ebecf

24 files changed

+1268
-19
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
⚠️ **Please note, this application is currently in beta. We welcome all feedback through Github issues**
2-
31
# Voice Diagnostics Tool
42

53
[![CircleCI](https://circleci.com/gh/twilio/rtc-diagnostics-react-app.svg?style=svg)](https://circleci.com/gh/twilio/rtc-diagnostics-react-app)
@@ -16,8 +14,8 @@ This application uses Programmable Voice and Twilio NTS to perform the tests and
1614
- Side by side comparison of Edge locations connection results
1715
- JSON formatted report
1816
- Easy Copy of report to clipboard
19-
- Interactive Mic testing (coming soon)
20-
- Interactive Speaker tests (coming soon)
17+
- Interactive Mic testing
18+
- Interactive Speaker tests
2119

2220
## Prerequisites
2321

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
2+
restoreMocks: true,
23
roots: ['<rootDir>/src'],
34
transform: {
45
'^.+\\.tsx?$': 'ts-jest',

src/App.test.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import React from 'react';
22
import App from './App';
33
import BrowserCompatibilityWidget from './BrowserCompatibilityWidget/BrowserCompatibilityWidget';
4+
import AudioDeviceTestWidget from './AudioDeviceTestWidget/AudioDeviceTestWidget';
45
import NetworkTestWidget from './NetworkTestWidget/NetworkTestWidget';
56
import { shallow } from 'enzyme';
67

78
let mockDevice = { isSupported: true };
89
jest.mock('twilio-client', () => ({
9-
// This is a getter to avoid the "Cannot access 'mockDevice' before initialization error"
10+
get Connection() {
11+
return { Codec: {PCMU: 'pcmu', Opus: 'opus'} }
12+
},
1013
get Device() {
1114
return mockDevice;
1215
},
@@ -31,6 +34,11 @@ describe('the App component', () => {
3134
const wrapper = shallow(<App />);
3235
expect(wrapper.find(NetworkTestWidget).exists()).toBe(true);
3336
});
37+
38+
it('should render the AudioDeviceTestWidget component', () => {
39+
const wrapper = shallow(<App />);
40+
expect(wrapper.find(AudioDeviceTestWidget).exists()).toBe(true);
41+
});
3442
});
3543

3644
describe('when the browser is not supported', () => {
@@ -45,5 +53,10 @@ describe('the App component', () => {
4553
const wrapper = shallow(<App />);
4654
expect(wrapper.find(NetworkTestWidget).exists()).toBe(false);
4755
});
56+
57+
it('should not render the AudioDeviceTestWidget component', () => {
58+
const wrapper = shallow(<App />);
59+
expect(wrapper.find(AudioDeviceTestWidget).exists()).toBe(false);
60+
});
4861
});
4962
});

src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import React, { useState } from 'react';
2+
import { getLogger } from 'loglevel';
23
import { AppBar, Container, Toolbar, Grid, Paper, CssBaseline, makeStyles, Typography } from '@material-ui/core';
4+
import AudioDeviceTestWidget from './AudioDeviceTestWidget/AudioDeviceTestWidget';
35
import BrowserCompatibilityWidget from './BrowserCompatibilityWidget/BrowserCompatibilityWidget';
46
import CopyResultsWidget from './CopyResultsWidget/CopyResultsWidget';
57
import { Device } from 'twilio-client';
68
import { getJSON } from './utils';
9+
import { APP_NAME, LOG_LEVEL } from './constants';
710
import NetworkTestWidget from './NetworkTestWidget/NetworkTestWidget';
811
import ResultWidget from './ResultWidget/ResultWidget';
912
import SummaryWidget from './SummaryWidget/SummaryWidget';
1013

14+
const log = getLogger(APP_NAME);
15+
log.setLevel(LOG_LEVEL);
16+
1117
const useStyles = makeStyles({
1218
container: {
1319
marginTop: '2em',
@@ -52,6 +58,11 @@ function App() {
5258
)}
5359
{Device.isSupported && (
5460
<>
61+
<Grid item xs={12}>
62+
<Paper className={classes.paper} elevation={3}>
63+
<AudioDeviceTestWidget/>
64+
</Paper>
65+
</Grid>
5566
<Grid item xs={12}>
5667
<Paper className={classes.paper} elevation={3}>
5768
<NetworkTestWidget
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React from 'react';
2+
import { Select, Typography } from '@material-ui/core';
3+
import { render } from '@testing-library/react';
4+
import { mount, shallow } from 'enzyme';
5+
6+
import AudioDevice from './AudioDevice';
7+
import ProgressBar from '../../common/ProgressBar/ProgressBar';
8+
9+
const mediaInfoProps = { groupId: 'foo', toJSON: () => {} };
10+
const mockDevices = [{
11+
deviceId: 'input1',
12+
label: 'deviceinput1',
13+
kind: 'audioinput',
14+
...mediaInfoProps,
15+
},{
16+
deviceId: 'output1',
17+
label: 'deviceoutput1',
18+
kind: 'audiooutput',
19+
...mediaInfoProps,
20+
}];
21+
22+
jest.mock('../useDevices/useDevices', () => ({
23+
useDevices: () => mockDevices,
24+
}));
25+
26+
describe('the AudioDevice component', () => {
27+
const noop = () => {};
28+
let originalAudio: any;
29+
let mockAudio: any;
30+
31+
beforeEach(() => {
32+
mockAudio = {
33+
prototype: {
34+
setSinkId: true
35+
}
36+
};
37+
originalAudio = global.Audio;
38+
global.Audio = mockAudio;
39+
});
40+
41+
afterEach(() => {
42+
global.Audio = originalAudio;
43+
});
44+
45+
it('should render default audio output if audio redirect is not supported', () => {
46+
mockAudio.prototype.setSinkId = false;
47+
const wrapper = shallow(<AudioDevice disabled={false} level={1} kind="audiooutput" onDeviceChange={noop} />);
48+
expect(wrapper.find(Select).exists()).toBeFalsy();
49+
expect(wrapper.find(Typography).at(2).text()).toEqual('System Default Audio Output');
50+
});
51+
52+
describe('props.disabled', () => {
53+
it('should disable dropdown if disabled=true', () => {
54+
const { container } = render(<AudioDevice disabled={true} level={1} kind="audioinput" onDeviceChange={noop} />);
55+
const el = container.querySelector('.MuiInputBase-root') as HTMLDivElement;
56+
expect(el.className.includes('Mui-disabled')).toBeTruthy();
57+
});
58+
it('should not disable dropdown if disabled=false', () => {
59+
const { container } = render(<AudioDevice disabled={false} level={1} kind="audioinput" onDeviceChange={noop} />);
60+
const el = container.querySelector('.MuiInputBase-root') as HTMLDivElement;
61+
expect(el.className.includes('Mui-disabled')).toBeFalsy();
62+
});
63+
});
64+
65+
describe('props.level', () => {
66+
it('should set progress to 0 if level is 0', () => {
67+
const wrapper = shallow(<AudioDevice disabled={false} level={1} kind="audioinput" onDeviceChange={noop} />);
68+
expect(wrapper.find(ProgressBar).prop('position')).toEqual(1);
69+
});
70+
it('should set progress to 20 if level is 20', () => {
71+
const wrapper = shallow(<AudioDevice disabled={false} level={20} kind="audioinput" onDeviceChange={noop} />);
72+
expect(wrapper.find(ProgressBar).prop('position')).toEqual(20);
73+
});
74+
});
75+
76+
describe('props.kind', () => {
77+
it('should render input devices if kind is audioinput', () => {
78+
const wrapper = shallow(<AudioDevice disabled={false} level={1} kind="audioinput" onDeviceChange={noop} />);
79+
expect(wrapper.find(Select).at(0).text()).toEqual('deviceinput1');
80+
});
81+
it('should render output devices if kind is audiooutput', () => {
82+
const wrapper = shallow(<AudioDevice disabled={false} level={1} kind="audiooutput" onDeviceChange={noop} />);
83+
expect(wrapper.find(Select).at(0).text()).toEqual('deviceoutput1');
84+
});
85+
});
86+
87+
describe('props.onDeviceChange', () => {
88+
let onDeviceChange: () => any;
89+
90+
beforeEach(() => {
91+
onDeviceChange = jest.fn();
92+
});
93+
94+
it('should trigger onDeviceChange when devices are present', () => {
95+
mount(<AudioDevice disabled={false} level={1} kind="audioinput" onDeviceChange={onDeviceChange} />);
96+
expect(onDeviceChange).toHaveBeenCalled();
97+
});
98+
99+
it('should trigger onDeviceChange when a new device is selected', () => {
100+
const wrapper = mount(<AudioDevice disabled={false} level={1} kind="audioinput" onDeviceChange={onDeviceChange} />);
101+
const selectEl = wrapper.find(Select);
102+
selectEl.simulate('change', 'input1');
103+
expect(onDeviceChange).toHaveBeenCalledWith('input1');
104+
});
105+
});
106+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import { FormControl, InputLabel, MenuItem, Select, Typography } from '@material-ui/core';
3+
import { makeStyles } from '@material-ui/core/styles';
4+
5+
import ProgressBar from '../../common/ProgressBar/ProgressBar';
6+
import { useDevices } from '../useDevices/useDevices';
7+
8+
const labels = {
9+
audioinput: {
10+
audioLevelText: 'Input level',
11+
deviceLabelHeader: 'Input device',
12+
headerText: 'Microphone',
13+
},
14+
audiooutput: {
15+
audioLevelText: 'Output level',
16+
deviceLabelHeader: 'Output device',
17+
headerText: 'Speaker',
18+
}
19+
};
20+
21+
const useStyles = makeStyles(() => ({
22+
audioLevelContainer: {
23+
display: 'flex',
24+
alignItems: 'center',
25+
},
26+
form: {
27+
margin: '1em 0',
28+
minWidth: 200,
29+
},
30+
deviceLabelContainer: {
31+
margin: '1em 0',
32+
'&> *': {
33+
marginBottom: '0.3em'
34+
}
35+
}
36+
}));
37+
38+
interface AudioDeviceProps {
39+
disabled: boolean;
40+
level: number;
41+
kind: 'audioinput' | 'audiooutput';
42+
onDeviceChange: (value: string) => void;
43+
}
44+
45+
export default function AudioDevice({ disabled, level, kind, onDeviceChange }: AudioDeviceProps) {
46+
const classes = useStyles();
47+
const devices = useDevices().filter(device => device.kind === kind);
48+
const [selectedDevice, setSelectedDevice] = useState('');
49+
50+
const { audioLevelText, deviceLabelHeader, headerText } = labels[kind];
51+
const noAudioRedirect = !Audio.prototype.setSinkId && kind === 'audiooutput';
52+
53+
const updateSelectedDevice = useCallback((value: string) => {
54+
onDeviceChange(value);
55+
setSelectedDevice(value);
56+
}, [onDeviceChange, setSelectedDevice]);
57+
58+
useEffect(() => {
59+
const hasSelectedDevice = devices.some((device) => device.deviceId === selectedDevice);
60+
if (devices.length && !hasSelectedDevice) {
61+
updateSelectedDevice(devices[0].deviceId);
62+
}
63+
}, [devices, selectedDevice, updateSelectedDevice]);
64+
65+
return (
66+
<div style={{ width: 'calc(50% - 1em)', minWidth: '300px', marginBottom: '30px' }}>
67+
<Typography variant="h5">{headerText}</Typography>
68+
69+
{noAudioRedirect && (
70+
<div className={classes.deviceLabelContainer}>
71+
<Typography variant="subtitle2">{deviceLabelHeader}</Typography>
72+
<Typography>System Default Audio Output</Typography>
73+
</div>
74+
)}
75+
76+
{!noAudioRedirect && (
77+
<FormControl disabled={disabled} variant="outlined" className={classes.form} fullWidth>
78+
<InputLabel>{deviceLabelHeader}</InputLabel>
79+
<Select
80+
label={deviceLabelHeader}
81+
value={selectedDevice}
82+
onChange={e => updateSelectedDevice(e.target.value as string)}
83+
>
84+
{devices.map((device) => (
85+
<MenuItem value={device.deviceId} key={device.deviceId}>
86+
{device.label}
87+
</MenuItem>
88+
))}
89+
</Select>
90+
</FormControl>
91+
)}
92+
93+
<div className={classes.audioLevelContainer}>
94+
<Typography variant="subtitle2" style={{ marginRight: '1em' }}>
95+
{audioLevelText}:
96+
</Typography>
97+
<ProgressBar
98+
position={level}
99+
duration={0.1}
100+
style={{ flex: '1', margin: '0' }}
101+
/>
102+
</div>
103+
</div>
104+
);
105+
}

0 commit comments

Comments
 (0)