1
0
mirror of https://github.com/pavlobu/deskreen.git synced 2025-05-28 05:10:09 -07:00

Merge pull request #14 from pavlobu/tests

Client Tests and Github Action fixes
This commit is contained in:
Pavlo Buidenkov 2020-12-24 23:27:40 +00:00 committed by GitHub
commit 7fb1114035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 9941 additions and 5883 deletions

View File

@ -8,54 +8,20 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v2.3.1
# - name: Install Node.js, NPM and Yarn
# env:
# ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
# uses: actions/setup-node@v1.4.2
# with:
# node-version: 14
# - name: yarn install from npmjs registry
# env:
# ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
# run: |
# npm config set registry https://registry.npmjs.org
# cd app/client
# yarn install --no-lockfile
# cd ..
# yarn install --no-lockfile
# cd ..
# yarn install --no-lockfile
- name: configure app/client for yarn cache
- name: install yarn dependencies in app/client using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./app/client
- name: configure project root for yarn cache
- name: install yarn dependencies in ./ using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./
- name: ./app/client yarn install
run: yarn install --frozen-lockfile
working-directory: ./app/client
- name: ./ yarn install
run: yarn install --frozen-lockfile
working-directory: ./
# - name: Configure private AWS npm registry and install packages from it
# env:
# ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
# if: ${{ failure() }}
# run: |
# npm config set registry https://packages.deskreen.com/
# npm set //packages.deskreen.com/:_authToken="${{ secrets.NPMRC_USER_TOKEN }}"
# npm config set always-auth true
# cd app/client
# yarn install --frozen-lockfile
# cd ../..
# yarn install --frozen-lockfile
- name: install yarn dependencies in ./app using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./app
- name: yarn build
run: yarn build

View File

@ -41,37 +41,52 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2.3.1
- name: Install Node.js, NPM and Yarn
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
uses: actions/setup-node@v1.4.2
- name: install yarn dependencies in app/client using cache
uses: bahmutov/npm-install@v1.6.0
with:
node-version: 14
working-directory: ./app/client
- name: yarn install from npmjs registry
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
run: |
npm config set registry https://registry.npmjs.org
cd app/client
yarn install --no-lockfile
cd ..
yarn install --no-lockfile
cd ..
yarn install --no-lockfile
- name: install yarn dependencies in ./ using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./
- name: Configure private AWS npm registry and install packages from it
- name: install yarn dependencies in ./app using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./app
- name: yarn build
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
if: ${{ failure() }}
run: |
npm config set registry https://packages.deskreen.com/
npm set //packages.deskreen.com/:_authToken="${{ secrets.NPMRC_USER_TOKEN }}"
npm config set always-auth true
cd app/client
yarn install --frozen-lockfile
cd ../..
yarn install --frozen-lockfile
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn build
- name: yarn lint
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn lint
- name: yarn tsc
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn tsc
- name: yarn test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn test
- name: yarn build-ux
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn build-ux
- name: yarn test-ux
uses: GabrielBB/xvfb-action@v1.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
run: yarn test-ux
- name: yarn package-ci
env:

View File

@ -16,37 +16,20 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v2.3.1
- name: Install Node.js, NPM and Yarn
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
uses: actions/setup-node@v1.4.2
- name: install yarn dependencies in app/client using cache
uses: bahmutov/npm-install@v1.6.0
with:
node-version: 14
working-directory: ./app/client
- name: yarn install from npmjs registry
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
run: |
npm config set registry https://registry.npmjs.org
cd app/client
yarn install --no-lockfile
cd ..
yarn install --no-lockfile
cd ..
yarn install --no-lockfile
- name: install yarn dependencies in ./ using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./
- name: Configure private AWS npm registry and install packages from it
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
if: ${{ failure() }}
run: |
npm config set registry https://packages.deskreen.com/
npm set //packages.deskreen.com/:_authToken="${{ secrets.NPMRC_USER_TOKEN }}"
npm config set always-auth true
cd app/client
yarn install --frozen-lockfile
cd ../..
yarn install --frozen-lockfile
- name: install yarn dependencies in ./app using cache
uses: bahmutov/npm-install@v1.6.0
with:
working-directory: ./app
# following step does code signing when `electron-builder --publish always` (look in package.json)
- name: yarn package-ci

View File

@ -4,7 +4,7 @@ let protocol;
let port;
if (!host && !protocol && !port) {
host = 'localhost';
host = '127.0.0.1';
protocol = 'http';
port = 3131; // TODO: read port from signaling server api
}

View File

@ -143,6 +143,10 @@ body
left: -40px !important;
}
div.class-allow-device-to-connect-alert {
z-index: 9999;
}
/* ALLOW CONNECTION ALERT BLINK ANIMATION START */
div.class-allow-device-to-connect-alert
> div.bp3-alert-body

View File

@ -25,6 +25,7 @@
"react-reveal": "^1.2.2",
"react-scripts": "3.4.3",
"react-spinners": "^0.9.0",
"react-test-renderer": "^17.0.1",
"screenfull": "^5.0.2",
"shortid": "^2.2.15",
"socket.io-client": "^2.3.0",

View File

@ -1,7 +1,8 @@
sonar.projectKey=deskreen-viewer
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.sources=src
sonar.coverage.exclusions=src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx
sonar.cpd.exclusions=src/**/mocks/*,src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx
sonar.coverage.exclusions=src/**/mocks/*,src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx
sonar.host.url=http://localhost:9000
sonar.login=e3b5f73b8778290f7074c40a4159c32b7f15a8e6
sonar.exclusions=src/serviceWorker.ts,node_modules/**

View File

@ -1,332 +1,9 @@
import React, {
useEffect,
useState,
useRef,
useContext,
useCallback,
} from 'react';
import { useTranslation } from 'react-i18next';
import i18n from './config/i18n';
import { H3, Position, Toaster } from '@blueprintjs/core';
import { Grid, Row, Col } from 'react-flexbox-grid';
import { findDOMNode } from 'react-dom';
import ReactPlayer from 'react-player';
import screenfull from 'screenfull';
import Crypto from './utils/crypto';
import './App.css';
import PeerConnection from './features/PeerConnection';
import VideoAutoQualityOptimizer from './features/VideoAutoQualityOptimizer';
import ConnectingIndicator from './components/ConnectingIndicator';
import MyDeviceInfoCard from './components/MyDeviceInfoCard';
import {
DARK_UI_BACKGROUND,
LIGHT_UI_BACKGROUND,
} from './constants/styleConstants';
import { AppContext } from './providers/AppContextProvider';
import PlayerControlPanel from './components/PlayerControlPanel';
import { VideoQuality } from './features/PeerConnection/VideoQualityEnum';
import { REACT_PLAYER_WRAPPER_ID } from './constants/appConstants';
import { TFunction } from 'i18next';
import ErrorDialog from './components/ErrorDialog';
import { ErrorMessage } from './components/ErrorDialog/ErrorMessageEnum';
const Fade = require('react-reveal/Fade');
const Slide = require('react-reveal/Slide');
function getPromptContent(step: number, t: TFunction) {
switch (step) {
case 1:
return (
<H3>
{t(
'Waiting for user to click ALLOW button on screen sharing device...'
)}
</H3>
);
case 2:
return <H3>Connected!</H3>;
case 3:
return (
<H3>
{t(
'Wating for user to select source to share from screen sharing device...'
)}
</H3>
);
default:
return <H3>Error occured :(</H3>;
}
}
import React from 'react';
import MainView from './containers/MainView';
function App() {
const { t } = useTranslation();
const { isDarkTheme, setIsDarkThemeHook } = useContext(AppContext);
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState(false);
const [toaster, setToaster] = useState<undefined | Toaster>();
const refHandlers = {
toaster: (ref: Toaster) => {
setToaster(ref);
},
};
const player = useRef(null);
const [promptStep, setPromptStep] = useState(1);
const [dialogErrorMessage, setDialogErrorMessage] = useState<ErrorMessage>(
ErrorMessage.UNKNOWN_ERROR
);
const [connectionIconType, setConnectionIconType] = useState<
'feed' | 'feed-subscribed'
>('feed');
const [myDeviceDetails, setMyDeviceDetails] = useState<DeviceDetails>({
myIP: '',
myOS: '',
myDeviceType: '',
myBrowser: '',
});
const [playing, setPlaying] = useState(true);
const [isFullScreenOn, setIsFullScreenOn] = useState(false);
const [url, setUrl] = useState();
const [screenSharingSourceType, setScreenSharingSourceType] = useState<
'screen' | 'window'
>('screen');
const [isWithControls, setIsWithControls] = useState(!screenfull.isEnabled);
const [isShownTextPrompt, setIsShownTextPrompt] = useState(false);
const [isShownSpinnerIcon, setIsShownSpinnerIcon] = useState(false);
const [spinnerIconType, setSpinnerIconType] = useState<
'desktop' | 'application'
>('desktop');
const [videoQuality, setVideoQuality] = useState<VideoQuality>(
VideoQuality.Q_AUTO
);
const [peer, setPeer] = useState<undefined | PeerConnection>();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
useEffect(() => {
if (!peer) return;
if (!peer.isStreamStarted) return;
peer.setVideoQuality(videoQuality);
}, [videoQuality, peer]);
useEffect(() => {
document.body.style.backgroundColor = isDarkTheme
? DARK_UI_BACKGROUND
: LIGHT_UI_BACKGROUND;
}, [isDarkTheme]);
useEffect(() => {
if (!peer) {
const _peer = new PeerConnection(
setUrl,
new Crypto(),
new VideoAutoQualityOptimizer(),
isDarkTheme,
setMyDeviceDetails,
() => {
setConnectionIconType('feed-subscribed');
setIsShownTextPrompt(false);
setIsShownTextPrompt(true);
setPromptStep(2);
setTimeout(() => {
setIsShownTextPrompt(false);
setIsShownTextPrompt(true);
setPromptStep(3);
}, 2000);
},
setScreenSharingSourceType,
setIsDarkThemeHook,
changeLanguage,
setDialogErrorMessage,
setIsErrorDialogOpen
);
setPeer(_peer);
setTimeout(() => {
setIsShownTextPrompt(true);
}, 100);
}
}, [setIsDarkThemeHook, isDarkTheme, peer]);
useEffect(() => {
// infinite use effect
setTimeout(() => {
setIsShownSpinnerIcon(!isShownSpinnerIcon);
setSpinnerIconType(
spinnerIconType === 'desktop' ? 'application' : 'desktop'
);
}, 1500);
}, [isShownSpinnerIcon, spinnerIconType]);
const handlePlayPause = useCallback(() => {
setPlaying(!playing);
}, [playing]);
useEffect(() => {
if (url !== undefined) {
setTimeout(() => {
// @ts-ignore
document.querySelector('.container > .react-reveal').style.display =
'none';
}, 1000);
}
}, [url]);
useEffect(() => {
if (promptStep === 3) {
// start infinite use effect
setIsShownSpinnerIcon(true);
}
}, [promptStep]);
return (
<Grid>
<Slide
id="connection-prompts-slide"
bottom
when={url === undefined ? true : false}
>
<div
style={{
position: 'absolute',
zIndex: 10,
top: 0,
left: 0,
width: '100%',
height: '100vh',
boxShadow: `0 0 0 5px ${isDarkTheme ? '#000' : '#A7B6C2'}`,
backgroundColor: isDarkTheme
? DARK_UI_BACKGROUND
: LIGHT_UI_BACKGROUND,
}}
>
<Row
bottom="xs"
style={{
height: '50vh',
width: '100%',
marginRight: '0px',
marginLeft: '0px',
}}
>
<Row center="xs" style={{ width: '100%', margin: '0 auto' }}>
<Col
xs={12}
style={{
marginBottom: '50px',
textAlign: 'center',
width: '100%',
}}
>
<div style={{ width: '100%' }}>
<Row center="xs" style={{ width: '100%', margin: '0 auto' }}>
<Col md={6} xl={4}>
<MyDeviceInfoCard deviceDetails={myDeviceDetails} />
</Col>
</Row>
<Fade
opposite
when={isShownTextPrompt}
duration={2000}
style={{ width: '100%' }}
>
<div id="prompt-text" style={{ fontSize: '20px' }}>
{getPromptContent(promptStep, t)}
</div>
</Fade>
</div>
</Col>
</Row>
</Row>
<Fade
opposite
when={isShownTextPrompt}
duration={500}
style={{ width: '100%' }}
>
<ConnectingIndicator
currentStep={promptStep}
connectionIconType={connectionIconType}
isShownSelectingSharingIcon={isShownSpinnerIcon}
selectingSharingIconType={spinnerIconType}
/>
</Fade>
</div>
</Slide>
<div
style={{
position: 'absolute',
zIndex: 1,
top: 0,
left: 0,
width: '100%',
height: '100vh',
}}
>
<PlayerControlPanel
onSwitchChangedCallback={(isEnabled) => setIsWithControls(isEnabled)}
isDefaultPlayerTurnedOn={isWithControls}
handleClickFullscreen={() => {
if (!screenfull.isEnabled) return;
// @ts-ignore Property 'request' does not exist on type '{ isEnabled: false; }'.
screenfull.request(findDOMNode(player.current));
setIsFullScreenOn(!isFullScreenOn);
}}
handleClickPlayPause={handlePlayPause}
isPlaying={playing}
setVideoQuality={setVideoQuality}
selectedVideoQuality={videoQuality}
screenSharingSourceType={screenSharingSourceType}
toaster={toaster}
/>
<div
id="video-container"
style={{
margin: '0 auto',
position: 'relative',
}}
>
<div
id="player-wrapper-id"
className="player-wrapper"
style={{
position: 'relative',
paddingTop: '56.25%',
}}
>
<ReactPlayer
ref={player}
id={REACT_PLAYER_WRAPPER_ID}
playing={playing}
playsinline={true}
controls={isWithControls}
muted={true}
url={(url as unknown) as MediaStream}
width="100%"
height="100%"
style={{
position: 'absolute',
top: 0,
left: 0,
}}
/>
</div>
<canvas id="comparison-canvas" style={{ display: 'none' }}></canvas>
</div>
</div>
<Toaster ref={refHandlers.toaster} position={Position.TOP_LEFT} />
<ErrorDialog
errorMessage={dialogErrorMessage}
isOpen={isErrorDialogOpen}
/>
</Grid>
<MainView />
);
}

View File

@ -1,4 +1,3 @@
/* istanbul ignore file */
let host;
let protocol;
let port;

View File

@ -0,0 +1,34 @@
import generator from './generator';
import { TEST_PORT, TEST_HOST, TEST_PROTOCOL } from './mocks/generatorTestVariables';
// how to use local variables in jest mock to get rid of hoisting mocks to top most code block:
//stackoverflow.com/questions/44649699/service-mocked-with-jest-causes-the-module-factory-of-jest-mock-is-not-allowe
jest.mock('./config', () => {
const generatorTestVariables = require('./mocks/generatorTestVariables');
return {
host: generatorTestVariables.TEST_HOST,
protocol: generatorTestVariables.TEST_PROTOCOL,
port: generatorTestVariables.TEST_PORT,
};
});
describe('generator.ts', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('when generator() is called properly', () => {
it('should produce correct string', () => {
const roomID = '333';
const result = generator(roomID);
expect(result).toMatch(
`${TEST_PROTOCOL}://${TEST_HOST}:${TEST_PORT}/${roomID}`
);
});
});
});

View File

@ -1,4 +1,3 @@
/* istanbul ignore file */
import config from './config';
export default (resourceName = '') => {

View File

@ -0,0 +1,3 @@
export const TEST_PORT = '3232';
export const TEST_HOST = '123.123.123.123';
export const TEST_PROTOCOL = 'http';

View File

@ -0,0 +1,18 @@
import React from 'react';
import renderer from 'react-test-renderer';
import LoadingSharingIcon from './LoadingSharingIcon';
jest.useFakeTimers();
it('should match exact snapshot', () => {
const subject = renderer.create(
<>
<LoadingSharingIcon
loadingSharingIconType="desktop"
isShownLoadingSharingIcon
/>
</>
);
expect(subject).toMatchSnapshot();
});

View File

@ -0,0 +1,65 @@
import React, { useContext } from 'react';
import { Icon } from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { AppContext } from '../../providers/AppContextProvider';
const Fade = require('react-reveal/Fade');
interface SelectSharingIconProps {
loadingSharingIconType: LoadingSharingIconType;
isShownLoadingSharingIcon: boolean;
}
function LoadingSharingIcon(props: SelectSharingIconProps) {
const { isDarkTheme } = useContext(AppContext);
const {
loadingSharingIconType: selectingSharingIconType,
isShownLoadingSharingIcon: isShownSelectingSharingIcon,
} = props;
return (
<Row
center="xs"
top="xs"
style={{
height: '100%',
width: '100%',
marginRight: '0px',
marginLeft: '0px',
}}
>
<Col>
<Row
style={{
width: '100%',
height: '50px',
}}
center="xs"
>
<Col xs={8} md={4}>
<PropagateLoader
loading
size={18}
color={isDarkTheme ? '#BFCCD6' : '#5C7080'}
/>
</Col>
</Row>
<Row center="xs">
<Col>
<Fade opposite when={isShownSelectingSharingIcon} duration={500}>
<Icon
icon={selectingSharingIconType}
iconSize={60}
color={isDarkTheme ? '#BFCCD6' : '#5C7080'}
/>
</Fade>
</Col>
</Row>
</Col>
</Row>
);
}
export default LoadingSharingIcon;

View File

@ -1,66 +0,0 @@
import React, { useContext } from 'react';
import { Icon } from '@blueprintjs/core';
import { Col, Row } from 'react-flexbox-grid';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { AppContext } from '../../providers/AppContextProvider';
const Fade = require('react-reveal/Fade');
interface SelectSharingIconProps {
selectingSharingIconType: 'desktop' | 'application';
isShownSelectingSharingIcon: boolean;
}
function SelectSharingIcon(props: SelectSharingIconProps) {
const { isDarkTheme } = useContext(AppContext);
const { selectingSharingIconType, isShownSelectingSharingIcon } = props;
return (
<Row
center="xs"
top="xs"
style={{
height: '100%',
width: '100%',
marginRight: '0px',
marginLeft: '0px',
}}
>
<Col>
<Row
style={{
width: '100%',
height: '50px',
}}
center="xs"
>
<Col xs={8} md={4}>
<PropagateLoader
loading
size={18}
color={isDarkTheme ? '#BFCCD6' : '#5C7080'}
/>
</Col>
</Row>
<Row center="xs">
<Col>
<Fade
opposite
when={isShownSelectingSharingIcon}
duration={500}
>
<Icon
icon={selectingSharingIconType}
iconSize={60}
color={isDarkTheme ? '#BFCCD6' : '#5C7080'}
/>
</Fade>
</Col>
</Row>
</Col>
</Row>
);
}
export default SelectSharingIcon;

View File

@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should match exact snapshot 1`] = `
<div
className="row center-xs top-xs"
style={
Object {
"height": "100%",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className=""
>
<div
className="row center-xs"
style={
Object {
"height": "50px",
"width": "100%",
}
}
>
<div
className="col-xs-8 col-md-4"
>
<div
className="css-9ebb9a"
>
<div
className="css-1nih24x"
/>
<div
className="css-182ruc5"
/>
<div
className="css-fapzgm"
/>
<div
className="css-fqj3wr"
/>
<div
className="css-srr2wk"
/>
<div
className="css-1netspe"
/>
</div>
</div>
</div>
<div
className="row center-xs"
>
<div
className=""
>
<div
className="react-reveal"
style={
Object {
"opacity": undefined,
}
}
>
<span
className="bp3-icon bp3-icon-desktop"
icon="desktop"
>
<svg
data-icon="desktop"
fill="#5C7080"
height={60}
viewBox="0 0 20 20"
width={60}
>
<desc>
desktop
</desc>
<path
d="M19 0H1C.45 0 0 .45 0 1v13c0 .55.45 1 1 1h5.67l-.5 3H5c-.55 0-1 .45-1 1s.45 1 1 1h10c.55 0 1-.45 1-1s-.45-1-1-1h-1.17l-.5-3H19c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-1 13H2V2h16v11z"
fillRule="evenodd"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Text } from '@blueprintjs/core';
import { Row } from 'react-flexbox-grid';
import ConnectingIndicatorIcon from './ConnectingIndicatorIcon';
import SelectSharingIcon from './SelectSharingIcon';
import LoadingSharingIcon from './LoadingSharingIcon';
const basePulsingCircleStyles = {
borderRadius: '100%',
@ -18,9 +18,9 @@ const basePulsingCircleStyles = {
function getConnectingStepContent(
currentStep: number,
connectionIconType: 'feed' | 'feed-subscribed',
selectingSharingIconType: 'desktop' | 'application',
isShownSelectingSharingIcon: boolean,
connectionIconType: ConnectionIconType,
loadingSharingIconType: LoadingSharingIconType,
isShownLoadingSharingIcon: boolean,
) {
const pulsingCircle1Styles = {
@ -68,9 +68,9 @@ function getConnectingStepContent(
);
case 3:
return (
<SelectSharingIcon
isShownSelectingSharingIcon={isShownSelectingSharingIcon}
selectingSharingIconType={selectingSharingIconType}
<LoadingSharingIcon
isShownLoadingSharingIcon={isShownLoadingSharingIcon}
loadingSharingIconType={loadingSharingIconType}
/>
);
default:
@ -80,9 +80,9 @@ function getConnectingStepContent(
interface ConnectingIndicatorProps {
currentStep: number;
connectionIconType: 'feed' | 'feed-subscribed';
connectionIconType: ConnectionIconType;
isShownSelectingSharingIcon: boolean;
selectingSharingIconType: 'desktop' | 'application';
selectingSharingIconType: LoadingSharingIconType;
}
function ConnectingIndicator(props: ConnectingIndicatorProps) {

View File

@ -1,6 +1,6 @@
export enum ErrorMessage {
UNKNOWN_ERROR = 'An unknonw error uccured.',
DENY_TO_CONNECT = 'You were not allowed to connect.',
DICONNECTED = 'You were disconnected.',
DISCONNECTED = 'You were disconnected.',
NOT_ALLOWED = 'You were not allowed to connect.',
}

View File

@ -0,0 +1,393 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should match exact snapshot 1`] = `
<div
className="bp3-card bp3-elevation-4"
>
<div
className="row middle-xs between-xs"
>
<div
className="col-xs-12 col-md-3"
>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<div
className="row middle-xs"
style={
Object {
"opacity": "0.75",
}
}
>
<div
className="col-xs-4"
>
<img
alt="logo"
height={42}
src="deskreen_logo_128x128.png"
width={42}
/>
</div>
<div
className="col-xs-8"
>
<h5
className="bp3-heading"
style={
Object {
"marginBottom": "0px",
}
}
>
Deskreen
</h5>
</div>
</div>
</span>
</button>
</span>
</span>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
style={
Object {
"borderRadius": "100px",
}
}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<div
className="row start-xs"
>
<div
className="col-xs"
>
<img
alt="heart"
height={16}
src="red_heart_2764_twemoji_120x120.png"
style={
Object {
"transform": "translateY(2px)",
}
}
width={16}
/>
</div>
<div
className="col-xs"
>
<div
style={
Object {
"transform": "translateY(2px) translateX(-5px)",
}
}
>
<div
className=""
>
Donate!
</div>
</div>
</div>
</div>
</span>
</button>
</span>
</span>
</div>
<div
className="col-xs-12 col-md-6"
>
<div
className="row center-xs"
style={
Object {
"height": "42px",
}
}
>
<div
className="row center-xs middle-xs"
style={
Object {
"backgroundColor": "#137CBD",
"borderRadius": "20px",
"height": "100%",
"width": "190px",
}
}
>
<div
className="row center-xs middle-xs"
style={
Object {
"width": "100%",
}
}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
style={
Object {
"color": "white",
"minWidth": "70px",
"width": "85px",
}
}
type="button"
>
<span
className="bp3-button-text"
>
<div
className="row"
>
<div
className="col-xs"
>
<span
className="bp3-icon bp3-icon-pause"
icon="pause"
>
<svg
data-icon="pause"
fill="white"
height={16}
viewBox="0 0 16 16"
width={16}
>
<desc>
pause
</desc>
<path
d="M6 3H4c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1h2c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zm6 0h-2c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1h2c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1z"
fillRule="evenodd"
/>
</svg>
</span>
</div>
<div
className="col-xs"
>
<div
className="bp3-text-large"
>
Pause
</div>
</div>
</div>
</span>
</button>
<div
className="bp3-divider"
style={
Object {
"borderBottom": "1px solid #ffffffa8",
"borderRight": "1px solid #ffffffa8",
"height": "20px",
}
}
/>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onClick={[Function]}
>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<span
className="bp3-icon bp3-icon-cog"
icon="cog"
>
<svg
data-icon="cog"
fill="white"
height={16}
viewBox="0 0 16 16"
width={16}
>
<desc>
cog
</desc>
<path
d="M15.19 6.39h-1.85c-.11-.37-.27-.71-.45-1.04l1.36-1.36c.31-.31.31-.82 0-1.13l-1.13-1.13a.803.803 0 00-1.13 0l-1.36 1.36c-.33-.17-.67-.33-1.04-.44V.79c0-.44-.36-.8-.8-.8h-1.6c-.44 0-.8.36-.8.8v1.86c-.39.12-.75.28-1.1.47l-1.3-1.3c-.3-.3-.79-.3-1.09 0L1.82 2.91c-.3.3-.3.79 0 1.09l1.3 1.3c-.2.34-.36.7-.48 1.09H.79c-.44 0-.8.36-.8.8v1.6c0 .44.36.8.8.8h1.85c.11.37.27.71.45 1.04l-1.36 1.36c-.31.31-.31.82 0 1.13l1.13 1.13c.31.31.82.31 1.13 0l1.36-1.36c.33.18.67.33 1.04.44v1.86c0 .44.36.8.8.8h1.6c.44 0 .8-.36.8-.8v-1.86c.39-.12.75-.28 1.1-.47l1.3 1.3c.3.3.79.3 1.09 0l1.09-1.09c.3-.3.3-.79 0-1.09l-1.3-1.3c.19-.35.36-.71.48-1.1h1.85c.44 0 .8-.36.8-.8v-1.6a.816.816 0 00-.81-.79zm-7.2 4.6c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"
fillRule="evenodd"
/>
</svg>
</span>
</span>
</button>
</span>
</span>
</span>
</span>
<div
className="bp3-divider"
style={
Object {
"borderBottom": "1px solid #ffffffa8",
"borderRight": "1px solid #ffffffa8",
"height": "20px",
}
}
/>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<img
alt="fullscreen-toggle"
height={16}
src="fullscreen_24px.svg"
style={
Object {
"filter": "invert(100%) sepia(100%) saturate(0%) hue-rotate(127deg) brightness(107%) contrast(102%)",
"transform": "scale(1.5) translateY(1px)",
}
}
width={16}
/>
</span>
</button>
</span>
</span>
</div>
</div>
</div>
</div>
<div
className="col-xs-12 col-md-3"
>
<div
className="row end-xs"
>
<div
className="col-xs-12"
>
<label
className="bp3-control bp3-switch bp3-disabled bp3-inline bp3-align-right"
style={
Object {
"marginBottom": "0px",
}
}
>
<input
checked={true}
disabled={true}
onChange={[Function]}
type="checkbox"
/>
<span
className="bp3-control-indicator"
>
<div
className="bp3-control-indicator-child"
>
<div
className="bp3-switch-inner-text"
>
ON
</div>
</div>
<div
className="bp3-control-indicator-child"
>
<div
className="bp3-switch-inner-text"
>
ON
</div>
</div>
</span>
Default Video Player
</label>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,33 @@
/* istanbul ignore file */
// IMPORTANT! leave upper blank line so this file is ignored for coverage!!! More on this issue here
// https://github.com/facebook/create-react-app/issues/6106#issuecomment-550076629
import { REACT_PLAYER_WRAPPER_ID } from "../../constants/appConstants";
export default () => {
const player = document.querySelector(
`#${REACT_PLAYER_WRAPPER_ID} > video`
);
if (!player) return;
// @ts-ignore
if (player.requestFullScreen) {
// @ts-ignore
player.requestFullScreen();
// @ts-ignore
} else if (player.webkitRequestFullScreen) {
// @ts-ignore
player.webkitRequestFullScreen();
// @ts-ignore
} else if (player.mozRequestFullScreen) {
// @ts-ignore
player.mozRequestFullScreen();
// @ts-ignore
} else if (player.msRequestFullscreen) {
// @ts-ignore
player.msRequestFullscreen();
// @ts-ignore
} else if (player.webkitEnterFullscreen) {
// @ts-ignore
player.webkitEnterFullscreen(); //for iphone this code worked
}
};

View File

@ -0,0 +1,25 @@
import React from 'react';
import renderer from 'react-test-renderer';
import PlayerControlPanel from '.';
import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
jest.useFakeTimers();
it('should match exact snapshot', () => {
const subject = renderer.create(
<>
<PlayerControlPanel
onSwitchChangedCallback={() => {}}
isPlaying
isDefaultPlayerTurnedOn
handleClickFullscreen={() => {}}
handleClickPlayPause={() => {}}
setVideoQuality={() => {}}
selectedVideoQuality={VideoQuality.Q_100_PERCENT}
screenSharingSourceType={'screen'}
toaster={undefined}
/>
</>
);
expect(subject).toMatchSnapshot();
});

View File

@ -1,9 +1,8 @@
import React, {
useEffect,
useMemo,
useState,
useCallback,
} from 'react';
/* istanbul ignore file */
// IMPORTANT! leave upper blank line so this file is ignored for coverage!!! More on this issue here
// https://github.com/facebook/create-react-app/issues/6106#issuecomment-550076629
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import {
Alignment,
Button,
@ -26,8 +25,9 @@ import DeskreenIconPNG from '../../images/deskreen_logo_128x128.png';
import RedHeartTwemojiPNG from '../../images/red_heart_2764_twemoji_120x120.png';
import { Col, Row } from 'react-flexbox-grid';
import screenfull from 'screenfull';
import { VideoQuality } from '../../features/PeerConnection/VideoQualityEnum';
import { REACT_PLAYER_WRAPPER_ID } from '../../constants/appConstants';
import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
import handlePlayerToggleFullscreen from './handlePlayerToggleFullscreen';
import initScreenfullOnChange from './initScreenfullOnChange';
const videoQualityButtonStyle: React.CSSProperties = {
width: '100%',
@ -42,12 +42,11 @@ interface PlayerControlPanelProps {
handleClickPlayPause: () => void;
setVideoQuality: (q: VideoQuality) => void;
selectedVideoQuality: VideoQuality;
screenSharingSourceType: 'screen' | 'window';
screenSharingSourceType: ScreenSharingSourceType;
toaster: undefined | Toaster;
}
function PlayerControlPanel(props: PlayerControlPanelProps) {
const {
isPlaying,
onSwitchChangedCallback,
@ -65,40 +64,11 @@ function PlayerControlPanel(props: PlayerControlPanelProps) {
const [isFullScreenOn, setIsFullScreenOn] = useState(false);
useEffect(() => {
if (!screenfull.isEnabled) return;
// @ts-ignore
screenfull.on('change', () => {
// @ts-ignore
setIsFullScreenOn(screenfull.isFullscreen);
});
initScreenfullOnChange(setIsFullScreenOn);
}, []);
const handleClickFullscreenWhenDefaultPlayerIsOn = useCallback(() => {
const player = document.querySelector(
`#${REACT_PLAYER_WRAPPER_ID} > video`
);
if (!player) return;
// @ts-ignore
if (player.requestFullScreen) {
// @ts-ignore
player.requestFullScreen();
// @ts-ignore
} else if (player.webkitRequestFullScreen) {
// @ts-ignore
player.webkitRequestFullScreen();
// @ts-ignore
} else if (player.mozRequestFullScreen) {
// @ts-ignore
player.mozRequestFullScreen();
// @ts-ignore
} else if (player.msRequestFullscreen) {
// @ts-ignore
player.msRequestFullscreen();
// @ts-ignore
} else if (player.webkitEnterFullscreen) {
// @ts-ignore
player.webkitEnterFullscreen(); //for iphone this code worked
}
handlePlayerToggleFullscreen();
}, []);
return (
@ -113,7 +83,12 @@ function PlayerControlPanel(props: PlayerControlPanelProps) {
<Button minimal>
<Row middle="xs" style={{ opacity: '0.75' }}>
<Col xs={4}>
<img src={DeskreenIconPNG} width={42} height={42} alt="logo" />
<img
src={DeskreenIconPNG}
width={42}
height={42}
alt="logo"
/>
</Col>
<Col xs={8}>
<H5 style={{ marginBottom: '0px' }}>Deskreen</H5>
@ -125,7 +100,7 @@ function PlayerControlPanel(props: PlayerControlPanelProps) {
content="If you like Deskreen, consider donating! Deskreen is free and opensource forever! You can help us to make Deskreen even better!"
position={Position.BOTTOM}
>
<Button>
<Button style={{ borderRadius: '100px' }}>
<Row start="xs">
<Col xs>
<img
@ -217,7 +192,7 @@ function PlayerControlPanel(props: PlayerControlPanelProps) {
{Object.values(VideoQuality).map(
(q: VideoQuality) => {
return (
<Row>
<Row key={q}>
<Button
minimal
active={selectedVideoQuality === q}

View File

@ -0,0 +1,10 @@
import screenfull from "screenfull";
export default (setIsFullScreenOn: (_: boolean) => void) => {
if (!screenfull.isEnabled) return;
// @ts-ignore
screenfull.on('change', () => {
// @ts-ignore
setIsFullScreenOn(screenfull.isFullscreen);
});
};

View File

@ -6,3 +6,10 @@ export const VIDEO_QUALITY_TO_DECIMAL = {
'80%': 0.80, // Q_80_PERCENT
'100%': 1, // Q_100_PERCENT
}
export const COMPARISON_CANVAS_ID = 'comparison-canvas';
export const DUMMY_MY_DEVICE_DETAILS = {
myIP: '',
myOS: '',
myDeviceType: '',
myBrowser: '',
};

View File

@ -0,0 +1,933 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`when getPromptContent is called should match exact snapshot on each step 1`] = `
<div
className="react-reveal"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"boxShadow": "0 0 0 5px #A7B6C2",
"height": "100vh",
"left": 0,
"opacity": undefined,
"position": "absolute",
"top": 0,
"width": "100%",
"zIndex": 10,
}
}
>
<div
className="row bottom-xs"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-xs-12"
style={
Object {
"marginBottom": "50px",
"textAlign": "center",
"width": "100%",
}
}
>
<div
style={
Object {
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-md-6 col-xl-4"
>
<div
className="bp3-card bp3-elevation-3"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"marginBottom": "30px",
}
}
>
<h3
className="bp3-heading"
>
My Device Info:
</h3>
<div
className="bp3-callout"
>
<div
className=""
>
Device Type:
</div>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className=""
style={
Object {
"backgroundColor": "#00f99273",
"borderRadius": "20px",
"fontWeight": 900,
"paddingLeft": "10px",
"paddingRight": "10px",
}
}
tabIndex={0}
>
<div
className=""
>
Device IP:
</div>
</div>
</span>
</span>
<div
className=""
>
Device Browser:
</div>
<div
className=""
>
Device OS:
</div>
</div>
<div
className="bp3-text-muted"
>
These details should match with the ones that you see in alert popup on screen sharing device.
</div>
</div>
</div>
</div>
<div
id="prompt-text"
style={
Object {
"fontSize": "20px",
}
}
>
<h3
className="bp3-heading"
>
Error occured :(
</h3>
</div>
</div>
</div>
</div>
</div>
<div
className="row top-xs"
id="connecting-screen"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className=""
>
Error occured :(
</div>
</div>
</div>
`;
exports[`when getPromptContent is called should match exact snapshot on each step 2`] = `
<div
className="react-reveal"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"boxShadow": "0 0 0 5px #A7B6C2",
"height": "100vh",
"left": 0,
"opacity": undefined,
"position": "absolute",
"top": 0,
"width": "100%",
"zIndex": 10,
}
}
>
<div
className="row bottom-xs"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-xs-12"
style={
Object {
"marginBottom": "50px",
"textAlign": "center",
"width": "100%",
}
}
>
<div
style={
Object {
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-md-6 col-xl-4"
>
<div
className="bp3-card bp3-elevation-3"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"marginBottom": "30px",
}
}
>
<h3
className="bp3-heading"
>
My Device Info:
</h3>
<div
className="bp3-callout"
>
<div
className=""
>
Device Type:
</div>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className=""
style={
Object {
"backgroundColor": "#00f99273",
"borderRadius": "20px",
"fontWeight": 900,
"paddingLeft": "10px",
"paddingRight": "10px",
}
}
tabIndex={0}
>
<div
className=""
>
Device IP:
</div>
</div>
</span>
</span>
<div
className=""
>
Device Browser:
</div>
<div
className=""
>
Device OS:
</div>
</div>
<div
className="bp3-text-muted"
>
These details should match with the ones that you see in alert popup on screen sharing device.
</div>
</div>
</div>
</div>
<div
id="prompt-text"
style={
Object {
"fontSize": "20px",
}
}
>
<h3
className="bp3-heading"
>
Waiting for user to click ALLOW button on screen sharing device...
</h3>
</div>
</div>
</div>
</div>
</div>
<div
className="row top-xs"
id="connecting-screen"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
id="pulsing-circle-1"
style={
Object {
"backgroundColor": "rgba(43, 149, 214, 0.7)",
"borderRadius": "100%",
"height": "100px",
"left": "0",
"marginLeft": "auto",
"marginRight": "auto",
"position": "absolute",
"right": "0",
"textAlign": "center",
"width": "100px",
"zIndex": 1,
}
}
/>
<div
id="pulsing-circle-2"
style={
Object {
"backgroundColor": "#2B95D6",
"borderRadius": "100%",
"height": "100px",
"left": "0",
"marginLeft": "auto",
"marginRight": "auto",
"position": "absolute",
"right": "0",
"textAlign": "center",
"width": "100px",
"zIndex": 2,
}
}
>
<div
className="row center-xs middle-xs"
style={
Object {
"height": "100%",
"width": "100%",
}
}
>
<div
className=""
>
<span
className="bp3-icon bp3-icon-feed"
icon="feed"
style={
Object {
"transform": "translateX(10px)",
}
}
>
<svg
data-icon="feed"
fill="white"
height={50}
viewBox="0 0 20 20"
width={50}
>
<desc>
feed
</desc>
<path
d="M2.5 15a2.5 2.5 0 000 5 2.5 2.5 0 000-5zm.5-5c-.55 0-1 .45-1 1s.45 1 1 1c2.76 0 5 2.24 5 5 0 .55.45 1 1 1s1-.45 1-1c0-3.87-3.13-7-7-7zM3 0c-.55 0-1 .45-1 1s.45 1 1 1c8.28 0 15 6.72 15 15 0 .55.45 1 1 1s1-.45 1-1C20 7.61 12.39 0 3 0zm0 5c-.55 0-1 .45-1 1s.45 1 1 1c5.52 0 10 4.48 10 10 0 .55.45 1 1 1s1-.45 1-1C15 10.37 9.63 5 3 5z"
fillRule="evenodd"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
`;
exports[`when getPromptContent is called should match exact snapshot on each step 3`] = `
<div
className="react-reveal"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"boxShadow": "0 0 0 5px #A7B6C2",
"height": "100vh",
"left": 0,
"opacity": undefined,
"position": "absolute",
"top": 0,
"width": "100%",
"zIndex": 10,
}
}
>
<div
className="row bottom-xs"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-xs-12"
style={
Object {
"marginBottom": "50px",
"textAlign": "center",
"width": "100%",
}
}
>
<div
style={
Object {
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-md-6 col-xl-4"
>
<div
className="bp3-card bp3-elevation-3"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"marginBottom": "30px",
}
}
>
<h3
className="bp3-heading"
>
My Device Info:
</h3>
<div
className="bp3-callout"
>
<div
className=""
>
Device Type:
</div>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className=""
style={
Object {
"backgroundColor": "#00f99273",
"borderRadius": "20px",
"fontWeight": 900,
"paddingLeft": "10px",
"paddingRight": "10px",
}
}
tabIndex={0}
>
<div
className=""
>
Device IP:
</div>
</div>
</span>
</span>
<div
className=""
>
Device Browser:
</div>
<div
className=""
>
Device OS:
</div>
</div>
<div
className="bp3-text-muted"
>
These details should match with the ones that you see in alert popup on screen sharing device.
</div>
</div>
</div>
</div>
<div
id="prompt-text"
style={
Object {
"fontSize": "20px",
}
}
>
<h3
className="bp3-heading"
>
Connected!
</h3>
</div>
</div>
</div>
</div>
</div>
<div
className="row top-xs"
id="connecting-screen"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className="pulse-3-once"
id="pulsing-circle-3"
style={
Object {
"backgroundColor": "#15B371",
"borderRadius": "100%",
"height": "100px",
"left": "0",
"marginLeft": "auto",
"marginRight": "auto",
"position": "absolute",
"right": "0",
"textAlign": "center",
"width": "100px",
}
}
>
<div
className="row center-xs middle-xs"
style={
Object {
"height": "100%",
"width": "100%",
}
}
>
<div
className=""
>
<span
className="bp3-icon bp3-icon-feed"
icon="feed"
style={
Object {
"transform": "translateX(10px)",
}
}
>
<svg
data-icon="feed"
fill="white"
height={50}
viewBox="0 0 20 20"
width={50}
>
<desc>
feed
</desc>
<path
d="M2.5 15a2.5 2.5 0 000 5 2.5 2.5 0 000-5zm.5-5c-.55 0-1 .45-1 1s.45 1 1 1c2.76 0 5 2.24 5 5 0 .55.45 1 1 1s1-.45 1-1c0-3.87-3.13-7-7-7zM3 0c-.55 0-1 .45-1 1s.45 1 1 1c8.28 0 15 6.72 15 15 0 .55.45 1 1 1s1-.45 1-1C20 7.61 12.39 0 3 0zm0 5c-.55 0-1 .45-1 1s.45 1 1 1c5.52 0 10 4.48 10 10 0 .55.45 1 1 1s1-.45 1-1C15 10.37 9.63 5 3 5z"
fillRule="evenodd"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
`;
exports[`when getPromptContent is called should match exact snapshot on each step 4`] = `
<div
className="react-reveal"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"boxShadow": "0 0 0 5px #A7B6C2",
"height": "100vh",
"left": 0,
"opacity": undefined,
"position": "absolute",
"top": 0,
"width": "100%",
"zIndex": 10,
}
}
>
<div
className="row bottom-xs"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-xs-12"
style={
Object {
"marginBottom": "50px",
"textAlign": "center",
"width": "100%",
}
}
>
<div
style={
Object {
"width": "100%",
}
}
>
<div
className="row center-xs"
style={
Object {
"margin": "0 auto",
"width": "100%",
}
}
>
<div
className="col-md-6 col-xl-4"
>
<div
className="bp3-card bp3-elevation-3"
style={
Object {
"backgroundColor": "rgba(240, 248, 250, 1)",
"marginBottom": "30px",
}
}
>
<h3
className="bp3-heading"
>
My Device Info:
</h3>
<div
className="bp3-callout"
>
<div
className=""
>
Device Type:
</div>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className=""
style={
Object {
"backgroundColor": "#00f99273",
"borderRadius": "20px",
"fontWeight": 900,
"paddingLeft": "10px",
"paddingRight": "10px",
}
}
tabIndex={0}
>
<div
className=""
>
Device IP:
</div>
</div>
</span>
</span>
<div
className=""
>
Device Browser:
</div>
<div
className=""
>
Device OS:
</div>
</div>
<div
className="bp3-text-muted"
>
These details should match with the ones that you see in alert popup on screen sharing device.
</div>
</div>
</div>
</div>
<div
id="prompt-text"
style={
Object {
"fontSize": "20px",
}
}
>
<h3
className="bp3-heading"
>
Wating for user to select source to share from screen sharing device...
</h3>
</div>
</div>
</div>
</div>
</div>
<div
className="row top-xs"
id="connecting-screen"
style={
Object {
"height": "50vh",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className="row center-xs top-xs"
style={
Object {
"height": "100%",
"marginLeft": "0px",
"marginRight": "0px",
"width": "100%",
}
}
>
<div
className=""
>
<div
className="row center-xs"
style={
Object {
"height": "50px",
"width": "100%",
}
}
>
<div
className="col-xs-8 col-md-4"
>
<div
className="css-9ebb9a"
>
<div
className="css-1nih24x"
/>
<div
className="css-182ruc5"
/>
<div
className="css-fapzgm"
/>
<div
className="css-fqj3wr"
/>
<div
className="css-srr2wk"
/>
<div
className="css-1netspe"
/>
</div>
</div>
</div>
<div
className="row center-xs"
>
<div
className=""
>
<div
className="react-reveal"
style={
Object {
"opacity": undefined,
}
}
>
<span
className="bp3-icon bp3-icon-application"
icon="application"
>
<svg
data-icon="application"
fill="#5C7080"
height={60}
viewBox="0 0 20 20"
width={60}
>
<desc>
application
</desc>
<path
d="M3.5 9h9c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-9c-.28 0-.5.22-.5.5s.22.5.5.5zm0 2h5c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-5c-.28 0-.5.22-.5.5s.22.5.5.5zM19 1H1c-.55 0-1 .45-1 1v16c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V2c0-.55-.45-1-1-1zm-1 16H2V6h16v11zM3.5 13h7c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-7c-.28 0-.5.22-.5.5s.22.5.5.5z"
fillRule="evenodd"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,32 @@
import React from 'react';
import renderer from 'react-test-renderer';
import ConnectionPrompts from '.';
jest.useFakeTimers();
const TEST_DEVICE_DETAILS = {
myIP: '',
myOS: '',
myDeviceType: '',
myBrowser: '',
};
describe('when getPromptContent is called', () => {
it('should match exact snapshot on each step', () => {
for (let i = 0; i < 4; ++i) {
const subject = renderer.create(
<>
<ConnectionPrompts
myDeviceDetails={TEST_DEVICE_DETAILS}
isShownTextPrompt
promptStep={i}
connectionIconType="feed"
isShownSpinnerIcon
spinnerIconType="application"
isOpen
/>
</>
);
expect(subject).toMatchSnapshot();
}
});
});

View File

@ -0,0 +1,125 @@
import React, { useContext } from 'react';
import { Row, Col } from 'react-flexbox-grid';
import { useTranslation } from 'react-i18next';
import { AppContext } from '../../providers/AppContextProvider';
import {
DARK_UI_BACKGROUND,
LIGHT_UI_BACKGROUND,
} from '../../constants/styleConstants';
import MyDeviceInfoCard from '../../components/MyDeviceInfoCard';
import { TFunction } from 'i18next';
import { H3 } from '@blueprintjs/core';
import ConnectingIndicator from '../../components/ConnectingIndicator';
const Slide = require('react-reveal/Slide');
interface ConnectionPropmptsProps {
myDeviceDetails: DeviceDetails;
isShownTextPrompt: boolean;
promptStep: number;
connectionIconType: ConnectionIconType;
isShownSpinnerIcon: boolean;
spinnerIconType: LoadingSharingIconType;
isOpen: boolean;
}
function getPromptContent(step: number, t: TFunction) {
switch (step) {
case 1:
return (
<H3>
{t(
'Waiting for user to click ALLOW button on screen sharing device...'
)}
</H3>
);
case 2:
return <H3>Connected!</H3>;
case 3:
return (
<H3>
{t(
'Wating for user to select source to share from screen sharing device...'
)}
</H3>
);
default:
return <H3>Error occured :(</H3>;
}
}
function ConnectionPropmpts(props: ConnectionPropmptsProps) {
const {
myDeviceDetails,
promptStep,
connectionIconType,
isShownSpinnerIcon,
spinnerIconType,
isOpen
} = props;
const { t } = useTranslation();
const { isDarkTheme } = useContext(AppContext);
return (
<Slide
id="connection-prompts-slide"
bottom
when={isOpen}
duration={1000}
>
<div
style={{
position: 'absolute',
zIndex: 10,
top: 0,
left: 0,
width: '100%',
height: '100vh',
boxShadow: `0 0 0 5px ${isDarkTheme ? '#000' : '#A7B6C2'}`,
backgroundColor: isDarkTheme ? DARK_UI_BACKGROUND : LIGHT_UI_BACKGROUND,
}}
>
<Row
bottom="xs"
style={{
height: '50vh',
width: '100%',
marginRight: '0px',
marginLeft: '0px',
}}
>
<Row center="xs" style={{ width: '100%', margin: '0 auto' }}>
<Col
xs={12}
style={{
marginBottom: '50px',
textAlign: 'center',
width: '100%',
}}
>
<div style={{ width: '100%' }}>
<Row center="xs" style={{ width: '100%', margin: '0 auto' }}>
<Col md={6} xl={4}>
<MyDeviceInfoCard deviceDetails={myDeviceDetails} />
</Col>
</Row>
<div id="prompt-text" style={{ fontSize: '20px' }}>
{getPromptContent(promptStep, t)}
</div>
</div>
</Col>
</Row>
</Row>
<ConnectingIndicator
currentStep={promptStep}
connectionIconType={connectionIconType}
isShownSelectingSharingIcon={isShownSpinnerIcon}
selectingSharingIconType={spinnerIconType}
/>
</div>
</Slide>
);
}
export default ConnectionPropmpts;

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should match exact snapshot 1`] = `ShallowWrapper {}`;

View File

@ -0,0 +1,5 @@
import i18n from "../../config/i18n";
export default (lng: string) => {
i18n.changeLanguage(lng);
};

View File

@ -0,0 +1,64 @@
import PeerConnection from '../../features/PeerConnection';
import PeerConnectionUIHandler from '../../features/PeerConnection/PeerConnectionUIHandler';
import getRoomIDOfCurrentBrowserWindow from '../../utils/getRoomIDOfCurrentBrowserWindow';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../../features/VideoAutoQualityOptimizer';
import changeLanguage from './changeLanguage';
export default (params: CreatePeerConnectionUseEffectParams) => {
const {
isDarkTheme,
peer,
setIsDarkThemeHook,
setMyDeviceDetails,
setConnectionIconType,
setIsShownTextPrompt,
setPromptStep,
setScreenSharingSourceType,
setDialogErrorMessage,
setIsErrorDialogOpen,
setUrl,
setPeer,
} = params;
return () => {
if (!peer) {
const UIHandler = new PeerConnectionUIHandler(
isDarkTheme,
setMyDeviceDetails,
() => {
setConnectionIconType('feed-subscribed');
setIsShownTextPrompt(false);
setIsShownTextPrompt(true);
setPromptStep(2);
setTimeout(() => {
setIsShownTextPrompt(false);
setIsShownTextPrompt(true);
setPromptStep(3);
}, 2000);
},
setScreenSharingSourceType,
setIsDarkThemeHook,
changeLanguage,
setDialogErrorMessage,
setIsErrorDialogOpen
);
const _peer = new PeerConnection(
getRoomIDOfCurrentBrowserWindow(),
setUrl,
new Crypto(),
new VideoAutoQualityOptimizer(),
UIHandler
);
setPeer(_peer);
setTimeout(() => {
setIsShownTextPrompt(true);
}, 100);
}
};
};

View File

@ -0,0 +1,50 @@
import handleDisplayingLoadingSharingIconLoop from './handleDisplayingLoadingSharingIconLoop';
jest.useFakeTimers();
describe('handleDisplayingLoadingSharingIconLoop callback', () => {
let testParams: handleDisplayingLoadingSharingIconLoopParams;
beforeEach(() => {
testParams = {
promptStep: 3,
url: undefined,
setIsShownLoadingSharingIcon: jest.fn(),
setLoadingSharingIconType: jest.fn(),
loadingSharingIconType: 'desktop',
isShownLoadingSharingIcon: false,
} as handleDisplayingLoadingSharingIconLoopParams
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('when it was called with promptStep 3 and url undefined', () => {
it('it should start loop, the ui should change with interval, showing and hiding icons', () => {
const callback = handleDisplayingLoadingSharingIconLoop(testParams);
callback();
// imitation of infinite loop
jest.advanceTimersByTime(2000);
expect(testParams.setIsShownLoadingSharingIcon).toBeCalledWith(true);
jest.clearAllMocks();
jest.advanceTimersByTime(2000);
expect(testParams.setIsShownLoadingSharingIcon).toBeCalledWith(false);
expect(testParams.setLoadingSharingIconType).toBeCalledWith('application');
jest.clearAllMocks();
jest.advanceTimersByTime(2000);
expect(testParams.setIsShownLoadingSharingIcon).toBeCalledWith(true);
expect(testParams.setLoadingSharingIconType).toBeCalledWith('desktop');
jest.clearAllMocks();
// ... by this time we are sure, this function works!
});
});
});

View File

@ -0,0 +1,37 @@
export default (
params: handleDisplayingLoadingSharingIconLoopParams
) => {
const {
promptStep,
url,
setIsShownLoadingSharingIcon,
loadingSharingIconType,
isShownLoadingSharingIcon,
setLoadingSharingIconType,
} = params;
return () => {
let interval: NodeJS.Timeout;
if (promptStep === 3 && url === undefined) {
setIsShownLoadingSharingIcon(true);
let currentIcon = loadingSharingIconType;
let isShownIcon = isShownLoadingSharingIcon;
let isShownWithFadingUIEffect = false;
interval = setInterval(() => {
isShownIcon = !isShownIcon;
setIsShownLoadingSharingIcon(isShownIcon);
if (isShownWithFadingUIEffect) {
currentIcon = currentIcon === 'desktop' ? 'application' : 'desktop';
setLoadingSharingIconType(currentIcon);
isShownWithFadingUIEffect = false;
} else {
isShownWithFadingUIEffect = true;
}
}, 1500);
}
return () => {
clearInterval(interval);
};
};
};

View File

@ -0,0 +1,17 @@
import { DUMMY_MY_DEVICE_DETAILS } from "../../constants/appConstants";
export default (
myDeviceDetails: DeviceDetails,
setIsErrorDialogOpen: (_: boolean) => void
) => {
return () => {
const timeout = setTimeout(() => {
if (myDeviceDetails === DUMMY_MY_DEVICE_DETAILS) {
setIsErrorDialogOpen(true);
}
}, 10000);
return () => {
clearTimeout(timeout);
};
};
};

View File

@ -0,0 +1,13 @@
export default (
url: undefined | MediaStream
) => {
return () => {
if (url !== undefined) {
setTimeout(() => {
// @ts-ignore
document.querySelector('.container > .react-reveal').style.display =
'none';
}, 1000);
}
};
};

View File

@ -0,0 +1,13 @@
import PeerConnection from "../../features/PeerConnection";
import { VideoQuality } from "../../features/VideoAutoQualityOptimizer/VideoQualityEnum";
export default (
videoQuality: VideoQuality,
peer: PeerConnection | undefined
) => {
return () => {
if (!peer) return;
if (!peer.isStreamStarted) return;
peer.setVideoQuality(videoQuality);
};
};

View File

@ -0,0 +1,38 @@
import React, { Suspense } from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import MainView from '.';
import { I18nextProvider } from 'react-i18next';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Mock react-i18next
i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
resources: {
en: {
common: {},
},
},
debug: false,
});
Enzyme.configure({ adapter: new Adapter() });
jest.useFakeTimers();
it('should match exact snapshot', () => {
const subject = shallow(
<Suspense fallback={<div>loading</div>}>
<I18nextProvider i18n={i18n}>
<MainView />
</I18nextProvider>
</Suspense>
);
expect(subject).toMatchSnapshot();
});

View File

@ -0,0 +1,126 @@
import React, { useEffect, useState, useContext, useCallback } from 'react';
import { Grid } from 'react-flexbox-grid';
import screenfull from 'screenfull';
import './index.css';
import PeerConnection from '../../features/PeerConnection';
import { AppContext } from '../../providers/AppContextProvider';
import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
import ErrorDialog from '../../components/ErrorDialog';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import ConnectionPropmpts from '../../containers/ConnectionPrompts';
import PlayerView from '../../containers/PlayerView';
import handleSetVideoQuality from './handleSetVideoQuality';
import { DUMMY_MY_DEVICE_DETAILS } from '../../constants/appConstants';
import handleNoConnectionTimeout from './handleNoConnectionTimeout';
import handleCreatePeerConnection from './handleCreatePeerConnection';
import handleRemoveDanglingReactRevealContainer from './handleRemoveDanglingReactRevealContainer';
import handleDisplayingLoadingSharingIconLoop from './handleDisplayingLoadingSharingIconLoop';
function MainView() {
const { isDarkTheme, setIsDarkThemeHook } = useContext(AppContext);
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState(false);
const [promptStep, setPromptStep] = useState(1);
const [dialogErrorMessage, setDialogErrorMessage] = useState<ErrorMessage>(
ErrorMessage.UNKNOWN_ERROR
);
const [connectionIconType, setConnectionIconType] = useState<
ConnectionIconType
>('feed');
const [myDeviceDetails, setMyDeviceDetails] = useState<DeviceDetails>(
DUMMY_MY_DEVICE_DETAILS
);
const [playing, setPlaying] = useState(true);
const [isFullScreenOn, setIsFullScreenOn] = useState(false);
const [url, setUrl] = useState<undefined | MediaStream>(undefined);
const [screenSharingSourceType, setScreenSharingSourceType] = useState<
ScreenSharingSourceType
>('screen');
const [isWithControls, setIsWithControls] = useState(!screenfull.isEnabled);
const [isShownTextPrompt, setIsShownTextPrompt] = useState(false);
const [isShownLoadingSharingIcon, setIsShownLoadingSharingIcon] = useState(
false
);
const [loadingSharingIconType, setLoadingSharingIconType] = useState<
LoadingSharingIconType
>('desktop');
const [videoQuality, setVideoQuality] = useState<VideoQuality>(
VideoQuality.Q_AUTO
);
const [peer, setPeer] = useState<undefined | PeerConnection>();
useEffect(handleSetVideoQuality(videoQuality, peer), [videoQuality, peer]);
useEffect(handleNoConnectionTimeout(myDeviceDetails, setIsErrorDialogOpen), [
myDeviceDetails,
]);
useEffect(
handleCreatePeerConnection({
isDarkTheme,
peer,
setIsDarkThemeHook,
setMyDeviceDetails,
setConnectionIconType,
setIsShownTextPrompt,
setPromptStep,
setScreenSharingSourceType,
setDialogErrorMessage,
setIsErrorDialogOpen,
setUrl,
setPeer,
}),
[setIsDarkThemeHook, isDarkTheme, peer]
);
const handlePlayPause = useCallback(() => {
setPlaying(!playing);
}, [playing]);
useEffect(handleRemoveDanglingReactRevealContainer(url), [url]);
useEffect(
handleDisplayingLoadingSharingIconLoop({
promptStep,
url,
setIsShownLoadingSharingIcon,
loadingSharingIconType,
isShownLoadingSharingIcon,
setLoadingSharingIconType,
}),
[promptStep, url]
);
return (
<Grid>
<ConnectionPropmpts
isOpen={url === undefined ? true : false}
myDeviceDetails={myDeviceDetails}
isShownTextPrompt={isShownTextPrompt}
promptStep={promptStep}
connectionIconType={connectionIconType}
spinnerIconType={loadingSharingIconType}
isShownSpinnerIcon={isShownLoadingSharingIcon}
/>
<PlayerView
streamUrl={url}
screenSharingSourceType={screenSharingSourceType}
setIsWithControls={setIsWithControls}
isWithControls={isWithControls}
isFullScreenOn={isFullScreenOn}
setIsFullScreenOn={setIsFullScreenOn}
handlePlayPause={handlePlayPause}
isPlaying={playing}
setVideoQuality={setVideoQuality}
videoQuality={videoQuality}
/>
<ErrorDialog
errorMessage={dialogErrorMessage}
isOpen={isErrorDialogOpen}
/>
</Grid>
);
}
export default MainView;

View File

@ -0,0 +1,448 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should match exact snapshot 1`] = `
<div
style={
Object {
"height": "100vh",
"left": 0,
"position": "absolute",
"top": 0,
"width": "100%",
"zIndex": 1,
}
}
>
<div
className="bp3-card bp3-elevation-4"
>
<div
className="row middle-xs between-xs"
>
<div
className="col-xs-12 col-md-3"
>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<div
className="row middle-xs"
style={
Object {
"opacity": "0.75",
}
}
>
<div
className="col-xs-4"
>
<img
alt="logo"
height={42}
src="deskreen_logo_128x128.png"
width={42}
/>
</div>
<div
className="col-xs-8"
>
<h5
className="bp3-heading"
style={
Object {
"marginBottom": "0px",
}
}
>
Deskreen
</h5>
</div>
</div>
</span>
</button>
</span>
</span>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
style={
Object {
"borderRadius": "100px",
}
}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<div
className="row start-xs"
>
<div
className="col-xs"
>
<img
alt="heart"
height={16}
src="red_heart_2764_twemoji_120x120.png"
style={
Object {
"transform": "translateY(2px)",
}
}
width={16}
/>
</div>
<div
className="col-xs"
>
<div
style={
Object {
"transform": "translateY(2px) translateX(-5px)",
}
}
>
<div
className=""
>
Donate!
</div>
</div>
</div>
</div>
</span>
</button>
</span>
</span>
</div>
<div
className="col-xs-12 col-md-6"
>
<div
className="row center-xs"
style={
Object {
"height": "42px",
}
}
>
<div
className="row center-xs middle-xs"
style={
Object {
"backgroundColor": "#137CBD",
"borderRadius": "20px",
"height": "100%",
"width": "190px",
}
}
>
<div
className="row center-xs middle-xs"
style={
Object {
"width": "100%",
}
}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
style={
Object {
"color": "white",
"minWidth": "70px",
"width": "85px",
}
}
type="button"
>
<span
className="bp3-button-text"
>
<div
className="row"
>
<div
className="col-xs"
>
<span
className="bp3-icon bp3-icon-play"
icon="play"
>
<svg
data-icon="play"
fill="white"
height={16}
viewBox="0 0 16 16"
width={16}
>
<desc>
play
</desc>
<path
d="M12 8c0-.35-.19-.64-.46-.82l.01-.02-6-4-.01.02A.969.969 0 005 3c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1 .21 0 .39-.08.54-.18l.01.02 6-4-.01-.02c.27-.18.46-.47.46-.82z"
fillRule="evenodd"
/>
</svg>
</span>
</div>
<div
className="col-xs"
>
<div
className="bp3-text-large"
>
Play
</div>
</div>
</div>
</span>
</button>
<div
className="bp3-divider"
style={
Object {
"borderBottom": "1px solid #ffffffa8",
"borderRight": "1px solid #ffffffa8",
"height": "20px",
}
}
/>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onClick={[Function]}
>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<span
className="bp3-icon bp3-icon-cog"
icon="cog"
>
<svg
data-icon="cog"
fill="white"
height={16}
viewBox="0 0 16 16"
width={16}
>
<desc>
cog
</desc>
<path
d="M15.19 6.39h-1.85c-.11-.37-.27-.71-.45-1.04l1.36-1.36c.31-.31.31-.82 0-1.13l-1.13-1.13a.803.803 0 00-1.13 0l-1.36 1.36c-.33-.17-.67-.33-1.04-.44V.79c0-.44-.36-.8-.8-.8h-1.6c-.44 0-.8.36-.8.8v1.86c-.39.12-.75.28-1.1.47l-1.3-1.3c-.3-.3-.79-.3-1.09 0L1.82 2.91c-.3.3-.3.79 0 1.09l1.3 1.3c-.2.34-.36.7-.48 1.09H.79c-.44 0-.8.36-.8.8v1.6c0 .44.36.8.8.8h1.85c.11.37.27.71.45 1.04l-1.36 1.36c-.31.31-.31.82 0 1.13l1.13 1.13c.31.31.82.31 1.13 0l1.36-1.36c.33.18.67.33 1.04.44v1.86c0 .44.36.8.8.8h1.6c.44 0 .8-.36.8-.8v-1.86c.39-.12.75-.28 1.1-.47l1.3 1.3c.3.3.79.3 1.09 0l1.09-1.09c.3-.3.3-.79 0-1.09l-1.3-1.3c.19-.35.36-.71.48-1.1h1.85c.44 0 .8-.36.8-.8v-1.6a.816.816 0 00-.81-.79zm-7.2 4.6c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"
fillRule="evenodd"
/>
</svg>
</span>
</span>
</button>
</span>
</span>
</span>
</span>
<div
className="bp3-divider"
style={
Object {
"borderBottom": "1px solid #ffffffa8",
"borderRight": "1px solid #ffffffa8",
"height": "20px",
}
}
/>
<span
className="bp3-popover-wrapper"
>
<span
className="bp3-popover-target"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="bp3-button bp3-minimal"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
tabIndex={0}
type="button"
>
<span
className="bp3-button-text"
>
<img
alt="fullscreen-toggle"
height={16}
src="fullscreen_24px.svg"
style={
Object {
"filter": "invert(100%) sepia(100%) saturate(0%) hue-rotate(127deg) brightness(107%) contrast(102%)",
"transform": "scale(1.5) translateY(1px)",
}
}
width={16}
/>
</span>
</button>
</span>
</span>
</div>
</div>
</div>
</div>
<div
className="col-xs-12 col-md-3"
>
<div
className="row end-xs"
>
<div
className="col-xs-12"
>
<label
className="bp3-control bp3-switch bp3-disabled bp3-inline bp3-align-right"
style={
Object {
"marginBottom": "0px",
}
}
>
<input
checked={false}
disabled={true}
onChange={[Function]}
type="checkbox"
/>
<span
className="bp3-control-indicator"
>
<div
className="bp3-control-indicator-child"
>
<div
className="bp3-switch-inner-text"
>
OFF
</div>
</div>
<div
className="bp3-control-indicator-child"
>
<div
className="bp3-switch-inner-text"
>
OFF
</div>
</div>
</span>
Default Video Player
</label>
</div>
</div>
</div>
</div>
</div>
<div
id="video-container"
style={
Object {
"margin": "0 auto",
"position": "relative",
}
}
>
<div
className="player-wrapper"
id="player-wrapper-id"
style={
Object {
"paddingTop": "56.25%",
"position": "relative",
}
}
>
<div
id="react-player-wrapper-id"
style={
Object {
"backgroundColor": "black",
"height": "100%",
"left": 0,
"position": "absolute",
"top": 0,
"width": "100%",
}
}
/>
</div>
<canvas
id="comparison-canvas"
style={
Object {
"display": "none",
}
}
/>
</div>
</div>
`;

View File

@ -0,0 +1,26 @@
import React from 'react';
import renderer from 'react-test-renderer';
import PlayerView from '.';
import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
jest.useFakeTimers();
it('should match exact snapshot', () => {
const subject = renderer.create(
<>
<PlayerView
isWithControls={false}
setIsWithControls={() => {}}
isFullScreenOn={false}
setIsFullScreenOn={() => {}}
handlePlayPause={() => {}}
isPlaying={false}
setVideoQuality={() => {}}
videoQuality={VideoQuality.Q_100_PERCENT}
screenSharingSourceType={'screen'}
streamUrl={undefined}
/>
</>
);
expect(subject).toMatchSnapshot();
});

View File

@ -0,0 +1,116 @@
import { Position, Toaster } from '@blueprintjs/core';
import React, { useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import screenfull from 'screenfull';
import PlayerControlPanel from '../../components/PlayerControlPanel';
import {
COMPARISON_CANVAS_ID,
REACT_PLAYER_WRAPPER_ID,
} from '../../constants/appConstants';
import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum';
interface PlayerViewProps {
isWithControls: boolean;
setIsWithControls: (_: boolean) => void;
isFullScreenOn: boolean;
setIsFullScreenOn: (_: boolean) => void;
handlePlayPause: () => void;
isPlaying: boolean;
setVideoQuality: (_: VideoQuality) => void;
videoQuality: VideoQuality;
screenSharingSourceType: ScreenSharingSourceType;
streamUrl: undefined | MediaStream;
}
function PlayerView(props: PlayerViewProps) {
const {
screenSharingSourceType,
setIsWithControls,
isWithControls,
isFullScreenOn,
setIsFullScreenOn,
handlePlayPause,
isPlaying,
setVideoQuality,
videoQuality,
streamUrl,
} = props;
const player = useRef(null);
const [toaster, setToaster] = useState<undefined | Toaster>();
const refHandlers = {
toaster: (ref: Toaster) => {
setToaster(ref);
},
};
return (
<div
style={{
position: 'absolute',
zIndex: 1,
top: 0,
left: 0,
width: '100%',
height: '100vh',
}}
>
<PlayerControlPanel
onSwitchChangedCallback={(isEnabled) => setIsWithControls(isEnabled)}
isDefaultPlayerTurnedOn={isWithControls}
handleClickFullscreen={() => {
if (!screenfull.isEnabled) return;
const playerElement = document.querySelector(`#${REACT_PLAYER_WRAPPER_ID}`);
if (!playerElement) return;
screenfull.request(playerElement);
setIsFullScreenOn(!isFullScreenOn);
}}
handleClickPlayPause={handlePlayPause}
isPlaying={isPlaying}
setVideoQuality={setVideoQuality}
selectedVideoQuality={videoQuality}
screenSharingSourceType={screenSharingSourceType}
toaster={toaster}
/>
<div
id="video-container"
style={{
margin: '0 auto',
position: 'relative',
}}
>
<div
id="player-wrapper-id"
className="player-wrapper"
style={{
position: 'relative',
paddingTop: '56.25%',
}}
>
<ReactPlayer
ref={player}
id={REACT_PLAYER_WRAPPER_ID}
playing={isPlaying}
playsinline={true}
controls={isWithControls}
muted={true}
url={streamUrl}
width="100%"
height="100%"
style={{
position: 'absolute',
top: 0,
left: 0,
backgroundColor: 'black',
}}
/>
</div>
<canvas id={COMPARISON_CANVAS_ID} style={{ display: 'none' }}></canvas>
</div>
<Toaster ref={refHandlers.toaster} position={Position.TOP_LEFT} />
</div>
);
}
export default PlayerView;

View File

@ -0,0 +1 @@
export default { username: '', publicKey: '', privateKey: '' };

View File

@ -0,0 +1,4 @@
interface PartnerPeerUser {
username: string;
publicKey: string;
}

View File

@ -0,0 +1,41 @@
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
export default class PeerConnectionUIHandler {
isDarkTheme: boolean;
setMyDeviceDetails: (details: DeviceDetails) => void;
hostAllowedToConnectCallback: () => void;
setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void;
setIsDarkThemeCallback: (val: boolean) => void;
setAppLanguageCallback: (newLang: string) => void;
setDialogErrorMessageCallback: (message: ErrorMessage) => void;
setIsErrorDialogOpen: (val: boolean) => void;
errorDialogMessage = ErrorMessage.UNKNOWN_ERROR;
constructor(
isDarkTheme: boolean,
setMyDeviceDetails: (details: DeviceDetails) => void,
hostAllowedToConnectCallback: () => void,
setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void,
setIsDarkThemeCallback: (val: boolean) => void,
setAppLanguageCallback: (newLang: string) => void,
setDialogErrorMessageCallback: (message: ErrorMessage) => void,
setIsErrorDialogOpen: (val: boolean) => void
) {
this.isDarkTheme = isDarkTheme;
this.hostAllowedToConnectCallback = hostAllowedToConnectCallback;
this.setMyDeviceDetails = setMyDeviceDetails;
this.setScreenSharingSourceTypeCallback = setScreenSharingSourceTypeCallback;
this.setIsDarkThemeCallback = setIsDarkThemeCallback;
this.setAppLanguageCallback = setAppLanguageCallback;
this.setDialogErrorMessageCallback = setDialogErrorMessageCallback;
this.setIsErrorDialogOpen = setIsErrorDialogOpen;
}
}

View File

@ -0,0 +1,6 @@
interface ReceiveEncryptedMessagePayload {
payload: string;
signature: string;
iv: string;
keys: { sessionKey: string; signingKey: string }[];
}

View File

@ -0,0 +1,7 @@
export default class PeerConnectionPartnerIsNotDefinedError extends Error {
constructor() {
super('partner should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionPartnerIsNotDefinedError.prototype);
}
}

View File

@ -0,0 +1,7 @@
export default class PeerConnectionPeerIsNullError extends Error {
constructor() {
super('peer of PeerConnection should not be null!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionPeerIsNullError.prototype);
}
}

View File

@ -0,0 +1,7 @@
export default class PeerConnectionSocketNotDefined extends Error {
constructor() {
super('socket should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionSocketNotDefined.prototype);
}
}

View File

@ -0,0 +1,7 @@
export default class PeerConnectionUserIsNotDefinedError extends Error {
constructor() {
super('user should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, PeerConnectionUserIsNotDefinedError.prototype);
}
}

View File

@ -0,0 +1,237 @@
jest.useFakeTimers();
jest.mock('../../utils/crypto.ts');
jest.mock('../VideoAutoQualityOptimizer');
jest.mock('simple-peer');
jest.mock('./setAndShowErrorDialogMessage');
jest.mock('./peerConnectionReceiveEncryptedMessage');
import SimplePeer from 'simple-peer';
import PeerConnection from '.';
import { VIDEO_QUALITY_TO_DECIMAL } from '../../constants/appConstants';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum';
import NullUser from './NullUser';
import PeerConnectionPartnerIsNotDefinedError from './errors/PeerConnectionPartnerIsNotDefinedError';
import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
import { prepareDataMessageToChangeQuality } from './simplePeerDataMessages';
import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage';
const SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD = {
type: 'DUMMY_MESSAGE',
payload: {},
};
const RECEIVE_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD = {
payload: '',
signature: '',
iv: '',
keys: [{ sessionKey: '', signingKey: '' }],
};
describe('PeerConnection class', () => {
let peerConnection: PeerConnection;
beforeEach(() => {
peerConnection = new PeerConnection(
'123',
jest.fn(),
new Crypto(),
new VideoAutoQualityOptimizer(),
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn()
)
);
peerConnection.peer = new SimplePeer();
peerConnection.peer.send = jest.fn();
const listeners = new Map<string, any[]>();
peerConnection.socket = {
on: jest.fn().mockImplementation((s: string, callback: any) => {
if (!listeners.has(s)) {
listeners.set(s, []);
}
listeners.get(s)?.push(callback);
}),
emit: jest.fn().mockImplementation((s: string, any: any) => {
listeners.forEach((callbacks, key) => {
callbacks.forEach((callback) => {
if (key === s) {
callback(any);
}
});
});
}),
};
jest.clearAllMocks();
jest.restoreAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('when new PeerConnection is created with not corrent roomId', () => {
it('should change UI accordingly and notify user that error occured', () => {
peerConnection = new PeerConnection(
'',
jest.fn(),
new Crypto(),
new VideoAutoQualityOptimizer(),
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn()
)
);
expect(setAndShowErrorDialogMessage).toBeCalled();
});
});
describe('when new PeerConnection was created properly', () => {
describe('when setVideoQuality is called properly', () => {
it('should set video quality properly', () => {
peerConnection.videoQualityChangedCallback = jest.fn();
const newVideoQuality = VideoQuality.Q_100_PERCENT;
peerConnection.setVideoQuality(newVideoQuality);
expect(peerConnection.videoQualityChangedCallback).toBeCalled();
expect(peerConnection.videoQuality).toBe(newVideoQuality);
});
});
describe('when videoQualityChangedCallback is called and videoQuality of peer is VideoQuality.Q_AUTO', () => {
it('should call peerConnection.peer.send with proper message', () => {
peerConnection.videoQuality = VideoQuality.Q_AUTO;
peerConnection.videoQualityChangedCallback();
expect(peerConnection.peer?.send).toBeCalledWith(
prepareDataMessageToChangeQuality(1)
);
});
});
describe('when videoQualityChangedCallback is called and videoQuality of peer is NOT VideoQuality.Q_AUTO', () => {
it('should call peerConnection.peer.send with proper message', () => {
peerConnection.videoQuality = VideoQuality.Q_25_PERCENT;
peerConnection.videoQualityChangedCallback();
expect(peerConnection.peer?.send).toBeCalledWith(
prepareDataMessageToChangeQuality(
VIDEO_QUALITY_TO_DECIMAL[peerConnection.videoQuality]
)
);
});
});
describe('when initApp is called when socket is not defined', () => {
it('should throw an appropriate error', () => {
peerConnection.socket = undefined;
try {
peerConnection.initApp(NullUser, '');
fail('it should have thrown an error here');
} catch (e) {
expect(e).toEqual(new PeerConnectionSocketNotDefined());
}
});
});
describe('when initApp is called properly', () => {
it('should call emit on socket with USER_ENTER', () => {
peerConnection.initApp(NullUser, '');
expect(peerConnection.socket.emit).toBeCalledWith(
'USER_ENTER',
expect.anything()
);
});
});
describe('when sendEncryptedMessage is called when socket is NOT defined', () => {
it('should throw an appropriate error', () => {
peerConnection.socket = undefined;
try {
peerConnection.sendEncryptedMessage(
SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD
);
fail('it should have thrown an error here');
} catch (e) {
expect(e).toEqual(new PeerConnectionSocketNotDefined());
}
});
});
describe('when sendEncryptedMessage is called when user is NOT defined', () => {
it('should throw an appropriate error', () => {
try {
peerConnection.sendEncryptedMessage(
SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD
);
fail('it should have thrown an error here');
} catch (e) {
expect(e).toEqual(new PeerConnectionUserIsNotDefinedError());
}
});
});
describe('when sendEncryptedMessage is called when partner is NOT defined', () => {
it('should throw an appropriate error', () => {
peerConnection.user = {
username: 'af',
privateKey: 'af',
publicKey: 'af',
};
try {
peerConnection.sendEncryptedMessage(
SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD
);
fail('it should have thrown an error here');
} catch (e) {
expect(e).toEqual(new PeerConnectionPartnerIsNotDefinedError());
}
});
});
describe('when receiveEncryptedMessage is called', () => {
it('should call peerConnectionReceiveEncryptedMessage callback', () => {
peerConnection.receiveEncryptedMessage(
RECEIVE_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD
);
expect(peerConnectionReceiveEncryptedMessage).toBeCalled();
});
});
describe('when createUserAndInitSocket is called and when socket is NOT defined', () => {
it('should throw an appropriate error', () => {
peerConnection.socket = undefined;
try {
peerConnection.createUserAndInitSocket();
fail('it should have thrown an error here');
} catch (e) {
expect(e).toEqual(new PeerConnectionSocketNotDefined());
}
});
});
});
});

View File

@ -1,25 +1,26 @@
import shortId from 'shortid';
// import pixelmatch from 'pixelmatch';
import SimplePeer from 'simple-peer';
import { UAParser } from 'ua-parser-js';
import { connect as connectSocket } from '../../utils/socket';
import {
prepare as prepareMessage,
process as processMessage,
} from '../../utils/message';
import { prepare as prepareMessage } from '../../utils/message';
import setSdpMediaBitrate from './setSdpMediaBitrate';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import {
getBrowserFromUAParser,
getDeviceTypeFromUAParser,
getOSFromUAParser,
} from '../../utils/userAgentParserHelpers';
import { VideoQuality } from './VideoQualityEnum';
import prepareDataMessageToChangeQuality from './prepareDataMessageToChangeQuality';
import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum';
import { prepareDataMessageToChangeQuality } from './simplePeerDataMessages';
import { VIDEO_QUALITY_TO_DECIMAL } from './../../constants/appConstants';
import prepareDataMessageToGetSharingSourceType from './prepareDataMessageToGetSharingSourceType';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import areWeTestingWithJest from '../../utils/areWeTestingWithJest';
import peerConnectionHandleSocket from './peerConnectionHandleSocket';
import peerConnectionHandlePeer from './peerConnectionHandlePeer';
import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage';
import startSocketConnectedCheckingLoop from './startSocketConnectedCheckingLoop';
import NullUser from './NullUser';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';
import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';
import PeerConnectionPartnerIsNotDefinedError from './errors/PeerConnectionPartnerIsNotDefinedError';
interface LocalPeerUser {
username: string;
@ -27,25 +28,11 @@ interface LocalPeerUser {
publicKey: string;
}
interface PartnerPeerUser {
username: string;
publicKey: string;
}
const nullUser = { username: '', publicKey: '', privateKey: '' };
interface SendEncryptedMessagePayload {
type: string;
payload: Record<string, unknown>;
}
interface ReceiveEncryptedMessagePayload {
payload: string;
signature: string;
iv: string;
keys: { sessionKey: string; signingKey: string }[];
}
export default class PeerConnection {
roomId: string;
@ -53,33 +40,22 @@ export default class PeerConnection {
crypto: Crypto;
user: LocalPeerUser = nullUser;
user: LocalPeerUser = NullUser;
partner: PartnerPeerUser = nullUser;
partner: PartnerPeerUser = NullUser;
peer: null | SimplePeer.Instance = null;
myIP = '';
myDeviceDetails: DeviceDetails = {
myIP: '',
myOS: '',
myDeviceType: '',
myBrowser: '',
};
myOS: any;
setUrlCallback: (url: any) => void;
myDeviceType: any;
myBrowser: any;
mousePos: any;
setUrlCallback: any;
private uaParser: UAParser;
canvas: any;
video: any;
prevFrame: any;
largeMismatchFramesCount: number;
uaParser: UAParser;
screenSharingSourceType: string | undefined = undefined;
@ -87,74 +63,32 @@ export default class PeerConnection {
videoAutoQualityOptimizer: VideoAutoQualityOptimizer;
isDarkTheme: boolean;
isStreamStarted: boolean = false;
setMyDeviceDetails: (details: DeviceDetails) => void;
hostAllowedToConnectCallback: () => void;
setScreenSharingSourceTypeCallback: (s: 'screen' | 'window') => void;
setIsDarkThemeCallback: (val: boolean) => void;
setAppLanguageCallback: (newLang: string) => void;
setDialogErrorMessageCallback: (message: ErrorMessage) => void;
setIsErrorDialogOpen: (val: boolean) => void;
errorDialogMessage = ErrorMessage.UNKNOWN_ERROR;
UIHandler: PeerConnectionUIHandler;
constructor(
setUrlCallback: any,
roomId: string,
setUrlCallback: (url: any) => void,
crypto: Crypto,
videoAutoQualityOptimizer: VideoAutoQualityOptimizer,
isDarkTheme: boolean,
setMyDeviceDetailsCallback: (details: DeviceDetails) => void,
hostAllowedToConnectCallback: () => void,
setScreenSharingSourceTypeCallback: (s: 'screen' | 'window') => void,
setIsDarkThemeCallback: (val: boolean) => void,
setAppLanguageCallback: (newLang: string) => void,
setDialogErrorMessageCallback: (message: ErrorMessage) => void,
setIsErrorDialogOpen: (val: boolean) => void
UIHandler: PeerConnectionUIHandler
) {
this.setUrlCallback = setUrlCallback;
this.crypto = crypto;
this.videoAutoQualityOptimizer = videoAutoQualityOptimizer;
this.isDarkTheme = isDarkTheme;
this.setMyDeviceDetails = setMyDeviceDetailsCallback;
this.hostAllowedToConnectCallback = hostAllowedToConnectCallback;
this.roomId = encodeURI(window.location.pathname.replace('/', ''));
this.UIHandler = UIHandler;
this.roomId = roomId;
this.socket = connectSocket(this.roomId);
this.uaParser = new UAParser();
this.createUserAndInitSocket();
this.createPeer();
this.setScreenSharingSourceTypeCallback = setScreenSharingSourceTypeCallback;
this.setIsDarkThemeCallback = setIsDarkThemeCallback;
this.setAppLanguageCallback = setAppLanguageCallback;
this.setDialogErrorMessageCallback = setDialogErrorMessageCallback;
this.setIsErrorDialogOpen = setIsErrorDialogOpen;
this.video = null;
this.canvas = null;
this.largeMismatchFramesCount = 0;
if (!this.roomId || this.roomId === '') {
setDialogErrorMessageCallback(ErrorMessage.UNKNOWN_ERROR);
setIsErrorDialogOpen(true);
setAndShowErrorDialogMessage(this, ErrorMessage.NOT_ALLOWED);
}
setInterval(() => {
if (!this.socket.connected) {
if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) {
this.setDialogErrorMessageCallback(ErrorMessage.DENY_TO_CONNECT);
this.setIsErrorDialogOpen(true);
this.errorDialogMessage = ErrorMessage.DENY_TO_CONNECT;
}
}
}, 2000);
startSocketConnectedCheckingLoop(this);
}
setVideoQuality(videoQuality: VideoQuality) {
@ -162,28 +96,25 @@ export default class PeerConnection {
this.videoQualityChangedCallback();
}
setErrorDialogMessage(message: ErrorMessage) {
this.errorDialogMessage = message;
}
videoQualityChangedCallback() {
if (this.videoQuality !== VideoQuality.Q_AUTO) {
if (this.videoQuality === VideoQuality.Q_AUTO) {
this.peer?.send(prepareDataMessageToChangeQuality(1));
} else {
this.peer?.send(
prepareDataMessageToChangeQuality(
VIDEO_QUALITY_TO_DECIMAL[this.videoQuality]
)
);
} else {
this.peer?.send(prepareDataMessageToChangeQuality(1));
}
}
createPeer() {
// When we are testing with jest, SimplePeer() can not be created, so we just return
if (areWeTestingWithJest()) return;
const peer = new SimplePeer({
initiator: false,
// trickle: true,
// stream: null,
// allowHalfTrickle: false,
config: { iceServers: [] },
sdpTransform: (sdp) => {
let newSDP = sdp;
@ -196,58 +127,14 @@ export default class PeerConnection {
},
});
peer.on('stream', (stream) => {
this.videoAutoQualityOptimizer.setGoodQualityCallback(() => {
if (this.videoQuality === VideoQuality.Q_AUTO) {
this.peer?.send(prepareDataMessageToChangeQuality(1));
}
});
this.videoAutoQualityOptimizer.setHalfQualityCallbak(() => {
if (this.videoQuality === VideoQuality.Q_AUTO) {
this.peer?.send(prepareDataMessageToChangeQuality(0.5));
}
});
this.videoAutoQualityOptimizer.startOptimizationLoop();
this.setUrlCallback(stream);
setTimeout(() => {
this.peer?.send(prepareDataMessageToGetSharingSourceType());
}, 1000);
this.isStreamStarted = true;
});
peer.on('signal', (data) => {
// fired when webrtc done preparation to start call on this machine
this.sendEncryptedMessage({
type: 'CALL_ACCEPTED',
payload: {
signalData: data,
},
});
});
peer.on('data', (data) => {
const dataJSON = JSON.parse(data);
if (dataJSON.type === 'screen_sharing_source_type') {
this.screenSharingSourceType = dataJSON.payload.value;
if (
this.screenSharingSourceType === 'screen' ||
this.screenSharingSourceType === 'window'
) {
this.setScreenSharingSourceTypeCallback(this.screenSharingSourceType);
}
}
});
this.peer = peer;
peerConnectionHandlePeer(this);
}
initApp(user: LocalPeerUser, myIP: string) {
if (!this.socket) return;
if (!this.socket) {
throw new PeerConnectionSocketNotDefined();
}
this.socket.emit('USER_ENTER', {
username: user.username,
publicKey: user.publicKey,
@ -275,152 +162,36 @@ export default class PeerConnection {
});
}
async sendEncryptedMessage(payload: SendEncryptedMessagePayload) {
if (!this.socket) return;
if (!this.user) return;
if (!this.partner) return;
const msg = (await prepareMessage(payload, this.user, this.partner)) as any;
this.socket.emit('ENCRYPTED_MESSAGE', msg.toSend);
sendEncryptedMessage(payload: SendEncryptedMessagePayload) {
if (!this.socket) {
throw new PeerConnectionSocketNotDefined();
}
if (!this.user || this.user === NullUser) {
throw new PeerConnectionUserIsNotDefinedError();
}
if (!this.partner || this.partner === NullUser) {
throw new PeerConnectionPartnerIsNotDefinedError();
}
prepareMessage(payload, this.user, this.partner).then((msg: any) => {
this.socket.emit('ENCRYPTED_MESSAGE', msg.toSend);
})
}
async receiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload) {
if (!this.user) return;
const message = (await processMessage(
payload,
this.user.privateKey
)) as any;
if (message.type === 'CALL_USER') {
this.peer?.signal(message.payload.signalData);
}
if (message.type === 'DENY_TO_CONNECT') {
if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) {
this.setDialogErrorMessageCallback(ErrorMessage.DENY_TO_CONNECT);
this.setIsErrorDialogOpen(true);
this.errorDialogMessage = ErrorMessage.DENY_TO_CONNECT;
}
}
if (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') {
if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) {
this.setDialogErrorMessageCallback(ErrorMessage.DICONNECTED);
this.setIsErrorDialogOpen(true);
this.errorDialogMessage = ErrorMessage.DICONNECTED;
}
}
if (message.type === 'ALLOWED_TO_CONNECT') {
this.hostAllowedToConnectCallback();
}
if (message.type === 'APP_THEME') {
if (this.isDarkTheme !== message.payload.value) {
this.setIsDarkThemeCallback(message.payload.value);
this.isDarkTheme = message.payload.value;
}
}
if (message.type === 'APP_LANGUAGE') {
this.setAppLanguageCallback(message.payload.value);
}
receiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload) {
peerConnectionReceiveEncryptedMessage(this, payload);
}
createUserAndInitSocket() {
if (!this.socket) return;
if (!this.socket) {
throw new PeerConnectionSocketNotDefined();
}
this.socket.removeAllListeners();
const userCreatedCallback = (createdUser: LocalPeerUser) => {
this.user = createdUser;
this.socket.on('disconnect', () => {
// this.props.toggleSocketConnected(false);
if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) {
this.setDialogErrorMessageCallback(ErrorMessage.DICONNECTED);
this.setIsErrorDialogOpen(true);
this.errorDialogMessage = ErrorMessage.DICONNECTED;
}
});
this.socket.on('connect', () => {
this.socket.emit('GET_MY_IP', (ip: string) => {
this.myIP = ip;
this.uaParser.setUA(window.navigator.userAgent);
this.myOS = getOSFromUAParser(this.uaParser);
this.myDeviceType = getDeviceTypeFromUAParser(this.uaParser);
this.myBrowser = getBrowserFromUAParser(this.uaParser);
this.initApp(createdUser, ip);
});
});
this.socket.on('NOT_ALLOWED', () => {
if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) {
this.setDialogErrorMessageCallback(ErrorMessage.NOT_ALLOWED);
this.setIsErrorDialogOpen(true);
this.errorDialogMessage = ErrorMessage.NOT_ALLOWED;
}
});
this.socket.on('USER_ENTER', (payload: { users: PartnerPeerUser[] }) => {
const filteredPartner = payload.users.filter((v) => {
return createdUser.publicKey !== v.publicKey;
});
this.partner = filteredPartner[0];
this.sendEncryptedMessage({
type: 'ADD_USER',
payload: {
username: createdUser.username,
publicKey: createdUser.publicKey,
isOwner: true,
id: createdUser.username,
},
});
// TODO: send device details as strings here!
this.sendEncryptedMessage({
type: 'DEVICE_DETAILS',
payload: {
socketID: this.socket.io.engine.id,
os: this.myOS,
deviceType: this.myDeviceType,
browser: this.myBrowser,
deviceScreenWidth: window.screen.width,
deviceScreenHeight: window.screen.height,
},
});
this.sendEncryptedMessage({ type: 'GET_APP_THEME', payload: {} });
this.sendEncryptedMessage({ type: 'GET_APP_LANGUAGE', payload: {} });
setTimeout(() => {
this.setMyDeviceDetails({
myIP: this.myIP,
myOS: this.myOS,
myBrowser: this.myBrowser,
myDeviceType: this.myDeviceType,
});
}, 100);
});
this.socket.on('USER_EXIT', (payload: any) => {
// this.props.receiveUnencryptedMessage('USER_EXIT', payload);
});
this.socket.on(
'ENCRYPTED_MESSAGE',
(payload: ReceiveEncryptedMessagePayload) => {
this.receiveEncryptedMessage(payload);
}
);
this.socket.on('ROOM_LOCKED', (payload: any) => {
// TODO: call ROOM LOCKED callback to change react component contain ROOM LOCKED message
// @ts-ignore
// document.querySelector('#my-ip')?.innerHTML = 'ROOM LOCKED';
if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) {
this.setDialogErrorMessageCallback(ErrorMessage.DENY_TO_CONNECT);
this.setIsErrorDialogOpen(true);
this.errorDialogMessage = ErrorMessage.UNKNOWN_ERROR;
}
});
peerConnectionHandleSocket(this);
window.addEventListener('beforeunload', (_) => {
this.socket.emit('USER_DISCONNECT');

View File

@ -0,0 +1,2 @@
export const INPUTtestWindowNavigatorUserAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36';

View File

@ -0,0 +1,109 @@
export const INPUTtestSdpMediaBitrate =
`
v=0
o=- 5730467698688819135 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
b=AS:30
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:1
a=sctp-port:5000
a=max-message-size:262144
`;

View File

@ -0,0 +1,6 @@
export const OUTPUTDeviceDetailsFromUAParsed: DeviceDetails = {
myBrowser: 'Chrome 87.0.4280.88',
myDeviceType: 'computer',
myIP: '123.123.123.123',
myOS: 'Mac OS 10.15.6',
};

View File

@ -0,0 +1,110 @@
export const OUTPUTtestSdpMediaBitrate =
`
v=0
o=- 5730467698688819135 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116
c=IN IP4 0.0.0.0
b=AS:500000
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
b=AS:30
a=ice-ufrag:PY+h
a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc
a=ice-options:trickle
a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D
a=setup:active
a=mid:1
a=sctp-port:5000
a=max-message-size:262144
`;

View File

@ -0,0 +1,300 @@
import SimplePeer from 'simple-peer';
import PeerConnection from '.';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum';
import peerConnectionHandlePeer, {
getSharingShourceType,
} from './peerConnectionHandlePeer';
import PeerConnectionPeerIsNullError from './errors/PeerConnectionPeerIsNullError';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import {
prepareDataMessageToChangeQuality,
prepareDataMessageToGetSharingSourceType,
} from './simplePeerDataMessages';
jest.useFakeTimers();
jest.mock('../../utils/crypto.ts');
jest.mock('simple-peer', () => {
return jest.fn().mockImplementation(() => {
const listeners = new Map<string, any[]>();
return {
...jest.requireActual('simple-peer'),
on: jest.fn().mockImplementation((s: string, callback: any) => {
if (!listeners.has(s)) {
listeners.set(s, []);
}
listeners.get(s)?.push(callback);
}),
emit: jest.fn().mockImplementation((s: string, any: any) => {
listeners.forEach((callbacks, key) => {
callbacks.forEach((callback) => {
if (key === s) {
callback(any);
}
});
});
}),
send: jest.fn().mockImplementation((_: string) => {}),
};
});
});
const screen_sharing_source_type_JSON_DATA =
' \
{ \
"type": "screen_sharing_source_type", \
"payload": { \
"value": "screen" \
} \
} \
';
const window_sharing_source_type_JSON_DATA =
' \
{ \
"type": "screen_sharing_source_type", \
"payload": { \
"value": "window" \
} \
} \
';
describe('peerConnectionHandlePeer callback', () => {
let peerConnection: PeerConnection;
let videoQualityOptimizer: VideoAutoQualityOptimizer;
const setURLCallbackMock = jest.fn();
beforeEach(() => {
videoQualityOptimizer = new VideoAutoQualityOptimizer();
jest.spyOn(videoQualityOptimizer, 'startOptimizationLoop');
jest.spyOn(videoQualityOptimizer, 'goodQualityCallback');
jest.spyOn(videoQualityOptimizer, 'halfQualityCallbak');
peerConnection = new PeerConnection(
'123',
setURLCallbackMock,
new Crypto(),
videoQualityOptimizer,
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn()
)
);
jest.spyOn(peerConnection, 'sendEncryptedMessage');
peerConnection.peer = new SimplePeer();
peerConnection.user = {
username: 'af',
privateKey: 'af',
publicKey: 'af',
};
peerConnection.partner = {
username: 'af',
publicKey: 'af',
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe('when peerConnection with peer null passed as parameter', () => {
function callPeerConnectionHandlePeerWithPeerNull() {
peerConnection.peer = null;
peerConnectionHandlePeer(peerConnection);
}
it('should throw an error', () => {
peerConnection.peer = null;
expect(callPeerConnectionHandlePeerWithPeerNull).toThrow(
new PeerConnectionPeerIsNullError()
);
});
});
describe('when peerConnectionHandlePeer() is called properly', () => {
it('should have set .on("stream", () => {}) callback on PeerConnection.peer', () => {
jest.requireMock('simple-peer');
peerConnectionHandlePeer(peerConnection);
expect(peerConnection.peer?.on).toBeCalledWith(
'stream',
expect.anything()
);
});
it('should have set .on("data", () => {}) callback on PeerConnection.peer', () => {
jest.requireMock('simple-peer');
peerConnectionHandlePeer(peerConnection);
expect(peerConnection.peer?.on).toBeCalledWith('data', expect.anything());
});
it('should have set .on("signal", () => {}) callback on PeerConnection.peer', () => {
jest.requireMock('simple-peer');
peerConnectionHandlePeer(peerConnection);
expect(peerConnection.peer?.on).toBeCalledWith(
'signal',
expect.anything()
);
});
describe('when "stream" event occured', () => {
it('should start video quality optimization loop', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('stream');
expect(
peerConnection.videoAutoQualityOptimizer.startOptimizationLoop
).toBeCalled();
});
it('should call getSharingShourceType function to get sharing source type from host', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('stream');
expect(setTimeout).toHaveBeenLastCalledWith(
getSharingShourceType,
1000,
peerConnection
);
});
describe('when quality is AUTO and when video quality optimizer requiests GOOD quality', () => {
it('should call .send with proper data message', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('stream');
peerConnection.videoAutoQualityOptimizer.goodQualityCallback();
expect(peerConnection.videoQuality).toBe(VideoQuality.Q_AUTO);
expect(peerConnection.peer?.send).toBeCalledWith(
prepareDataMessageToChangeQuality(1)
);
});
});
describe('when quality is NOT AUTO and when video quality optimizer requiests GOOD quality', () => {
it('should call NOT .send with proper data message', () => {
peerConnection.videoQuality = VideoQuality.Q_25_PERCENT;
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('stream');
peerConnection.videoAutoQualityOptimizer.goodQualityCallback();
expect(peerConnection.peer?.send).not.toBeCalled();
});
});
describe('when quality is AUTO and when video quality optimizer requiests HALF quality', () => {
it('should call .send with proper data message', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('stream');
peerConnection.videoAutoQualityOptimizer.halfQualityCallbak();
expect(peerConnection.videoQuality).toBe(VideoQuality.Q_AUTO);
expect(peerConnection.peer?.send).toBeCalledWith(
prepareDataMessageToChangeQuality(0.5)
);
});
});
});
describe('when quality is NOT AUTO and when video quality optimizer requiests GOOD quality', () => {
it('should call NOT .send with proper data message', () => {
peerConnection.videoQuality = VideoQuality.Q_25_PERCENT;
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('stream');
peerConnection.videoAutoQualityOptimizer.halfQualityCallbak();
expect(peerConnection.peer?.send).not.toBeCalled();
});
});
describe('when "signal" event occured', () => {
it('should call sendEncryptedMessage on peer connection', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit('signal');
expect(peerConnection.sendEncryptedMessage).toBeCalled();
});
});
describe('when "data" event occured', () => {
describe('when data.type is "screen_sharing_source_type"', () => {
describe('when dataJSON.payload.value === screen', () => {
it('should call UIHandler.setScreenSharingSourceTypeCallback with "screen" string as parameter', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit(
'data',
screen_sharing_source_type_JSON_DATA
);
expect(
peerConnection.UIHandler.setScreenSharingSourceTypeCallback
).toBeCalledWith('screen');
});
});
describe('when dataJSON.payload.value === window', () => {
it('should call UIHandler.setScreenSharingSourceTypeCallback with "window" string as parameter', () => {
peerConnectionHandlePeer(peerConnection);
peerConnection.peer?.emit(
'data',
window_sharing_source_type_JSON_DATA
);
expect(
peerConnection.UIHandler.setScreenSharingSourceTypeCallback
).toBeCalledWith('window');
});
});
describe('when dataJSON.payload.value is NOT "screen" or "window"', () => {
it('should do nothing', () => {
expect(
peerConnection.UIHandler.setScreenSharingSourceTypeCallback
).not.toBeCalled();
});
});
});
describe('when data.type is NOT "screen_sharing_source_type"', () => {
it('should do nothing', () => {
expect(
peerConnection.UIHandler.setScreenSharingSourceTypeCallback
).not.toBeCalled();
});
});
});
});
describe('when getSharingSourceType is called properly', () => {
it('should call .send method with proper payload to get sharing source type from host', () => {
getSharingShourceType(peerConnection);
expect(peerConnection.peer?.send).toBeCalledWith(
prepareDataMessageToGetSharingSourceType()
);
});
});
});

View File

@ -0,0 +1,76 @@
import PeerConnection from '.';
import {
prepareDataMessageToChangeQuality,
prepareDataMessageToGetSharingSourceType,
} from './simplePeerDataMessages';
import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum';
import PeerConnectionPeerIsNullError from './errors/PeerConnectionPeerIsNullError';
export function getSharingShourceType(peerConnection: PeerConnection) {
try {
peerConnection.peer?.send(prepareDataMessageToGetSharingSourceType());
} catch (e) {
console.log(e);
}
}
export default (peerConnection: PeerConnection) => {
if (peerConnection.peer === null) {
throw new PeerConnectionPeerIsNullError();
}
peerConnection.peer.on('stream', (stream) => {
peerConnection.setUrlCallback(stream);
peerConnection.videoAutoQualityOptimizer.setGoodQualityCallback(() => {
if (peerConnection.videoQuality === VideoQuality.Q_AUTO) {
try {
peerConnection.peer?.send(prepareDataMessageToChangeQuality(1));
} catch (e) {
console.log(e);
}
}
});
peerConnection.videoAutoQualityOptimizer.setHalfQualityCallbak(() => {
if (peerConnection.videoQuality === VideoQuality.Q_AUTO) {
try {
peerConnection.peer?.send(prepareDataMessageToChangeQuality(0.5));
} catch (e) {
console.log(e);
}
}
});
peerConnection.videoAutoQualityOptimizer.startOptimizationLoop();
setTimeout(getSharingShourceType, 1000, peerConnection);
peerConnection.isStreamStarted = true;
});
peerConnection.peer.on('signal', (data) => {
// fired when webrtc done preparation to start call on peerConnection machine
peerConnection.sendEncryptedMessage({
type: 'CALL_ACCEPTED',
payload: {
signalData: data,
},
});
});
peerConnection.peer.on('data', (data) => {
const dataJSON = JSON.parse(data);
if (dataJSON.type === 'screen_sharing_source_type') {
peerConnection.screenSharingSourceType = dataJSON.payload.value;
if (
peerConnection.screenSharingSourceType === 'screen' ||
peerConnection.screenSharingSourceType === 'window'
) {
peerConnection.UIHandler.setScreenSharingSourceTypeCallback(
peerConnection.screenSharingSourceType
);
}
}
});
};

View File

@ -0,0 +1,241 @@
import { OUTPUTDeviceDetailsFromUAParsed } from './mocks/OUTPUTDeviceDetailsFromUAParsed';
import { INPUTtestWindowNavigatorUserAgent } from './mocks/INPUTtestWindowNavigatorUserAgent';
jest.mock('./setAndShowErrorDialogMessage');
// import SimplePeer from 'simple-peer';
import PeerConnection from '.';
// import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import peerConnectionHandleSocket, {
getMyIPCallback,
} from './peerConnectionHandleSocket';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';
jest.useFakeTimers();
// jest.mock('.');
jest.mock('../../utils/crypto.ts');
const TEST_IP = '123.123.123.123';
describe('peerConnectionHandleSocket callback', () => {
let peerConnection: PeerConnection;
beforeEach(() => {
peerConnection = new PeerConnection(
'123',
jest.fn(),
new Crypto(),
new VideoAutoQualityOptimizer(),
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn()
)
);
peerConnection.initApp = jest.fn();
const listeners = new Map<string, any[]>();
peerConnection.socket = {
on: jest.fn().mockImplementation((s: string, callback: any) => {
if (!listeners.has(s)) {
listeners.set(s, []);
}
listeners.get(s)?.push(callback);
}),
emit: jest.fn().mockImplementation((s: string, any: any) => {
listeners.forEach((callbacks, key) => {
callbacks.forEach((callback) => {
if (key === s) {
callback(any);
}
});
});
}),
io: {
engine: {
id: '434241'
}
}
};
peerConnection.receiveEncryptedMessage = jest.fn();
peerConnection.sendEncryptedMessage = jest.fn();
jest.clearAllMocks();
jest.restoreAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('when peerConnectionHandleSocket is called with .socket undefined', () => {
it('should throw an error', () => {
peerConnection.socket = undefined;
try {
peerConnectionHandleSocket(peerConnection);
fail('PeerConnectionSocketNotDefined should be thrown here');
} catch (e) {
expect(e).toEqual(new PeerConnectionSocketNotDefined());
}
});
});
describe('when peerConnectionHandleSocket is called properly', () => {
describe('when socket.on("disconnect") occured', () => {
it('should call setAndShowErrorDialogMessage to show user that they are disconnected', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('disconnect');
expect(setAndShowErrorDialogMessage).toBeCalled();
});
});
describe('when socket.on("connect") occured', () => {
it('should call socket.emit("GET_MY_IP")', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('connect');
expect(peerConnection.socket.emit).toBeCalledWith(
'GET_MY_IP',
expect.anything()
);
});
});
describe('when socket.on("ENCRYPTED_MESSAGE") occured', () => {
it('should call peerConnection receiveEncryptedMessage', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('ENCRYPTED_MESSAGE');
expect(peerConnection.receiveEncryptedMessage).toBeCalled();
});
});
describe('when socket.on("NOT_ALLOWED") occured', () => {
it('should change UI accordingly', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('NOT_ALLOWED');
expect(setAndShowErrorDialogMessage).toBeCalled();
});
});
describe('when socket.on("ROOM_LOCKED") occured', () => {
it('should change UI accordingly', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('ROOM_LOCKED');
expect(setAndShowErrorDialogMessage).toBeCalled();
});
});
describe('when socket.on("USER_ENTER") occured', () => {
it('should call setMyDeviceDetails on UIHandler', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('USER_ENTER', {
users: [{ username: 'asdf', publicKey: '1234' }],
});
jest.advanceTimersByTime(1000);
expect(peerConnection.UIHandler.setMyDeviceDetails).toBeCalled();
});
it('should call sendEncryptedMessage with ADD_USER type', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('USER_ENTER', {
users: [{ username: 'asdf', publicKey: '1234' }],
});
expect(peerConnection.sendEncryptedMessage).toBeCalledWith({
type: 'ADD_USER',
payload: expect.anything(),
});
});
it('should call sendEncryptedMessage with DEVICE_DETAILS type', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('USER_ENTER', {
users: [{ username: 'asdf', publicKey: '1234' }],
});
expect(peerConnection.sendEncryptedMessage).toBeCalledWith({
type: 'DEVICE_DETAILS',
payload: expect.anything(),
});
});
it('should call sendEncryptedMessage with GET_APP_THEME type', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('USER_ENTER', {
users: [{ username: 'asdf', publicKey: '1234' }],
});
expect(peerConnection.sendEncryptedMessage).toBeCalledWith({
type: 'GET_APP_THEME',
payload: expect.anything(),
});
});
it('should call sendEncryptedMessage with GET_APP_LANGUAGE type', () => {
peerConnectionHandleSocket(peerConnection);
peerConnection.socket.emit('USER_ENTER', {
users: [{ username: 'asdf', publicKey: '1234' }],
});
expect(peerConnection.sendEncryptedMessage).toBeCalledWith({
type: 'GET_APP_LANGUAGE',
payload: expect.anything(),
});
});
});
});
describe('when getMyIPCallback is called properly', () => {
it('should call initApp on peerConnection', () => {
getMyIPCallback(
peerConnection,
TEST_IP,
INPUTtestWindowNavigatorUserAgent
);
expect(peerConnection.initApp).toBeCalled();
});
it('should make peerConnection.myDeviceDetails with proper parameters', () => {
getMyIPCallback(
peerConnection,
TEST_IP,
INPUTtestWindowNavigatorUserAgent
);
expect(peerConnection.myDeviceDetails).toEqual(
OUTPUTDeviceDetailsFromUAParsed
);
});
});
});

View File

@ -0,0 +1,118 @@
import PeerConnection from '.';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import {
getBrowserFromUAParser,
getDeviceTypeFromUAParser,
getOSFromUAParser,
} from '../../utils/userAgentParserHelpers';
import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
export function getMyIPCallback(
peerConnection: PeerConnection,
ip: string,
userAgent: string
) {
peerConnection.myDeviceDetails.myIP = ip;
peerConnection.uaParser.setUA(userAgent);
peerConnection.myDeviceDetails.myOS = getOSFromUAParser(
peerConnection.uaParser
);
peerConnection.myDeviceDetails.myDeviceType = getDeviceTypeFromUAParser(
peerConnection.uaParser
);
peerConnection.myDeviceDetails.myBrowser = getBrowserFromUAParser(
peerConnection.uaParser
);
peerConnection.initApp(peerConnection.user, ip);
}
export default (peerConnection: PeerConnection) => {
if (!peerConnection.socket) {
throw new PeerConnectionSocketNotDefined();
}
peerConnection.socket.on('disconnect', () => {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);
});
peerConnection.socket.on('connect', () => {
peerConnection.socket.emit('GET_MY_IP', (ip: string) => {
getMyIPCallback(peerConnection, ip, window.navigator.userAgent);
});
});
peerConnection.socket.on('NOT_ALLOWED', () => {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.NOT_ALLOWED);
});
peerConnection.socket.on(
'USER_ENTER',
(payload: { users: PartnerPeerUser[] }) => {
const filteredPartner = payload.users.filter((v) => {
return peerConnection.user.publicKey !== v.publicKey;
});
peerConnection.partner = filteredPartner[0];
if (!peerConnection.partner) return;
peerConnection.sendEncryptedMessage({
type: 'ADD_USER',
payload: {
username: peerConnection.user.username,
publicKey: peerConnection.user.publicKey,
isOwner: true,
id: peerConnection.user.username,
},
});
peerConnection.sendEncryptedMessage({
type: 'DEVICE_DETAILS',
payload: {
socketID: peerConnection.socket.io.engine.id,
os: peerConnection.myDeviceDetails.myOS,
deviceType: peerConnection.myDeviceDetails.myDeviceType,
browser: peerConnection.myDeviceDetails.myBrowser,
deviceScreenWidth: window.screen.width,
deviceScreenHeight: window.screen.height,
},
});
peerConnection.sendEncryptedMessage({
type: 'GET_APP_THEME',
payload: {},
});
peerConnection.sendEncryptedMessage({
type: 'GET_APP_LANGUAGE',
payload: {},
});
setTimeout(() => {
peerConnection.UIHandler.setMyDeviceDetails({
myIP: peerConnection.myDeviceDetails.myIP,
myOS: peerConnection.myDeviceDetails.myOS,
myBrowser: peerConnection.myDeviceDetails.myBrowser,
myDeviceType: peerConnection.myDeviceDetails.myDeviceType,
});
}, 100);
}
);
// peerConnection.socket.on('USER_EXIT', (payload: any) => {
// // peerConnection.props.receiveUnencryptedMessage('USER_EXIT', payload);
// });
peerConnection.socket.on(
'ENCRYPTED_MESSAGE',
(payload: ReceiveEncryptedMessagePayload) => {
peerConnection.receiveEncryptedMessage(payload);
}
);
peerConnection.socket.on('ROOM_LOCKED', () => {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);
});
};

View File

@ -0,0 +1,284 @@
jest.mock('../../utils/message');
jest.mock('./setAndShowErrorDialogMessage');
import PeerConnection from '.';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage';
import NullUser from './NullUser';
import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';
import SimplePeer from 'simple-peer';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
jest.useFakeTimers();
const TEST_USER = { username: 'asdf', publicKey: 'ff', privateKey: 'sss' };
const DUMMY_PAYLOAD = {
payload: '',
signature: '',
iv: '',
keys: [],
};
const CALL_USER_PAYLOAD = {
payload: 'CALL_USER',
signature: '',
iv: '',
keys: [],
};
const DENY_TO_CONNECT_PAYLOAD = {
payload: 'DENY_TO_CONNECT',
signature: '',
iv: '',
keys: [],
};
const DISCONNECT_BY_HOST_MACHINE_USER = {
payload: 'DISCONNECT_BY_HOST_MACHINE_USER',
signature: '',
iv: '',
keys: [],
};
const ALLOWED_TO_CONNECT_PAYLOAD = {
payload: 'ALLOWED_TO_CONNECT',
signature: '',
iv: '',
keys: [],
};
const APP_THEME_PAYLOAD = {
payload: 'APP_THEME',
signature: '',
iv: '',
keys: [],
};
const APP_LANGUAGE_PAYLOAD = {
payload: 'APP_LANGUAGE',
signature: '',
iv: '',
keys: [],
};
jest.mock('../../utils/message', () => {
return {
process: (payload: any) => {
return new Promise<ProcessedMessage>((resolve) => {
if (payload.payload === 'CALL_USER') {
resolve({
type: 'CALL_USER',
payload: {
signalData: '1signal',
},
});
}
if (payload.payload === 'DENY_TO_CONNECT') {
resolve({
type: 'DENY_TO_CONNECT',
payload: {},
});
}
if (payload.payload === 'DISCONNECT_BY_HOST_MACHINE_USER') {
resolve({
type: 'DISCONNECT_BY_HOST_MACHINE_USER',
payload: {},
});
}
if (payload.payload === 'ALLOWED_TO_CONNECT') {
resolve({
type: 'ALLOWED_TO_CONNECT',
payload: {},
});
}
if (payload.payload === 'APP_THEME') {
resolve({
type: 'APP_THEME',
payload: {
value: false,
},
});
}
if (payload.payload === 'APP_LANGUAGE') {
resolve({
type: 'APP_LANGUAGE',
payload: {
value: 'latin',
},
});
}
resolve();
});
},
};
});
jest.mock('simple-peer', () => {
return jest.fn().mockImplementation(() => {
const listeners = new Map<string, any[]>();
return {
...jest.requireActual('simple-peer'),
on: jest.fn().mockImplementation((s: string, callback: any) => {
if (!listeners.has(s)) {
listeners.set(s, []);
}
listeners.get(s)?.push(callback);
}),
emit: jest.fn().mockImplementation((s: string, any: any) => {
listeners.forEach((callbacks, key) => {
callbacks.forEach((callback) => {
if (key === s) {
callback(any);
}
});
});
}),
send: jest.fn().mockImplementation((_: string) => {}),
signal: jest.fn().mockImplementation((_: string) => {}),
};
});
});
describe('peerConnectionReceiveEncryptedMessage', () => {
let peerConnection: PeerConnection;
beforeEach(() => {
jest.clearAllMocks();
peerConnection = new PeerConnection(
'123',
jest.fn(),
new Crypto(),
new VideoAutoQualityOptimizer(),
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn()
)
);
peerConnection.user = TEST_USER;
peerConnection.peer = new SimplePeer();
});
afterEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe('when peerConnectionReceiveEncryptedMessage is called', () => {
describe('when peerConnection.user is NullUser', () => {
it('should throw and error', () => {
peerConnection.user = NullUser;
peerConnectionReceiveEncryptedMessage(
peerConnection,
DUMMY_PAYLOAD
).catch((e) => expect(e).toEqual(new PeerConnectionUserIsNotDefinedError()));
});
});
describe('when processedMessageType is "CALL_USER"', () => {
it('should call .signal on simple-peer', async () => {
await peerConnectionReceiveEncryptedMessage(
peerConnection,
CALL_USER_PAYLOAD
);
expect(peerConnection.peer?.signal).toBeCalledWith('1signal');
});
});
describe('when processedMessageType is "DENY_TO_CONNECT"', () => {
it('should call setAndShowErrorDialogMessage with DENY_TO_CONNECT error', async () => {
await peerConnectionReceiveEncryptedMessage(
peerConnection,
DENY_TO_CONNECT_PAYLOAD
);
expect(setAndShowErrorDialogMessage).toBeCalledWith(
peerConnection,
ErrorMessage.DENY_TO_CONNECT
);
});
});
describe('when processedMessageType is "DISCONNECT_BY_HOST_MACHINE_USER"', () => {
it('should call setAndShowErrorDialogMessage with DISCONNECT_BY_HOST_MACHINE_USER error', async () => {
await peerConnectionReceiveEncryptedMessage(
peerConnection,
DISCONNECT_BY_HOST_MACHINE_USER
);
expect(setAndShowErrorDialogMessage).toBeCalledWith(
peerConnection,
ErrorMessage.DISCONNECTED
);
});
});
describe('when processedMessageType is "ALLOWED_TO_CONNECT"', () => {
it('should call setAndShowErrorDialogMessage with ALLOWED_TO_CONNECT error', async () => {
await peerConnectionReceiveEncryptedMessage(
peerConnection,
ALLOWED_TO_CONNECT_PAYLOAD
);
expect(
peerConnection.UIHandler.hostAllowedToConnectCallback
).toBeCalled();
});
});
describe('when processedMessageType is "APP_THEME"', () => {
it('should call setAndShowErrorDialogMessage with APP_THEME error', async () => {
await peerConnectionReceiveEncryptedMessage(
peerConnection,
APP_THEME_PAYLOAD
);
expect(peerConnection.UIHandler.setIsDarkThemeCallback).toBeCalledWith(
false
);
expect(peerConnection.UIHandler.isDarkTheme).toBe(false);
});
});
describe('when processedMessageType is "APP_THEME" and current theme is the same as received', () => {
it('should call setAndShowErrorDialogMessage with APP_LANGUAGE error', async () => {
peerConnection.UIHandler.isDarkTheme = false;
await peerConnectionReceiveEncryptedMessage(
peerConnection,
APP_LANGUAGE_PAYLOAD
);
expect(peerConnection.UIHandler.setIsDarkThemeCallback).not.toBeCalledWith(
false
);
expect(peerConnection.UIHandler.isDarkTheme).toBe(false);
});
});
describe('when processedMessageType is "APP_LANGUAGE"', () => {
it('should call setAndShowErrorDialogMessage with APP_LANGUAGE error', async () => {
await peerConnectionReceiveEncryptedMessage(
peerConnection,
APP_LANGUAGE_PAYLOAD
);
expect(peerConnection.UIHandler.setAppLanguageCallback).toBeCalledWith(
'latin'
);
});
});
});
});

View File

@ -0,0 +1,40 @@
import PeerConnection from '.';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import { process as processMessage } from '../../utils/message';
import NullUser from './NullUser';
import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
export default async (
peerConnection: PeerConnection,
payload: ReceiveEncryptedMessagePayload
) => {
if (peerConnection.user === NullUser) {
throw new PeerConnectionUserIsNotDefinedError();
}
const message = (await processMessage(
payload,
peerConnection.user.privateKey
)) as any;
if (message.type === 'CALL_USER') {
peerConnection.peer?.signal(message.payload.signalData);
}
if (message.type === 'DENY_TO_CONNECT') {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);
}
if (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED);
}
if (message.type === 'ALLOWED_TO_CONNECT') {
peerConnection.UIHandler.hostAllowedToConnectCallback();
}
if (message.type === 'APP_THEME') {
if (peerConnection.UIHandler.isDarkTheme !== message.payload.value) {
peerConnection.UIHandler.setIsDarkThemeCallback(message.payload.value);
peerConnection.UIHandler.isDarkTheme = message.payload.value;
}
}
if (message.type === 'APP_LANGUAGE') {
peerConnection.UIHandler.setAppLanguageCallback(message.payload.value);
}
};

View File

@ -1,10 +0,0 @@
export default (q: number) => {
return `
{
"type": "set_video_quality",
"payload": {
"value": ${q}
}
}
`;
};

View File

@ -1,9 +0,0 @@
export default () => {
return `
{
"type": "get_sharing_source_type",
"payload": {
}
}
`;
};

View File

@ -0,0 +1,112 @@
import SimplePeer from 'simple-peer';
import PeerConnection from '.';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
import Crypto from '../../utils/crypto';
import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer';
import PeerConnectionUIHandler from './PeerConnectionUIHandler';
import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage';
jest.useFakeTimers();
jest.mock('../../utils/crypto.ts');
jest.mock('simple-peer', () => {
return jest.fn().mockImplementation(() => {
const listeners = new Map<string, any[]>();
return {
...jest.requireActual('simple-peer'),
on: jest.fn().mockImplementation((s: string, callback: any) => {
if (!listeners.has(s)) {
listeners.set(s, []);
}
listeners.get(s)?.push(callback);
}),
emit: jest.fn().mockImplementation((s: string, any: any) => {
listeners.forEach((callbacks, key) => {
callbacks.forEach((callback) => {
if (key === s) {
callback(any);
}
});
});
}),
send: jest.fn().mockImplementation((_: string) => {}),
};
});
});
describe('peerConnectionHandlePeer callback', () => {
let peerConnection: PeerConnection;
const setIsErrorDialogOpen = jest.fn();
beforeEach(() => {
peerConnection = new PeerConnection(
'123',
jest.fn(),
new Crypto(),
new VideoAutoQualityOptimizer(),
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
setIsErrorDialogOpen
)
);
peerConnection.peer = new SimplePeer();
peerConnection.UIHandler.errorDialogMessage = ErrorMessage.UNKNOWN_ERROR;
jest.clearAllMocks();
jest.restoreAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('when setAndShowErrorDialogMessage is called properly', () => {
describe('when error message in ui handler is UNKNOWN_ERROR', () => {
it('whould show error dialog with given error message', () => {
expect(peerConnection.UIHandler.errorDialogMessage).toEqual(
ErrorMessage.UNKNOWN_ERROR
);
const errorMessage = ErrorMessage.DENY_TO_CONNECT;
setAndShowErrorDialogMessage(peerConnection, errorMessage);
expect(
peerConnection.UIHandler.setDialogErrorMessageCallback
).toBeCalledWith(errorMessage);
expect(peerConnection.UIHandler.setIsErrorDialogOpen).toBeCalledWith(
true
);
expect(peerConnection.UIHandler.errorDialogMessage).toEqual(
errorMessage
);
});
});
describe('when error message in ui handler is NOT UNKNOWN_ERROR', () => {
it('whould not show anything in UI and do nothing to UIHandler', () => {
const originalErrorMessage = ErrorMessage.DISCONNECTED;
peerConnection.UIHandler.errorDialogMessage = originalErrorMessage;
const errorMessage = ErrorMessage.UNKNOWN_ERROR;
setAndShowErrorDialogMessage(peerConnection, errorMessage);
expect(
peerConnection.UIHandler.setDialogErrorMessageCallback
).not.toBeCalled();
expect(peerConnection.UIHandler.setIsErrorDialogOpen).not.toBeCalled();
expect(peerConnection.UIHandler.errorDialogMessage).toEqual(
originalErrorMessage
);
});
});
});
});

View File

@ -0,0 +1,12 @@
import PeerConnection from '.';
import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum';
export default (peerConnection: PeerConnection, errorMessage: ErrorMessage) => {
if (
peerConnection.UIHandler.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR
) {
peerConnection.UIHandler.setDialogErrorMessageCallback(errorMessage);
peerConnection.UIHandler.setIsErrorDialogOpen(true);
peerConnection.UIHandler.errorDialogMessage = errorMessage;
}
};

View File

@ -0,0 +1,17 @@
import { INPUTtestSdpMediaBitrate } from './mocks/INPUTvideo500000testSdpMediaBitrate';
import { OUTPUTtestSdpMediaBitrate } from './mocks/OUTPUTvideo500000testSdpMediaBitrate';
import setSdpMediaBitrate from './setSdpMediaBitrate';
describe('setSdpMediaBitrate', () => {
describe('when setSdpMediaBitrate is called with sdp input', () => {
it('should produce a result that should match with test sdp string', () => {
const result = setSdpMediaBitrate(
INPUTtestSdpMediaBitrate,
'video',
500000
);
expect(result).toMatch(OUTPUTtestSdpMediaBitrate);
});
});
});

View File

@ -0,0 +1,20 @@
export function prepareDataMessageToChangeQuality(q: number) {
return `
{
"type": "set_video_quality",
"payload": {
"value": ${q}
}
}
`;
}
export function prepareDataMessageToGetSharingSourceType(){
return `
{
"type": "get_sharing_source_type",
"payload": {
}
}
`;
}

View File

@ -0,0 +1,62 @@
import PeerConnection from '..';
import VideoAutoQualityOptimizer from '../../VideoAutoQualityOptimizer';
import Crypto from '../../../utils/crypto';
import startSocketConnectedCheckingLoop from '.';
import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage';
import PeerConnectionUIHandler from '../PeerConnectionUIHandler';
jest.useFakeTimers();
jest.mock('../setAndShowErrorDialogMessage');
describe('startSocketConnectedCheckingLoop', () => {
let peerConnection: PeerConnection;
beforeEach(() => {
jest.clearAllMocks();
peerConnection = new PeerConnection(
'123',
jest.fn(),
new Crypto(),
new VideoAutoQualityOptimizer(),
new PeerConnectionUIHandler(
true,
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn(),
jest.fn()
)
);
peerConnection.socket = { connected: true };
});
afterEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe('when interval passed and socket is connected', () => {
it('should NOT call setAndShowErrorDialogMessage', () => {
startSocketConnectedCheckingLoop(peerConnection);
jest.advanceTimersByTime(3000);
expect(setAndShowErrorDialogMessage).not.toBeCalled();
});
});
describe('when interval passed and socket is NOT connected', () => {
it('should call setAndShowErrorDialogMessage', () => {
peerConnection.socket.connected = false;
startSocketConnectedCheckingLoop(peerConnection);
jest.advanceTimersByTime(3000);
expect(setAndShowErrorDialogMessage).toBeCalled();
});
});
});

View File

@ -0,0 +1,11 @@
import PeerConnection from '..';
import { ErrorMessage } from '../../../components/ErrorDialog/ErrorMessageEnum';
import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage';
export default (peerConnection: PeerConnection) => {
setInterval(() => {
if (!peerConnection.socket.connected) {
setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT);
}
}, 2000);
};

View File

@ -0,0 +1,7 @@
export default class CanvasNotDefinedError extends Error {
constructor() {
super('internal variable of canvas DOM elemenent should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, CanvasNotDefinedError.prototype);
}
}

View File

@ -0,0 +1,7 @@
export default class ImageDataIsUndefinedError extends Error {
constructor() {
super('imageData retreived is undefined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, ImageDataIsUndefinedError.prototype);
}
}

View File

@ -0,0 +1,9 @@
export default class VideoDimensionsAreWrongError extends Error {
constructor() {
super(
'video dimensions are wrong, neither width nor height can be zero!'
);
// Set the prototype explicitly.
Object.setPrototypeOf(this, VideoDimensionsAreWrongError.prototype);
}
}

View File

@ -0,0 +1,7 @@
export default class VideoNotDefinedError extends Error {
constructor() {
super('internal variable of video DOM elemenent should be defined!');
// Set the prototype explicitly.
Object.setPrototypeOf(this, VideoNotDefinedError.prototype);
}
}

View File

@ -0,0 +1,381 @@
import VideoAutoQualityOptimizer from '.';
import {
COMPARISON_CANVAS_ID,
REACT_PLAYER_WRAPPER_ID,
} from '../../constants/appConstants';
import CanvasNotDefinedError from './errors/CanvasNotDefinedError';
import ImageDataIsUndefinedError from './errors/ImageDataIsUndefinedError';
import VideoDimensionsAreWrongError from './errors/VideoDimensionsAreWrongError';
import VideoNotDefinedError from './errors/VideoNotDefinedError';
jest.useFakeTimers();
const TEST_IMAGE_DATA = {
data: new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
height: 1,
width: 10,
} as ImageData;
const PREV_FRAME_IMAGE_DATA = {
data: new Uint8ClampedArray([2, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
height: 1,
width: 10,
} as ImageData;
const LOW_MISMATCH = 0.09;
const HIGH_MISMATCH = 0.2;
describe('VideoAutoQualityOptimizer', () => {
let optimizer: VideoAutoQualityOptimizer;
beforeEach(() => {
optimizer = new VideoAutoQualityOptimizer();
optimizer.video = {
...optimizer.video,
videoWidth: 1142,
videoHeight: 1142,
} as HTMLVideoElement;
// @ts-ignore
optimizer.canvas = {
width: 123,
height: 123,
getContext: () => {
return {
clearRect: () => {},
drawImage: () => {},
};
},
} as HTMLCanvasElement;
});
describe('when VideoAutoQualityOptimizer is created properly', () => {
describe('whev optimization loop is running', () => {
it('should call goodQualityCallback when there is small frames mismatch and halfQualityCallback when there is large frames mismatch', () => {
optimizer.prepareCanvasAndVideo = () => {};
optimizer.validateBeforeCalculations = () => {};
optimizer.clearCanvas = () => {};
optimizer.scaleCanvas = () => {};
optimizer.drawVideoFrameToCanvas = () => {};
optimizer.getImageDataFromCanvas = () => ({} as ImageData);
optimizer.prevFrame = PREV_FRAME_IMAGE_DATA;
optimizer.halfQualityCallbak = jest.fn();
optimizer.goodQualityCallback = jest.fn();
// 1. STEP 1 simulate high percent of frames mismatch
optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.5
optimizer.largeMismatchFramesCount = 5;
optimizer.startOptimizationLoop();
jest.advanceTimersByTime(5000);
expect(optimizer.halfQualityCallbak).toBeCalled();
// 2. STEP 2 simulate low percent of frames mismatch
optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.03
optimizer.largeMismatchFramesCount = 0;
optimizer.startOptimizationLoop();
jest.advanceTimersByTime(5000);
expect(optimizer.goodQualityCallback).toBeCalled();
// 3. repeat step 1
optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.5
optimizer.largeMismatchFramesCount = 5;
optimizer.startOptimizationLoop();
jest.advanceTimersByTime(5000);
expect(optimizer.halfQualityCallbak).toBeCalled();
// 4. repeat step 2
optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.03
optimizer.largeMismatchFramesCount = 0;
optimizer.startOptimizationLoop();
jest.advanceTimersByTime(5000);
expect(optimizer.goodQualityCallback).toBeCalled();
});
});
describe('when validateVideoIsDefined is called', () => {
describe('when video local variable is undefined', () => {
it('should throw an error', () => {
optimizer.video = undefined;
try {
optimizer.validateVideoIsDefined();
} catch (e) {
expect(e).toEqual(new VideoNotDefinedError());
}
});
});
});
describe('when validateVideoIsDefined is called', () => {
describe('when canvas local variable is undefined', () => {
it('should throw an error', () => {
optimizer.canvas = undefined;
try {
optimizer.validateCanvasIsDefined();
} catch (e) {
expect(e).toEqual(new CanvasNotDefinedError());
}
});
});
});
describe('when startOptimizationLoop of VideoAutoQualityOptimizer is called', () => {
it('should define canvas and video internal variables', () => {
const originalDocumentQuerySelector = document.querySelector;
const valueForVideo = 'a';
const valueForCanvas = 'b';
try {
document.querySelector = (s: string) => {
if (s === `#${REACT_PLAYER_WRAPPER_ID} > video`) {
return valueForVideo;
}
if (s === `#${COMPARISON_CANVAS_ID}`) {
return valueForCanvas;
}
return '';
};
optimizer.startOptimizationLoop();
jest.advanceTimersByTime(2000);
} catch (e) {
expect(optimizer.video).toBe(valueForVideo);
expect(optimizer.canvas).toBe(valueForCanvas);
}
document.querySelector = originalDocumentQuerySelector;
});
});
describe('when doFrameComparisonAndQualityOptimization of VideoAutoQualityOptimizer is called', () => {
describe('when width of video is 0', () => {
it('should throw an error', () => {
optimizer.video = {
...optimizer.video,
videoWidth: 0,
videoHeight: 1142,
} as HTMLVideoElement;
try {
optimizer.doFrameComparisonAndQualityOptimization();
fail('should have thrown error here!');
} catch (e) {
expect(e).toEqual(new VideoDimensionsAreWrongError());
}
});
});
describe('when height of video is 0', () => {
it('should throw an error', () => {
optimizer.video = {
...optimizer.video,
videoWidth: 1142,
videoHeight: 0,
} as HTMLVideoElement;
try {
optimizer.doFrameComparisonAndQualityOptimization();
fail('should have thrown error here!');
} catch (e) {
expect(e).toEqual(new VideoDimensionsAreWrongError());
}
});
it('should call scaleCanvas()', () => {
const spy = jest.spyOn(optimizer, 'scaleCanvas');
try {
optimizer.doFrameComparisonAndQualityOptimization();
} catch (e) {}
expect(spy).toBeCalled();
});
});
it('should call drawVideoFrameToCanvas()', () => {
const spy = jest.spyOn(optimizer, 'drawVideoFrameToCanvas');
try {
optimizer.doFrameComparisonAndQualityOptimization();
} catch (e) {}
expect(spy).toBeCalled();
});
describe('when getImageDataFromCanvas returns undefined', () => {
it('should throw an error', () => {
jest
.spyOn(optimizer, 'getImageDataFromCanvas')
.mockImplementation(() => undefined);
try {
optimizer.doFrameComparisonAndQualityOptimization();
fail('it should have thrown an error here');
} catch (e) {
expect(e).toEqual(new ImageDataIsUndefinedError());
}
});
});
describe('when prevFrame is undefined', () => {
it('should set prevFrame to be the same as test image data', () => {
jest
.spyOn(optimizer, 'getImageDataFromCanvas')
.mockImplementation(() => TEST_IMAGE_DATA);
try {
optimizer.doFrameComparisonAndQualityOptimization();
} catch (e) {}
expect(optimizer.prevFrame).toEqual(TEST_IMAGE_DATA);
});
});
describe('when getPreviousAndCurrentFrameMismatchInPercent returns a number', () => {
it('should call handleFramesMismatch()', () => {
const TEST_MISMATCH = 0.1;
optimizer.prevFrame = TEST_IMAGE_DATA;
jest
.spyOn(optimizer, 'getImageDataFromCanvas')
.mockImplementation(() => TEST_IMAGE_DATA);
jest
.spyOn(optimizer, 'getPreviousAndCurrentFrameMismatchInPercent')
.mockImplementation(() => 0.1);
const spyOfHandleFramesMismatch = jest.spyOn(
optimizer,
'handleFramesMismatch'
);
optimizer.doFrameComparisonAndQualityOptimization();
expect(spyOfHandleFramesMismatch).toBeCalledWith(TEST_MISMATCH);
});
});
describe('when doFrameComparisonAndQualityOptimization run was successful', () => {
it('should set prevFrame to test image data', () => {
optimizer.prevFrame = PREV_FRAME_IMAGE_DATA;
jest
.spyOn(optimizer, 'getImageDataFromCanvas')
.mockImplementation(() => TEST_IMAGE_DATA);
jest
.spyOn(optimizer, 'getPreviousAndCurrentFrameMismatchInPercent')
.mockImplementation(() => 0.1);
optimizer.doFrameComparisonAndQualityOptimization();
expect(optimizer.prevFrame).toEqual(TEST_IMAGE_DATA);
});
});
describe('when getPreviousAndCurrentFrameMismatchInPercent run was successful', () => {});
});
describe('when getPreviousAndCurrentFrameMismatchInPercent was called', () => {
describe('when canvas is undefined', () => {
it('should return 0', () => {
optimizer.canvas = undefined;
const res = optimizer.getPreviousAndCurrentFrameMismatchInPercent(
TEST_IMAGE_DATA
);
expect(res).toBe(0);
});
});
describe('when getPreviousAndCurrentFrameMismatchInPercent ran properly', () => {
it('should return proper mismatch in percent', () => {
const MISMATCH_TO_RETURN = 44;
jest
.spyOn(optimizer, 'getNumberOfMismatchedPixels')
.mockImplementation(() => MISMATCH_TO_RETURN);
// @ts-ignore
optimizer.canvas = {
width: 123,
height: 123,
};
const width = optimizer.canvas?.width as number;
const height = optimizer.canvas?.height as number;
const expected = MISMATCH_TO_RETURN / (width * height);
const res = optimizer.getPreviousAndCurrentFrameMismatchInPercent(
TEST_IMAGE_DATA
);
expect(res).toBe(expected);
});
});
});
describe('when handleFramesMismatch was called', () => {
describe('when it received low percent mismatch and largeMismatchFramesCount is more than zero', () => {
it('should decrease largeMismatchFramesCount by one', () => {
const MISMATCH_FRAMES_COUNT = 2;
optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT;
optimizer.handleFramesMismatch(LOW_MISMATCH);
const expected = MISMATCH_FRAMES_COUNT - 1;
expect(optimizer.largeMismatchFramesCount).toBe(expected);
});
});
describe('when it received LOW percent mismatch and isRequestedHalfQuality is true', () => {
it('should call goodQualityCallback and set isRequestedHalfQuality to false', () => {
const MISMATCH_FRAMES_COUNT = 0;
optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT;
optimizer.isRequestedHalfQuality = true;
optimizer.goodQualityCallback = jest.fn();
optimizer.handleFramesMismatch(LOW_MISMATCH);
expect(optimizer.goodQualityCallback).toBeCalled();
expect(optimizer.isRequestedHalfQuality).toBe(false);
});
});
describe('when it received LOW percent mismatch and isRequestedHalfQuality is false', () => {
it('should call NOT goodQualityCallback', () => {
const MISMATCH_FRAMES_COUNT = 0;
optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT;
optimizer.isRequestedHalfQuality = false;
optimizer.goodQualityCallback = jest.fn();
optimizer.handleFramesMismatch(LOW_MISMATCH);
expect(optimizer.goodQualityCallback).not.toBeCalled();
});
});
describe('when it received HIGH percent mismatch', () => {
describe('when isRequestedHalfQuality is false and there were MORE than consecutive 3 frames mismatch', () => {
it('should call halfQualityCallbak and set isRequestedHalfQuality to true', () => {
const MISMATCH_FRAMES_COUNT = 3;
optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT;
optimizer.isRequestedHalfQuality = false;
optimizer.halfQualityCallbak = jest.fn();
optimizer.handleFramesMismatch(HIGH_MISMATCH);
expect(optimizer.halfQualityCallbak).toBeCalled();
expect(optimizer.isRequestedHalfQuality).toBe(true);
});
});
describe('when isRequestedHalfQuality is false and there were LESS than consecutive 3 frames mismatch', () => {
it('should increase largeMismatchFramesCount by one', () => {
const MISMATCH_FRAMES_COUNT = 1;
optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT;
optimizer.isRequestedHalfQuality = false;
optimizer.handleFramesMismatch(HIGH_MISMATCH);
const expected = MISMATCH_FRAMES_COUNT + 1;
expect(optimizer.largeMismatchFramesCount).toBe(expected);
});
});
});
});
});
});

View File

@ -1,12 +1,20 @@
import { COMPARISON_CANVAS_ID } from './../../constants/appConstants';
import pixelmatch from 'pixelmatch';
import { REACT_PLAYER_WRAPPER_ID } from '../../constants/appConstants';
import CanvasNotDefinedError from './errors/CanvasNotDefinedError';
import VideoDimensionsAreWrongError from './errors/VideoDimensionsAreWrongError';
import VideoNotDefinedError from './errors/VideoNotDefinedError';
import ImageDataIsUndefinedError from './errors/ImageDataIsUndefinedError';
export const CANVAS_SCALE_MULTIPLIER = 0.125; // 1/8 of original canvas size, to speed up calculations
export const MISMATCH_PERCENT_THRESHOLD = 0.1;
export default class VideoAutoQualityOptimizer {
video: any;
video: undefined | HTMLVideoElement;
canvas: any;
canvas: undefined | HTMLCanvasElement;
prevFrame: any;
prevFrame: undefined | ImageData;
largeMismatchFramesCount = 0;
@ -25,66 +33,158 @@ export default class VideoAutoQualityOptimizer {
}
startOptimizationLoop() {
this.prepareCanvasAndVideo();
setInterval(() => {
this.frameComparisonQualityOptimization();
}, 1000);
}
frameComparisonQualityOptimization() {
if (this.video && this.canvas) {
if (this.video.videoWidth === 0 || this.video.videoHeight === 0) return;
// scale the canvas accordingly
this.canvas
.getContext('2d')
.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.canvas.width = this.video.videoWidth / 8;
this.canvas.height = this.video.videoHeight / 8;
// draw the video at that frame
this.canvas
.getContext('2d')
.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
// convert it to a usable data URL
let imageData = this.canvas
.getContext('2d')
.getImageData(0, 0, this.canvas.width, this.canvas.height);
if (this.prevFrame) {
try {
const numMismatchedPixels = pixelmatch(
this.prevFrame.data,
imageData.data,
null,
this.canvas.width,
this.canvas.height,
{ threshold: 0.1 }
);
const mismatchInPercent =
numMismatchedPixels / (this.canvas.width * this.canvas.height);
if (mismatchInPercent < 0.1 && this.largeMismatchFramesCount > 0) {
this.largeMismatchFramesCount -= 1;
} else if (mismatchInPercent < 0.1 && this.isRequestedHalfQuality) {
this.largeMismatchFramesCount = 0;
this.isRequestedHalfQuality = false;
this.goodQualityCallback();
} else if (mismatchInPercent >= 0.1 && !this.isRequestedHalfQuality) {
if (this.largeMismatchFramesCount < 3) {
this.largeMismatchFramesCount += 1;
} else {
this.halfQualityCallbak();
this.isRequestedHalfQuality = true;
}
}
} catch (e) {
try {
this.doFrameComparisonAndQualityOptimization();
} catch (e) {
// some errors may be thrown here, better ignore them in production
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
}
}, 1000);
}
doFrameComparisonAndQualityOptimization() {
this.validateBeforeCalculations();
this.clearCanvas();
this.scaleCanvas();
this.drawVideoFrameToCanvas();
let imageData = this.getImageDataFromCanvas();
if (!imageData) {
throw new ImageDataIsUndefinedError();
}
if (!this.prevFrame) {
this.prevFrame = imageData;
imageData = null;
} else {
this.video = document.querySelector(
`#${REACT_PLAYER_WRAPPER_ID} > video`
);
this.canvas = document.getElementById('comparison-canvas');
return;
}
try {
const mismatchInPercent = this.getPreviousAndCurrentFrameMismatchInPercent(imageData);
this.handleFramesMismatch(mismatchInPercent);
} catch (e) {
// usually frames size mismatch thrown here, so can be ignored as it happens
// often when changing sharing window size
// so logging this error may be not necessary
}
this.prevFrame = imageData;
}
findAndSetVideoInternalVariable(document: Document) {
this.video = document.querySelector(
`#${REACT_PLAYER_WRAPPER_ID} > video`
) as HTMLVideoElement;
}
findAndSetCanvasInternalVariable(document: Document) {
this.canvas = document.querySelector(
`#${COMPARISON_CANVAS_ID}`
) as HTMLCanvasElement;
}
prepareCanvasAndVideo() {
setTimeout(() => {
this.findAndSetVideoInternalVariable(document);
this.findAndSetCanvasInternalVariable(document);
}, 1000);
}
clearCanvas() {
this.canvas
?.getContext('2d')
?.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
validateVideoWidthAndHeight() {
if (this.video?.videoWidth === 0 || this.video?.videoHeight === 0) {
throw new VideoDimensionsAreWrongError();
}
}
validateBeforeCalculations() {
this.validateVideoWidthAndHeight();
this.validateVideoIsDefined();
this.validateCanvasIsDefined();
}
validateVideoIsDefined() {
if (!this.video) {
throw new VideoNotDefinedError();
}
}
validateCanvasIsDefined() {
if (!this.canvas) {
throw new CanvasNotDefinedError();
}
}
scaleCanvas() {
if (
!this.canvas ||
!this.video
)
return;
this.canvas.width = this.video.videoWidth * CANVAS_SCALE_MULTIPLIER;
this.canvas.height = this.video.videoHeight * CANVAS_SCALE_MULTIPLIER;
}
drawVideoFrameToCanvas() {
if (!this.video) return;
this.canvas
?.getContext('2d')
?.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
}
getImageDataFromCanvas() {
return this.canvas
?.getContext('2d')
?.getImageData(0, 0, this.canvas.width, this.canvas.height);
}
getNumberOfMismatchedPixels(imageData: ImageData) {
if (!this.canvas || !this.canvas.width || !this.prevFrame) return 0;
return pixelmatch(
this.prevFrame.data,
imageData.data,
null,
this.canvas.width,
this.canvas.height,
{ threshold: 0.1 }
);
}
getPreviousAndCurrentFrameMismatchInPercent(imageData: ImageData) {
if (!this.canvas) return 0;
return this.getNumberOfMismatchedPixels(imageData) / (this.canvas.width * this.canvas.height);
}
handleFramesMismatch(mismatchInPercent: number) {
if (mismatchInPercent < 0.1 && this.largeMismatchFramesCount > 0) {
this.largeMismatchFramesCount -= 1;
} else if (mismatchInPercent < 0.1 && this.isRequestedHalfQuality) {
this.largeMismatchFramesCount = 0;
this.isRequestedHalfQuality = false;
this.goodQualityCallback();
} else if (mismatchInPercent >= 0.1 && !this.isRequestedHalfQuality) {
if (this.largeMismatchFramesCount < 3) {
this.largeMismatchFramesCount += 1;
} else {
this.halfQualityCallbak();
this.isRequestedHalfQuality = true;
}
}
}
isLowMismatchPercent(mismatchInPercent: number) {
return mismatchInPercent < MISMATCH_PERCENT_THRESHOLD;
}
isHighMismatchPercent(mismatchInPercent: number) {
return mismatchInPercent >= MISMATCH_PERCENT_THRESHOLD;
}
}

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should match exact snapshot 1`] = `null`;

View File

@ -0,0 +1,14 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { AppContextProvider } from '.';
jest.useFakeTimers();
it('should match exact snapshot', () => {
const subject = renderer.create(
<>
<AppContextProvider />
</>
);
expect(subject).toMatchSnapshot();
});

View File

@ -1 +1,27 @@
/// <reference types="react-scripts" />
type ConnectionIconType = 'feed' | 'feed-subscribed';
type LoadingSharingIconType = 'desktop' | 'application';
type ScreenSharingSourceType = 'screen' | 'window';
type CreatePeerConnectionUseEffectParams = {
isDarkTheme: boolean;
peer: undefined | PeerConnection;
setIsDarkThemeHook: (_: boolean) => void;
setMyDeviceDetails: (_: DeviceDetails) => void;
setConnectionIconType: (_: ConnectionIconType) => void;
setIsShownTextPrompt: (_: boolean) => void;
setPromptStep: (_: number) => void;
setScreenSharingSourceType: (_: ScreenSharingSourceType) => void;
setDialogErrorMessage: (_: ErrorMessage) => void;
setIsErrorDialogOpen: (_: boolean) => void;
setUrl: (_: MediaStream) => void;
setPeer: (_: PeerConnection) => void;
};
type handleDisplayingLoadingSharingIconLoopParams = {
promptStep: number;
url: undefined | MediaStream;
setIsShownLoadingSharingIcon: (_: boolean) => void;
loadingSharingIconType: LoadingSharingIconType;
isShownLoadingSharingIcon: boolean;
setLoadingSharingIconType: (_: LoadingSharingIconType) => void;
};

View File

@ -1,5 +1,5 @@
type CallAcceptedMessageWithPayload = {
type: 'CALL_ACCEPTED';
type CallUserMessageWithPayload = {
type: 'CALL_USER';
payload: {
signalData: string;
};
@ -17,6 +17,41 @@ type DeviceDetailsMessageWithPayload = {
};
};
type DenyToConnectMessageWithPayload = {
type: 'DENY_TO_CONNECT';
payload: {};
};
type DisconnectByHostMachineUserMessageWithPayload = {
type: 'DISCONNECT_BY_HOST_MACHINE_USER';
payload: {};
};
type AllowedToConnectMessageWithPayload = {
type: 'ALLOWED_TO_CONNECT';
payload: {};
};
type AppThemeMessageWithPayload = {
type: 'APP_THEME';
payload: {
value: boolean,
};
};
type AppLanguageMessageWithPayload = {
type: 'APP_LANGUAGE';
payload: {
value: string,
};
};
type ProcessedMessage =
| CallAcceptedMessageWithPayload
| DeviceDetailsMessageWithPayload;
| CallUserMessageWithPayload
| DeviceDetailsMessageWithPayload
| DenyToConnectMessageWithPayload
| DisconnectByHostMachineUserMessageWithPayload
| AllowedToConnectMessageWithPayload
| AppThemeMessageWithPayload
| AppLanguageMessageWithPayload;

View File

@ -0,0 +1,3 @@
export default () => {
return process.env.JEST_WORKER_ID !== undefined;
};

View File

@ -0,0 +1,3 @@
export default () => {
return encodeURI(window.location.pathname.replace('/', ''));
}

View File

@ -0,0 +1,53 @@
import { prepare, process } from './message';
import getTestPublickKeyPem from './mocks/getTestPublicKeyPem';
import getTestPrivateKeyPem from './mocks/getTestPrivateKeyPem';
interface TestPayload {
text: string;
}
describe('message.ts tests for proper encryption and decryption functionality', () => {
const TEST_TEXT = 'some test text here';
const TEST_TEXT_AS_URL = 'some%20test%20text%20here';
const testPayloadToEncrypt = {
payload: {
text: TEST_TEXT,
},
};
const testUser = {
username: 'testUsername',
id: 'testId',
};
const testPartner = {
publicKey: getTestPublickKeyPem(),
};
it('should create encrypted payload with prepare() method', async () => {
const encryptedPayload = await prepare(
testPayloadToEncrypt,
testUser,
testPartner
);
expect(encryptedPayload.toSend.payload).not.toContain(TEST_TEXT);
expect(encryptedPayload.toSend.payload).not.toContain(TEST_TEXT_AS_URL);
});
it('should decrypt encrypted payload with process() method', async () => {
const encryptedPayload = await prepare(
testPayloadToEncrypt,
testUser,
testPartner
);
const decryptedPayload = await process(
encryptedPayload.toSend,
getTestPrivateKeyPem()
);
expect(
((decryptedPayload.payload as unknown) as TestPayload).text
).toContain(TEST_TEXT_AS_URL);
});
});

View File

@ -42,7 +42,7 @@ export const process = (payload: any, privateKeyString: string) =>
key.sessionKey
);
signingHMACKey = crypto.unwrapKey(privateKey, key.signingKey);
resolvePayload();
resolvePayload(undefined);
} catch (e) {
console.error(e);
}

View File

@ -0,0 +1,29 @@
export default function getTestPrivateKeyPem() {
return `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAoQ8+vdVAerEyaxGaffzrTTv+sgpQ3xToBFYkxrT6f+zP4MqV
nTIy2+UMlEGGryMFgJWyurqv+NyoVf7vIFOxQcxJko1BL/oIt/e5YZ/fMIw9AhgP
0A0oZyFKNbGesCY3zMdpZqPE0brzfhjr/lu5VzioZI9vxocOSSc3+S8w1EXqujgO
X3PWXgZrD6Y//Oo+8BgNQta/e5PUyc9yNchU/W3ddzBdE0iUXTdQt7yFvzy4vTbS
ywUxoNNJnD6dUJ6dZ4c24VGvrvhJ5mzNqQjibAhtkPFbvhn0e0BHl0BZ876fLHGg
zpHgmmiWbKwBYl1ydv8fW9W3r5nQoW0RThYJjwIDAQABAoIBAHqOrUGrGsvCNwl+
db9VTICTHLbCXtPChuN14bpLUSszOuRlhAAAiO8HltDiI+j1j2RPhZfOI8YNsxLt
UW2aAhJ9r6aTUn19mFDVcv20uBOrQ2lqge3hdVM049GD/asxCdkMDUqLaGPoDQ1x
TXNavOiANrN+6qF5eAd2joNRw6hi7osyWqfpgM9y58kiYYazKHKlI/er19JsD2t5
hgRiFti+oN9nqhhVzJI/GYU9JugXnB/Z0uFvqOyt/3YDHPpOC07WYpfA3yzshBkW
TiiMp1eJNX/sRh4JHzRI9/MQe9ajlAS7T74HwCkclzaS8ZY6t+dfVZ097/ug77eQ
NwY4DAECgYEA0hWv7hpookKKcl0srfxbx+FbtBhzXqz+Tfy3H5do6uXRc0aUwQ25
PZLs7UAT3zjt+4aYu1xgkp3cUx5FNr/FAPYLtjXw1/6fmt3HX8820SpCMjIzS1bU
UwJvmAut70YA+n2eKwO1qJNeLcpNCDebbIXwj0lKDh5HQDyoswuz7I8CgYEAxEKV
N4Ma6GqZ0hgdQlO71oSuftHUI80Iz+Riorrp4AA8hp5A3V6Fd9QDy8PYLz1kD9pM
uWdCLqaxdOVDtLVnjNVZH1SI+wSBDA/0OtdvPaONL1JKRA21kol049f0M8wkulAw
6lfi2aQwZ3KWyEAfDy5iPdVROnQWqUcLmxO0kwECgYAGpSr4dBtlLoekkG/uXPIm
Q2mcK73Se9RbcSf1tttZusVCSTRBWwbF/NTDuGgogmt8rkg8fPKNELM8adO0pKI9
oorCS7h/jI1N38ADttE8EoMfhVj8BBYZPhV7kLsCu4siYUDUiXyAhZDQD/sZzHB9
IUt3rNDL24dTb9fCOheJ3wKBgGRgpY7Z2DZM51VkDfrxdp3WCKVGTljtMfeaGLSg
IqP1mv9DC2vtPxg1cKeUCArJPFc7UIh2/ot7qEFgTQusyERojgePJew0tofj1QcP
To7ZConMbb12wYosEYPC3NxtKc+82ffRcW3dIwCVw/axjPEnyQlVBBGAdGKpuo7b
Oj0BAoGAMs2zuAJfJB4xE1UZdLXk9uHWPAzgUNDzHuS5mMWuloGvKX+19yck6o2J
ZA/iq6GwOgvD2y9MC0mV4WkmwZpVXwPVpZD8GVQXZf2xZgh/q2e3IObAd80NLnaG
v86qN98DRTh9L+47Nsaf1J5vDDaAfH2Ir8UgAQ5ZMEFDm7P0hUQ=
-----END RSA PRIVATE KEY-----`;
}

View File

@ -1,3 +1,4 @@
// @ts-ignore
import { UAParser } from 'ua-parser-js';
export function getOSFromUAParser(uaParser: UAParser) {

View File

@ -17,7 +17,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"strict": true
"strict": true,
},
"include": [
"src"

View File

@ -1648,13 +1648,6 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/cheerio@^0.22.22":
version "0.22.23"
resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.23.tgz#74bcfee9c5ee53f619711dca953a89fe5cfa4eb4"
integrity sha512-QfHLujVMlGqcS/ePSf3Oe5hK3H8wi/yN2JYuxSB1U10VvW1fO3K8C+mURQesFYS1Hn7lspOsTT75SKq/XtydQg==
dependencies:
"@types/node" "*"
"@types/dom4@^2.0.1":
version "2.0.1"
resolved "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz#506d5781b9bcab81bd9a878b198aec7dee2a6033"
@ -2129,21 +2122,6 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
airbnb-prop-types@^2.16.0:
version "2.16.0"
resolved "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
dependencies:
array.prototype.find "^2.1.1"
function.prototype.name "^1.1.2"
is-regex "^1.1.0"
object-is "^1.1.2"
object.assign "^4.1.0"
object.entries "^1.1.2"
prop-types "^15.7.2"
prop-types-exact "^1.2.0"
react-is "^16.13.1"
ajv-errors@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
@ -2299,11 +2277,6 @@ array-equal@^1.0.0:
resolved "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@ -2342,15 +2315,7 @@ array-unique@^0.3.2:
resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
array.prototype.find@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c"
integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.4"
array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3:
array.prototype.flat@^1.2.1:
version "1.2.4"
resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
@ -3136,30 +3101,6 @@ chardet@^0.7.0:
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
cheerio-select-tmp@^0.1.0:
version "0.1.1"
resolved "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646"
integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==
dependencies:
css-select "^3.1.2"
css-what "^4.0.0"
domelementtype "^2.1.0"
domhandler "^4.0.0"
domutils "^2.4.4"
cheerio@^1.0.0-rc.3:
version "1.0.0-rc.5"
resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f"
integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==
dependencies:
cheerio-select-tmp "^0.1.0"
dom-serializer "~1.2.0"
domhandler "^4.0.0"
entities "~2.1.0"
htmlparser2 "^6.0.0"
parse5 "^6.0.0"
parse5-htmlparser2-tree-adapter "^6.0.0"
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -3366,7 +3307,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
commander@^2.11.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -3722,17 +3663,6 @@ css-select@^2.0.0:
domutils "^1.7.0"
nth-check "^1.0.2"
css-select@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8"
integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==
dependencies:
boolbase "^1.0.0"
css-what "^4.0.0"
domhandler "^4.0.0"
domutils "^2.4.3"
nth-check "^2.0.0"
css-tree@1.0.0-alpha.37:
version "1.0.0-alpha.37"
resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
@ -3759,11 +3689,6 @@ css-what@^3.2.1:
resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
css-what@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
@ -4098,11 +4023,6 @@ dir-glob@2.0.0:
arrify "^1.0.1"
path-type "^3.0.0"
discontinuous-range@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
dns-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
@ -4177,15 +4097,6 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-serializer@^1.0.1, dom-serializer@~1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.0.0"
entities "^2.0.0"
dom4@^2.1.5:
version "2.1.6"
resolved "https://registry.npmjs.org/dom4/-/dom4-2.1.6.tgz#c90df07134aa0dbd81ed4d6ba1237b36fc164770"
@ -4201,7 +4112,7 @@ domelementtype@1, domelementtype@^1.3.1:
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domelementtype@^2.0.1, domelementtype@^2.1.0:
domelementtype@^2.0.1:
version "2.1.0"
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
@ -4220,13 +4131,6 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
domhandler@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e"
integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==
dependencies:
domelementtype "^2.1.0"
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@ -4243,15 +4147,6 @@ domutils@^1.5.1, domutils@^1.7.0:
dom-serializer "0"
domelementtype "1"
domutils@^2.4.3, domutils@^2.4.4:
version "2.4.4"
resolved "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.0.1"
domhandler "^4.0.0"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
@ -4397,84 +4292,11 @@ entities@^1.1.1:
resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
entities@^2.0.0, entities@~2.1.0:
entities@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
enzyme-adapter-react-16@^1.15.5:
version "1.15.5"
resolved "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz#7a6f0093d3edd2f7025b36e7fbf290695473ee04"
integrity sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==
dependencies:
enzyme-adapter-utils "^1.13.1"
enzyme-shallow-equal "^1.0.4"
has "^1.0.3"
object.assign "^4.1.0"
object.values "^1.1.1"
prop-types "^15.7.2"
react-is "^16.13.1"
react-test-renderer "^16.0.0-0"
semver "^5.7.0"
enzyme-adapter-utils@^1.13.1:
version "1.14.0"
resolved "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0"
integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==
dependencies:
airbnb-prop-types "^2.16.0"
function.prototype.name "^1.1.3"
has "^1.0.3"
object.assign "^4.1.2"
object.fromentries "^2.0.3"
prop-types "^15.7.2"
semver "^5.7.1"
enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e"
integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==
dependencies:
has "^1.0.3"
object-is "^1.1.2"
enzyme-to-json@^3.6.1:
version "3.6.1"
resolved "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz#d60740950bc7ca6384dfe6fe405494ec5df996bc"
integrity sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==
dependencies:
"@types/cheerio" "^0.22.22"
lodash "^4.17.15"
react-is "^16.12.0"
enzyme@^3.11.0:
version "3.11.0"
resolved "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28"
integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==
dependencies:
array.prototype.flat "^1.2.3"
cheerio "^1.0.0-rc.3"
enzyme-shallow-equal "^1.0.1"
function.prototype.name "^1.1.2"
has "^1.0.3"
html-element-map "^1.2.0"
is-boolean-object "^1.0.1"
is-callable "^1.1.5"
is-number-object "^1.0.4"
is-regex "^1.0.5"
is-string "^1.0.5"
is-subset "^0.1.1"
lodash.escape "^4.0.1"
lodash.isequal "^4.5.0"
object-inspect "^1.7.0"
object-is "^1.0.2"
object.assign "^4.1.0"
object.entries "^1.1.1"
object.values "^1.1.1"
raf "^3.4.1"
rst-selector-parser "^2.2.3"
string.prototype.trim "^1.2.1"
errno@^0.1.3, errno@~0.1.7:
version "0.1.8"
resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
@ -4489,7 +4311,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4:
es-abstract@^1.17.0-next.1, es-abstract@^1.17.2:
version "1.17.7"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
@ -5341,26 +5163,11 @@ function-bind@^1.1.1:
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function.prototype.name@^1.1.2, function.prototype.name@^1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe"
integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
functions-have-names "^1.2.1"
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
functions-have-names@^1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
gensync@^1.0.0-beta.1:
version "1.0.0-beta.2"
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@ -5672,13 +5479,6 @@ html-comment-regex@^1.1.0:
resolved "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
html-element-map@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22"
integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==
dependencies:
array-filter "^1.0.0"
html-encoding-sniffer@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
@ -5740,16 +5540,6 @@ htmlparser2@^3.3.0:
inherits "^2.0.1"
readable-stream "^3.1.1"
htmlparser2@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01"
integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.0.0"
domutils "^2.4.4"
entities "^2.0.0"
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@ -6108,19 +5898,12 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.0.1:
version "1.1.0"
resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
dependencies:
call-bind "^1.0.0"
is-buffer@^1.0.2, is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2:
is-callable@^1.1.4, is-callable@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
@ -6249,11 +6032,6 @@ is-negative-zero@^2.0.0:
resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
is-number-object@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -6307,7 +6085,7 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1:
is-regex@^1.0.4, is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
@ -6339,11 +6117,6 @@ is-string@^1.0.5:
resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-subset@^0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
is-svg@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
@ -7207,21 +6980,6 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.escape@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -7576,11 +7334,6 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.5"
moo@^0.5.0:
version "0.5.1"
resolved "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -7663,16 +7416,6 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
nearley@^2.7.10:
version "2.20.1"
resolved "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==
dependencies:
commander "^2.19.0"
moo "^0.5.0"
railroad-diagrams "^1.0.0"
randexp "0.4.6"
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@ -7837,13 +7580,6 @@ nth-check@^1.0.2, nth-check@~1.0.1:
dependencies:
boolbase "~1.0.0"
nth-check@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
dependencies:
boolbase "^1.0.0"
num2fraction@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@ -7878,12 +7614,12 @@ object-hash@^2.0.1:
resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
object-inspect@^1.7.0, object-inspect@^1.8.0:
object-inspect@^1.8.0:
version "1.9.0"
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2:
object-is@^1.0.1:
version "1.1.4"
resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==
@ -7908,7 +7644,7 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
object.assign@^4.1.0, object.assign@^4.1.1:
version "4.1.2"
resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@ -7918,7 +7654,7 @@ object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
has-symbols "^1.0.1"
object-keys "^1.1.1"
object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2:
object.entries@^1.1.0, object.entries@^1.1.1:
version "1.1.3"
resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
@ -7928,7 +7664,7 @@ object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2:
es-abstract "^1.18.0-next.1"
has "^1.0.3"
object.fromentries@^2.0.2, object.fromentries@^2.0.3:
object.fromentries@^2.0.2:
version "2.0.3"
resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072"
integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==
@ -8193,13 +7929,6 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
parse5-htmlparser2-tree-adapter@^6.0.0:
version "6.0.1"
resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
dependencies:
parse5 "^6.0.1"
parse5@4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
@ -8210,11 +7939,6 @@ parse5@5.1.0:
resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
parse5@^6.0.0, parse5@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parseqs@0.0.6:
version "0.0.6"
resolved "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
@ -9202,15 +8926,6 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types-exact@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
dependencies:
has "^1.0.3"
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@ -9335,19 +9050,6 @@ raf@^3.4.1:
dependencies:
performance-now "^2.1.0"
railroad-diagrams@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
dependencies:
discontinuous-range "1.0.0"
ret "~0.1.10"
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -9456,12 +9158,12 @@ react-i18next@^11.7.4:
"@babel/runtime" "^7.3.1"
html-parse-stringify2 "2.0.1"
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.1:
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1:
version "17.0.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
@ -9562,6 +9264,14 @@ react-scripts@3.4.3:
optionalDependencies:
fsevents "2.1.2"
react-shallow-renderer@^16.13.1:
version "16.14.1"
resolved "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124"
integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
dependencies:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0"
react-spinners@^0.9.0:
version "0.9.0"
resolved "https://registry.npmjs.org/react-spinners/-/react-spinners-0.9.0.tgz#b22c38acbfce580cd6f1b04a4649e812370b1fb8"
@ -9569,15 +9279,15 @@ react-spinners@^0.9.0:
dependencies:
"@emotion/core" "^10.0.15"
react-test-renderer@^16.0.0-0:
version "16.14.0"
resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"
integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==
react-test-renderer@^17.0.1:
version "17.0.1"
resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3187e636c3063e6ae498aedf21ecf972721574c7"
integrity sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
react-is "^16.8.6"
scheduler "^0.19.1"
react-is "^17.0.1"
react-shallow-renderer "^16.13.1"
scheduler "^0.20.1"
react-transition-group@^2.9.0:
version "2.9.0"
@ -9692,11 +9402,6 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@ -9995,14 +9700,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
dependencies:
lodash.flattendeep "^4.4.0"
nearley "^2.7.10"
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@ -10100,6 +9797,14 @@ scheduler@^0.19.1:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.20.1:
version "0.20.1"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@ -10135,7 +9840,7 @@ selfsigned@^1.10.7:
dependencies:
node-forge "^0.10.0"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -10679,15 +10384,6 @@ string.prototype.matchall@^4.0.2:
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.3"
string.prototype.trim@^1.2.1:
version "1.2.3"
resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz#d23a22fde01c1e6571a7fadcb9be11decd8061a7"
integrity sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
string.prototype.trimend@^1.0.1:
version "1.0.3"
resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Intent, Alert, H4 } from '@blueprintjs/core';
import isProduction from '../utils/isProduction';
import DeviceInfoCallout from './DeviceInfoCallout';
import isWithReactRevealAnimations from '../utils/isWithReactRevealAnimations';
interface AllowConnectionForDeviceAlertProps {
device: Device | null;
@ -25,7 +25,10 @@ export default function AllowConnectionForDeviceAlert(
isOpen={isOpen}
onCancel={onCancel}
onConfirm={onConfirm}
transitionDuration={isProduction() ? 700 : 0}
transitionDuration={isWithReactRevealAnimations() ? 700 : 0}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
usePortal={false}
>
<H4>Device is trying to connect, do you allow?</H4>
<DeviceInfoCallout

View File

@ -15,11 +15,12 @@ import {
import { Row, Col } from 'react-flexbox-grid';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import CloseOverlayButton from './CloseOverlayButton';
import isProduction from '../utils/isProduction';
import ConnectedDevicesService from '../features/ConnectedDevicesService';
import SharingSessionsService from '../features/SharingSessionsService';
import DeviceInfoCallout from './DeviceInfoCallout';
import SharingSourcePreviewCard from './SharingSourcePreviewCard';
import isWithReactRevealAnimations from '../utils/isWithReactRevealAnimations';
import isProduction from '../utils/isProduction';
const sharingSessionsService = remote.getGlobal(
'sharingSessionService'
@ -149,7 +150,7 @@ export default function ConnectedDevicesListDrawer(
size={Drawer.SIZE_LARGE}
isOpen={props.isOpen}
onClose={props.handleToggle}
transitionDuration={isProduction() ? 700 : 0}
transitionDuration={isWithReactRevealAnimations() ? 700 : 0}
// transitionDuration={0}
>
<Row between="xs" middle="xs" className={classes.drawerInnerTopPanel}>
@ -176,7 +177,11 @@ export default function ConnectedDevicesListDrawer(
</Row>
<Row className={classes.connectedDevicesRoot}>
<Col xs={12}>
<Fade bottom cascade duration={isProduction() ? 700 : 0}>
<Fade
bottom
cascade
duration={isWithReactRevealAnimations() ? 700 : 0}
>
<div className={classes.zoomFullWidth}>
{connectedDevicesService.getDevices().map((device) => {
return (
@ -245,7 +250,7 @@ export default function ConnectedDevicesListDrawer(
setIsAlertDisconectAllOpen(false);
}}
onConfirm={handleDisconnectAndHideAllDevices}
transitionDuration={isProduction() ? 700 : 0}
transitionDuration={isWithReactRevealAnimations() ? 700 : 0}
>
<H4>
Are you sure you want to disconnect all connected viewing devices?

View File

@ -29,8 +29,8 @@ import i18n_client, {
getLangFullNameToLangISOKeyMap,
getLangISOKeyToLangFullNameMap,
} from '../../configs/i18next.config.client';
import isProduction from '../../utils/isProduction';
import SettingRowLabelAndInput from './SettingRowLabelAndInput';
import isWithReactRevealAnimations from '../../utils/isWithReactRevealAnimations';
const Fade = require('react-reveal/Fade');
@ -259,7 +259,7 @@ export default function SettingsOverlay(props: SettingsOverlayProps) {
transitionDuration={0}
>
<div className={getClassesCallback().overlayInnerRoot}>
<Fade duration={isProduction() ? 700 : 0}>
<Fade duration={isWithReactRevealAnimations() ? 700 : 0}>
<div
id="settings-overlay-inner"
className={`${getClassesCallback().overlayInsideFade} ${

View File

@ -1035,6 +1035,7 @@ exports[`should match exact snapshot 1`] = `
onKeyDown={[Function]}
>
<CSSTransition
addEndListener={[Function]}
classNames="bp3-overlay"
in={true}
key=".$__backdrop"
@ -1042,6 +1043,7 @@ exports[`should match exact snapshot 1`] = `
timeout={0}
>
<Transition
addEndListener={[Function]}
appear={false}
enter={true}
exit={true}
@ -1064,6 +1066,7 @@ exports[`should match exact snapshot 1`] = `
</Transition>
</CSSTransition>
<CSSTransition
addEndListener={[Function]}
classNames="bp3-overlay"
in={true}
key=".$.0"
@ -1071,6 +1074,7 @@ exports[`should match exact snapshot 1`] = `
timeout={0}
>
<Transition
addEndListener={[Function]}
appear={false}
enter={true}
exit={true}
@ -1144,6 +1148,7 @@ exports[`should match exact snapshot 1`] = `
<button
className="bp3-button makeStyles-closeButton-13 makeStyles-absoluteCloseButton-23"
id="close-overlay-button"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
@ -1275,6 +1280,8 @@ exports[`should match exact snapshot 1`] = `
</Blueprint3.Icon>
<Blueprint3.Text
className="bp3-text-large"
ellipsize={false}
tagName="div"
>
<div
className="bp3-text-large"
@ -1342,6 +1349,8 @@ exports[`should match exact snapshot 1`] = `
</Blueprint3.Icon>
<Blueprint3.Text
className="bp3-text-large"
ellipsize={false}
tagName="div"
>
<div
className="bp3-text-large"
@ -1407,6 +1416,8 @@ exports[`should match exact snapshot 1`] = `
</Blueprint3.Icon>
<Blueprint3.Text
className="bp3-text-large"
ellipsize={false}
tagName="div"
>
<div
className="bp3-text-large"
@ -1531,7 +1542,10 @@ exports[`should match exact snapshot 1`] = `
<div
className=""
>
<Blueprint3.Text>
<Blueprint3.Text
ellipsize={false}
tagName="div"
>
<div
className=""
>
@ -1569,6 +1583,7 @@ exports[`should match exact snapshot 1`] = `
>
<button
className="bp3-button bp3-active"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
@ -1618,6 +1633,7 @@ exports[`should match exact snapshot 1`] = `
>
<button
className="bp3-button"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
@ -1740,7 +1756,10 @@ exports[`should match exact snapshot 1`] = `
<div
className=""
>
<Blueprint3.Text>
<Blueprint3.Text
ellipsize={false}
tagName="div"
>
<div
className=""
>
@ -1899,7 +1918,10 @@ exports[`should match exact snapshot 1`] = `
<div
className=""
>
<Blueprint3.Text>
<Blueprint3.Text
ellipsize={false}
tagName="div"
>
<div
className=""
>

View File

@ -3,7 +3,7 @@ import { Button, Icon, ControlGroup, Text } from '@blueprintjs/core';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import ChooseAppOrScreenOverlay from './StepsOfStepper/ChooseAppOrScreenOverlay/ChooseAppOrScreenOverlay';
interface ShareEntireScreenOrAppWindowProps {
interface ShareAppOrScreenControlGroupProps {
handleNextEntireScreen: () => void;
handleNextApplicationWindow: () => void;
}
@ -50,8 +50,8 @@ const useStyles = makeStyles(() =>
})
);
export default function ShareEntireScreenOrAppWindowControlGroup(
props: ShareEntireScreenOrAppWindowProps
export default function ShareAppOrScreenControlGroup(
props: ShareAppOrScreenControlGroupProps
) {
const { handleNextEntireScreen, handleNextApplicationWindow } = props;
const classes = useStyles();

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Row, Col } from 'react-flexbox-grid';
import { Icon, Text, Button, Popover, Tooltip } from '@blueprintjs/core';
import isProduction from '../../utils/isProduction';
import DeviceInfoCallout from '../DeviceInfoCallout';
import isWithReactRevealAnimations from '../../utils/isWithReactRevealAnimations';
interface DeviceConnectedInfoButtonProps {
device: Device;
@ -55,7 +55,7 @@ export default function DeviceConnectedInfoButton(
content={getDeviceConnectedPopoverContent(device, onDisconnect)}
position="bottom"
inheritDarkTheme={false}
transitionDuration={isProduction() ? 700 : 0}
transitionDuration={isWithReactRevealAnimations() ? 700 : 0}
>
<Tooltip
content={<Text>Click to manage</Text>}

View File

@ -110,13 +110,17 @@ exports[`should match exact snapshot 1`] = `
<Blueprint3.Tooltip
className=""
content={
<Blueprint3.Text>
<Blueprint3.Text
ellipsize={false}
tagName="div"
>
Click to manage
</Blueprint3.Text>
}
hoverCloseDelay={0}
hoverOpenDelay={400}
key=".0"
minimal={false}
position="right"
transitionDuration={100}
>
@ -127,7 +131,10 @@ exports[`should match exact snapshot 1`] = `
captureDismiss={false}
className=""
content={
<Blueprint3.Text>
<Blueprint3.Text
ellipsize={false}
tagName="div"
>
Click to manage
</Blueprint3.Text>
}
@ -142,7 +149,13 @@ exports[`should match exact snapshot 1`] = `
interactionKind="hover-target"
lazy={true}
minimal={false}
modifiers={Object {}}
modifiers={
Object {
"arrow": Object {
"enabled": true,
},
}
}
openOnTargetFocus={true}
popoverClassName="bp3-tooltip"
position="right"
@ -191,6 +204,7 @@ exports[`should match exact snapshot 1`] = `
<button
className="bp3-button bp3-intent-success"
id="connected-device-info-stepper-button"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
style={
@ -254,7 +268,10 @@ exports[`should match exact snapshot 1`] = `
<div
className="col-xs"
>
<Blueprint3.Text>
<Blueprint3.Text
ellipsize={false}
tagName="div"
>
<div
className=""
>

View File

@ -4,9 +4,9 @@ import { H3, Card, Dialog } from '@blueprintjs/core';
import { Row, Col } from 'react-flexbox-grid';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import CloseOverlayButton from '../../CloseOverlayButton';
import isProduction from '../../../utils/isProduction';
import PreviewGridList from './PreviewGridList';
import DesktopCapturerSources from '../../../features/DesktopCapturerSourcesService';
import isWithReactRevealAnimations from '../../../utils/isWithReactRevealAnimations';
const desktopCapturerSourcesService = remote.getGlobal(
'desktopCapturerSourcesService'
@ -104,15 +104,15 @@ export default function ChooseAppOrScreenOverlay(
hasBackdrop
isOpen={isChooseAppOrScreenOverlayOpen}
usePortal
transitionDuration={isProduction() ? 750 : 0}
transitionDuration={isWithReactRevealAnimations() ? 750 : 0}
>
<div
id="choose-app-or-screen-overlay-container"
style={{ minHeight: '95%' }}
>
<Fade
duration={isProduction() ? 1000 : 0}
delay={isProduction() ? 1000 : 0}
duration={isWithReactRevealAnimations() ? 1000 : 0}
delay={isWithReactRevealAnimations() ? 1000 : 0}
>
<div
style={{
@ -172,7 +172,7 @@ export default function ChooseAppOrScreenOverlay(
</Fade>
<Zoom
duration={isProduction() ? 750 : 0}
duration={isWithReactRevealAnimations() ? 750 : 0}
style={{
position: 'relative',
zIndex: '1',

View File

@ -298,6 +298,7 @@ exports[`should match exact snapshot 1`] = `
onKeyDown={[Function]}
>
<CSSTransition
addEndListener={[Function]}
appear={true}
classNames="bp3-overlay"
in={true}
@ -306,6 +307,7 @@ exports[`should match exact snapshot 1`] = `
timeout={0}
>
<Transition
addEndListener={[Function]}
appear={true}
enter={true}
exit={true}
@ -328,6 +330,7 @@ exports[`should match exact snapshot 1`] = `
</Transition>
</CSSTransition>
<CSSTransition
addEndListener={[Function]}
appear={true}
classNames="bp3-overlay"
in={true}
@ -336,6 +339,7 @@ exports[`should match exact snapshot 1`] = `
timeout={0}
>
<Transition
addEndListener={[Function]}
appear={true}
enter={true}
exit={true}
@ -512,6 +516,7 @@ exports[`should match exact snapshot 1`] = `
<button
className="bp3-button"
id="close-overlay-button"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}

View File

@ -90,27 +90,13 @@ export default function IntermediateStep(props: IntermediateStepProps) {
)}
{
// TODO: uncomment this for TRUE production use ! no Connect Test Device buttons in production!
// // eslint-disable-next-line no-nested-ternary
// process.env.NODE_ENV === 'production' &&
// process.env.RUN_MODE !== 'dev' &&
// process.env.RUN_MODE !== 'test' ? (
// <></>
// ) : activeStep === 0 ? (
// // eslint-disable-next-line react/jsx-indent
// <Button
// onClick={() => {
// connectDevice(
// DEVICES[Math.floor(Math.random() * DEVICES.length)]
// );
// }}
// >
// Connect Test Device
// </Button>
// ) : (
// <></>
// )
activeStep === 0 ? (
// eslint-disable-next-line no-nested-ternary
process.env.NODE_ENV === 'production' &&
process.env.RUN_MODE !== 'dev' &&
process.env.RUN_MODE !== 'test' ? (
<></>
) : activeStep === 0 ? (
// eslint-disable-next-line react/jsx-indent
<Button
onClick={() => {
connectDevice(
@ -125,6 +111,21 @@ export default function IntermediateStep(props: IntermediateStepProps) {
) : (
<></>
)
// activeStep === 0 ? (
// <Button
// onClick={() => {
// connectDevice(
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore
// DEVICES[Math.floor(Math.random() * DEVICES.length)]
// );
// }}
// >
// Connect Test Device
// </Button>
// ) : (
// <></>
// )
}
{
/**/

View File

@ -155,6 +155,7 @@ const ScanQRStep: React.FC = () => {
canOutsideClickClose
transitionDuration={isProduction() ? 700 : 0}
style={{ position: 'relative', top: '0px' }}
usePortal={false}
>
<Row
id="qr-code-dialog-inner"

Some files were not shown because too many files have changed in this diff Show More