fix tests

This commit is contained in:
Raghunandan Balachandran
2020-02-05 11:35:19 +01:00
parent 6104d88d5a
commit c323e853a8
35 changed files with 275 additions and 1039 deletions
+2 -1
View File
@@ -16,7 +16,8 @@
"react-dom": "^16.12.0",
"react-helmet":"5.2.1",
"tinycolor2": "1.4.1",
"react-addons-text-content": "0.0.4"
"react-addons-text-content": "0.0.4",
"recompose": "0.30.0"
},
"scripts": {
"lint": "web-scripts lint",
@@ -1,19 +0,0 @@
.burst {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: -1;
}
/* base style of burst shape SVGs */
.burst-shape {
position: absolute;
right: 0;
height: 100%;
background-repeat: no-repeat;
background-size: cover;
opacity:0.1;
}
@@ -1 +0,0 @@
<svg width="484" height="379" xmlns="http://www.w3.org/2000/svg"><path d="M30.247 297.817C14.615 298.6 1.242 286.77.377 271.393c-.857-15.376 11.116-28.478 26.749-29.264 15.631-.78 29.001 11.046 29.866 26.423.86 15.38-11.113 28.479-26.745 29.265zm453.662-114.73c2.595 47.204-33.961 87.45-81.645 89.899-38.163 1.956-71.863-20.898-85.145-54.293-15.635 8.935-33.571 14.474-52.86 15.463-3.194.164-6.36.166-9.503.082 6.91 12.422 11.177 26.532 12.005 41.629 2.947 53.63-38.582 99.355-92.758 102.134-54.179 2.78-100.485-38.442-103.432-92.069-2.899-52.794 37.31-97.9 90.25-101.942-12.222-17.322-19.891-38.098-21.133-60.721C136.138 58.682 186.157 3.606 251.404.26c63.068-3.234 117.267 43.036 124.031 104.452a87.321 87.321 0 0 1 17.436-2.664c47.687-2.447 88.446 33.835 91.038 81.038z" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 803 B

@@ -1 +0,0 @@
<svg width="373" height="418" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M50.77 275.72c9.87-12.14 8.15-29.8-3.86-39.46-12-9.64-29.74-7.62-39.62 4.52-9.87 12.14-8.15 29.8 3.85 39.46 12 9.64 29.75 7.62 39.63-4.52"/><path d="M341.06 19.81c-36.8-29.66-91.04-23.64-121.13 13.43a87.32 87.32 0 0 0-9.7 14.74C160.08 11.91 89.43 21.23 49.64 70.26 8.44 121 15.9 195.02 66.27 235.6a116.01 116.01 0 0 0 59.27 24.93c-32.42 42.05-25.8 102.1 15.37 135.28 41.82 33.7 103.44 26.86 137.63-15.26 34.18-42.12 28-103.58-13.84-137.27a96.26 96.26 0 0 0-38.97-18.94 118.45 118.45 0 0 0 30.18-56.75 86.8 86.8 0 0 0 97.32-26.94c30.09-37.07 24.64-91.17-12.18-120.83"/></g></svg>

Before

Width:  |  Height:  |  Size: 676 B

@@ -1 +0,0 @@
<svg width="491" height="458" xmlns="http://www.w3.org/2000/svg"><path d="M0 246.48l266.02 100.18-148.35-40.13-10.57 39.65L490.6 457.9V138.2L367 0h-.01L298.5 61.24l48.13 53.82-118.57-72.34-93.33 153.03 173.63 105.9L20.1 193.1z" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 257 B

@@ -1 +0,0 @@
<svg width="702" height="620" xmlns="http://www.w3.org/2000/svg"><path d="M482.48.34L250.4 291.06l-120.27-83.27L.45 391.43l326.48 228.6L702.05 312.1l.01-.01L586.8 171.68l-51.08 41.94-61.25-74.63-130.12 106.82L517.8 28.54z" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 252 B

@@ -1 +0,0 @@
<svg width="702" height="620" xmlns="http://www.w3.org/2000/svg"><path d="M482.48.34L250.4 291.06l-120.27-83.27L.45 391.43l326.48 228.6L702.05 312.1l.01-.01L586.8 171.68l-51.08 41.94-61.25-74.63-130.12 106.82L517.8 28.54z" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 251 B

@@ -1 +0,0 @@
<svg width="1050" height="510" xmlns="http://www.w3.org/2000/svg"><path d="M646.72 234.41l67.93-93.6-169.42 83.96L740.73 9.12.36 439.99l851 70L997.7 244.7l-178.68 157.9L1049.97-.02z" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 212 B

@@ -1 +0,0 @@
<svg width="655" height="442" xmlns="http://www.w3.org/2000/svg"><path d="M348.86 202.96L290.11 122l146.53 72.62L267.55 8.1l386.7 228.44-482.4 204.77-158.4-259.26 83.43 29.82L.08.2z" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 212 B

@@ -1 +0,0 @@
<svg width="502" height="330" xmlns="http://www.w3.org/2000/svg"><path d="M501.57.14L341.2 329.8H.74L104.6 133.17l4.82 118.26 57.13-54-8 53.86z" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 174 B

@@ -0,0 +1,44 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Lifecycle extends Component {
static propTypes = {
isShorthand: PropTypes.bool,
fontSize: PropTypes.string,
};
}
const styles = {
alpha: {
color: '#d00150',
fontFamily: 'serif',
fontWeight: 'normal',
fontStyle: 'italic',
},
beta: {
color: '#4d65cc',
fontFamily: 'serif',
fontWeight: 'normal',
fontStyle: 'italic',
},
};
export class AlphaLabel extends Lifecycle {
render() {
const style = fontSize => ({ ...styles.alpha, fontSize, ...this.props.style });
return this.props.isShorthand ? (
<span style={style('120%')}>&alpha;</span>
) : (
<span style={style('100%')}>Alpha</span>
);
}
}
export class BetaLabel extends Lifecycle {
render() {
const fontSize = this.props.fontSize ? this.props.fontSize : this.props.isShorthand ? '120%' : '100%';
const style = { ...styles.beta, fontSize, ...this.props.style };
return this.props.isShorthand ? <span style={style}>&beta;</span> : <span style={style}>Beta</span>;
}
}
@@ -0,0 +1 @@
export { AlphaLabel, BetaLabel } from './Lifecycle';
@@ -0,0 +1,79 @@
import React from 'react';
import { pure } from 'recompose';
import { withStyles } from '@material-ui/core';
const styles = theme => ({
status: {
width: 12,
height: 12,
display: 'inline-block',
marginRight: 1,
},
ok: {
backgroundColor: theme.palette.status.ok,
borderRadius: '50%',
},
warning: {
backgroundColor: theme.palette.status.warning,
},
error: {
width: '0',
height: '0',
borderLeft: '7px solid transparent',
borderRight: '7px solid transparent',
borderBottom: `14px solid ${theme.palette.status.error}`,
},
pending: {
backgroundColor: theme.palette.status.pending,
},
failed: {
backgroundColor: 'rgba(245, 155, 35, 0.5)',
},
running: {
animation: 'blink 0.8s step-start 0s infinite',
backgroundColor: theme.palette.status.running,
},
'@keyframes blink': {
'50%': {
backgroundColor: theme.palette.status.background,
},
},
});
export const StatusOK = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.ok}`} aria-label="Status OK" {...props} />
)),
);
export const StatusWarning = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.warning}`} aria-label="Status warning" {...props} />
)),
);
export const StatusError = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.error}`} aria-label="Status error" {...props} />
)),
);
export const StatusNA = pure(() => <span aria-label="Status N/A">N/A</span>);
export const StatusPending = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.pending}`} aria-label="Status pending" {...props} />
)),
);
export const StatusRunning = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.running}`} aria-label="Status running" {...props} />
)),
);
export const StatusFailed = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.failed}`} aria-label="Status failed" {...props} />
)),
);
@@ -0,0 +1 @@
export { StatusError, StatusFailed, StatusNA, StatusOK, StatusPending, StatusRunning, StatusWarning } from './Status';
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './types';
export { default as createPlugin } from './createPlugin';
export { default as Page } from '../src/layout/Page';
export { shapes, gradients, theme } from '../src/layout/Page';
export { gradients, theme } from '../src/layout/Page';
export { default as Header } from '../src/layout/Header/Header';
export { default as HeaderLabel } from '../src/layout/HeaderLabel';
export { default as InfoCard } from '../src/layout/InfoCard';
@@ -1,16 +1,34 @@
import React, { Component } from 'react';
import '../../assets/bursts/bursts.css';
import React from 'react';
import { makeStyles } from '@material-ui/core';
class Burst extends Component {
render() {
const { theme } = this.props;
return (
<div className="burst" style={{ backgroundImage: theme.gradient }}>
<div className="burst-shape" style={theme.burstShape} />
</div>
);
const useStyles = makeStyles(theme => ({
burst: {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
'z-index': -1,
},
/* base style of burst shape SVGs */
burstShape: {
position: 'absolute',
right: 0,
height: '100%',
'background-repeat': 'no-repeat',
'background-size': 'cover',
opacity:0.1,
}
}));
const Burst = ({theme}) => {
const classes = useStyles();
return (
<div className={classes.burst} style={{ backgroundImage: theme.gradient }}>
<div className={classes.burstShape} style={theme.burstShape} />
</div>
);
}
export default Burst;
@@ -1,6 +1,6 @@
import React, { Fragment } from 'react';
import { IconButton, List, ListItem, ListItemIcon, ListItemText, Popover } from '@material-ui/core';
import { KebabMenuIcon } from 'shared/icons';
import { default as KebabMenuIcon } from './MenuVertical';
const ActionItem = ({ label, secondaryLabel, icon, disabled = false, onClick, WrapperComponent = React.Fragment }) => {
return (
@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
const SvgMenuVertical = (props: SvgIconProps) =>
React.createElement(
SvgIcon,
props,
<path d="M11 3a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V4a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2z" />,
);
export default SvgMenuVertical;
@@ -41,7 +41,7 @@ class HeaderLabel extends Component {
return (
<span className={classes.root}>
<Typography className={classes.label}>{label}</Typography>
{url ? <Link to={url}>{content}</Link> : content}
{url ? <Link href={url}>{content}</Link> : content}
</span>
);
}
@@ -1,10 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tooltip, withStyles } from '@material-ui/core';
import { Tooltip, Link, withStyles } from '@material-ui/core';
import { StatusError } from 'shared/components/Status';
import Link from 'shared/components/Link';
import HeaderLabel from 'shared/components/layout/HeaderLabel';
import { StatusError } from '../../components/Status';
import HeaderLabel from './HeaderLabel';
const style = theme => ({
notVerified: {
@@ -33,7 +32,7 @@ class OwnerHeaderLabel extends Component {
const isBadSquad = owner.type !== 'squad';
const notVerified = isBadSquad && (
<Link to="https://spotify.stackenterprise.co/a/4412/23">
<Link href="https://spotify.stackenterprise.co/a/4412/23">
<span className={classes.notVerified}>
<StatusError style={{ position: 'relative', top: 2 }} /> Squad not verified!
</span>
@@ -1,5 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react';
import { render, prettyDOM } from '@testing-library/react';
import OwnerHeaderLabel from './OwnerHeaderLabel';
import { wrapInThemedTestApp } from '../../testUtils';
@@ -17,7 +17,7 @@ export default class BottomLink extends Component {
return (
<div>
<Divider />
<Link to={link} onClick={onClick} highlight="none">
<Link href={link} onClick={onClick} highlight="none">
<ListItem>
<ListItemIcon>
<ArrowIcon />
@@ -1,14 +0,0 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
const SvgGcpProjectByTools = (props: SvgIconProps) =>
React.createElement(
SvgIcon,
props,
<path
fillRule="evenodd"
d="M10.053 3l2.35 4.05-2.35 4.05H5.35L3 7.05 5.351 3zM10.053 12.9l2.35 4.05-2.35 4.05H5.35L3 16.95l2.351-4.05zM18.649 7.95L21 12l-2.351 4.05h-4.702L11.597 12l2.35-4.05z"
/>,
);
export default SvgGcpProjectByTools;
@@ -1,11 +0,0 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
const SvgInfoSquared = (props: SvgIconProps) =>
React.createElement(
SvgIcon,
props,
<path d="M21 19V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2zM11 9V7h2v2h-2zm0 8v-6h2v6h-2z" />,
);
export default SvgInfoSquared;
@@ -1,168 +0,0 @@
import React, { Component, Fragment } from 'react';
import { matchPath } from 'react-router';
import { ListItem, ListItemIcon, ListItemText, Typography, withStyles, Tooltip, IconButton } from '@material-ui/core';
import { AlphaLabel, BetaLabel } from 'shared/components/Lifecycle';
import Link from 'shared/components/Link';
import { Theme } from 'shared/components/layout/Page';
import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
const styles = theme => ({
root: {
display: 'block',
overflow: 'hidden',
'&:first-child': {
borderTopRightRadius: 6,
borderTopLeftRadius: 6,
},
'&:last-child': {
borderBottomRightRadius: 6,
borderBottomLeftRadius: 6,
},
},
child: {
paddingLeft: theme.spacing(7),
'&:last-child': {
borderBottom: '1px solid #727272',
},
},
boldLabel: {
color: '#FFFFFF',
fontWeight: 'bolder',
},
childLabel: {
color: theme.palette.textVerySubtle,
fontSize: 14,
},
label: {
whiteSpace: 'nowrap',
lineHeight: 1.0,
},
iconImg: {
width: 24,
height: 24,
},
icon: {
margin: theme.spacing(0.5, 2, 0.5, 0),
minWidth: 0,
},
expand: {
color: 'white',
},
});
class NavItem extends Component {
static defaultProps = {
title: 'Mystery Link',
href: '/',
exact: true, // Only used for react-router matchPath to see if this sidebar item is active
icon: null,
isAlpha: false,
isBeta: false,
type: 'other', // Provided by Navigation
child: false, // Whether this is a child of another NavItem (for indenting)
};
// If NavItem has React children, the sub-navigation can be expanded or collapsed
state = {
expanded: false,
};
static isActive(href, exact) {
return matchPath(window.location.pathname, { path: href, exact: exact });
}
maybeActiveNavStyle(theme) {
return NavItem.isActive(this.props.href, this.props.exact)
? {
background: `linear-gradient(to right, ${theme.activeNavLinkColor} 0px, ${theme.activeNavLinkColor} 6px, #4F4F4F 6px, #4F4F4F 100%)`,
}
: {};
}
renderIcon = (condensed, iconElement, title, classes) => {
if (!iconElement) return null;
const listItemIcon = <ListItemIcon className={classes.icon}>{iconElement}</ListItemIcon>;
return condensed ? (
<Tooltip title={title} placement="right">
{listItemIcon}
</Tooltip>
) : (
listItemIcon
);
};
render() {
const { title, href, icon, isAlpha, isBeta, classes, condensed, child, children } = this.props;
const { expanded } = this.state;
const isSection = !!children;
// Automatically expand nested nav if one of the child elements is active
const isChildActive = React.Children.toArray(children).some(c => NavItem.isActive(c.props.href, c.props.exact));
const isExpanded = expanded || isChildActive;
const selfAwareChildren = children && React.cloneElement(children, { child: true });
let iconElement;
if (icon) {
iconElement = typeof icon === 'object' ? icon : <img src={icon} className={classes.iconImg} alt={title} />;
}
return (
<Theme.Consumer>
{theme => (
<>
<Link className={classes.root} style={this.maybeActiveNavStyle(theme)} to={href} gaprops={{ label: title }}>
<ListItem button className={`${classes.listItemGutters} ${child && classes.child}`}>
{this.renderIcon(condensed, iconElement, title, classes)}
{!condensed && (
<>
<ListItemText
primary={
<Typography
variant="subtitle1"
className={`${child ? classes.childLabel : classes.boldLabel} ${classes.label}`}
>
{title}
{isAlpha && (
<Fragment>
&nbsp;
<AlphaLabel isShorthand />
</Fragment>
)}
{isBeta && (
<Fragment>
&nbsp;
<BetaLabel isShorthand />
</Fragment>
)}
</Typography>
}
disableTypography
/>
{isSection && (
<IconButton
onClick={() => this.setState(prev => ({ expanded: !prev.expanded }))}
style={{ padding: 4 }}
data-testid="expand-toggle"
>
{isExpanded ? (
<ArrowDropUp className={classes.expand} />
) : (
<ArrowDropDown className={classes.expand} />
)}
</IconButton>
)}
</>
)}
</ListItem>
</Link>
{isExpanded && selfAwareChildren}
</>
)}
</Theme.Consumer>
);
}
}
export default withStyles(styles)(NavItem);
@@ -1,94 +0,0 @@
import React from 'react';
import { matchPath } from 'react-router';
import { fireEvent, render } from '@testing-library/react';
import NavItem from './NavItem';
import { wrapInThemedTestApp } from '../../testUtils';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
matchPath: jest.fn(),
}));
const minProps = {
title: 'A link title',
href: '/mocked',
};
const minChildProps = {
title: 'A child title',
href: '/child',
};
describe('<NavLink />', () => {
it('renders without exploding', () => {
const rendered = render(wrapInThemedTestApp(<NavItem {...minProps} />));
expect(rendered.getByText('A link title')).toBeInTheDocument();
});
it('should have an anchor with correct href', () => {
const rendered = render(wrapInThemedTestApp(<NavItem {...minProps} />));
const anchor = rendered.container.querySelector('a');
expect(anchor.href).toBe('http://localhost/mocked');
});
it('renders with icon src', () => {
const rendered = render(wrapInThemedTestApp(<NavItem icon="mocked-icon.png" {...minProps} />));
const icon = rendered.getByAltText('A link title');
expect(icon.src).toBe('http://localhost/mocked-icon.png');
});
it('renders with icon component', () => {
const Falafel = () => <div>falafel</div>;
const rendered = render(wrapInThemedTestApp(<NavItem icon={<Falafel />} {...minProps} />));
const icon = rendered.getByText('falafel');
expect(icon).toBeInTheDocument();
});
it('shows alpha icon', () => {
const rendered = render(wrapInThemedTestApp(<NavItem isAlpha {...minProps} />));
expect(rendered.getByText('α')).toBeInTheDocument();
});
it('shows beta icon', () => {
const rendered = render(wrapInThemedTestApp(<NavItem isBeta {...minProps} />));
expect(rendered.getByText('β')).toBeInTheDocument();
});
it('renders nested navigation without exploding', () => {
const { getByText, getByTestId } = render(
wrapInThemedTestApp(
<NavItem {...minProps}>
<NavItem {...minChildProps} />
</NavItem>,
),
);
expect(getByText('A link title')).toBeInTheDocument();
expect(getByTestId('expand-toggle')).toBeInTheDocument();
});
it('toggles nested navigation', () => {
const { getByText, getByTestId, queryByText } = render(
wrapInThemedTestApp(
<NavItem {...minProps}>
<NavItem {...minChildProps} />
</NavItem>,
),
);
expect(queryByText('A child link')).not.toBeInTheDocument();
fireEvent.click(getByTestId('expand-toggle'));
expect(getByText('A child title')).toBeInTheDocument();
});
it('shows nested navigation automatically if the child is active', () => {
matchPath.mockReturnValue(true);
const { getByText } = render(
wrapInThemedTestApp(
<NavItem {...minProps}>
<NavItem {...minChildProps} />
</NavItem>,
),
);
expect(getByText('A link title')).toBeInTheDocument();
expect(getByText('A child title')).toBeInTheDocument();
});
});
@@ -1,91 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { List, withStyles } from '@material-ui/core';
import ChevronLeft from '@material-ui/icons/ChevronLeft';
import ChevronRight from '@material-ui/icons/ChevronRight';
import { IconButton } from 'shared/components';
import { getFromStore, saveToStore } from 'shared/apis/storage/persistentStore';
const styles = theme => ({
nav: {
gridArea: 'pageNav',
width: 62,
marginLeft: theme.spacing(3),
marginTop: theme.spacing(3),
transition: 'width 0.07s, height 0s',
transitionTimingFunction: 'ease-in',
},
item: {
display: 'block',
},
list: {
padding: 0,
border: '0px solid #333333',
borderRadius: 6,
backgroundColor: '#333333',
},
buttonContainer: {
marginLeft: 'auto',
marginRight: 'auto',
marginTop: theme.spacing(0.5),
width: 48,
},
button: {
color: '#888',
},
});
class Navigation extends Component {
static propTypes = {
condensable: PropTypes.bool,
};
state = {
condensed: false,
};
constructor(props) {
super(props);
const condensedPref = getFromStore('navigation', 'condensed', false);
this.state.condensed = this.props.condensable && condensedPref;
}
buttonClicked = () => {
const condensed = !this.state.condensed;
saveToStore('navigation', 'condensed', condensed);
this.setState({ condensed });
};
render() {
const { classes, condensable } = this.props;
const { condensed } = this.state;
const ExpandButton = condensable && (
<div className={classes.buttonContainer}>
<IconButton className={classes.button} onClick={this.buttonClicked} data-testid="nav-expand-toggle">
{condensed ? <ChevronRight alt="nav-expand" /> : <ChevronLeft alt="nav-reduce" />}
</IconButton>
</div>
);
const children = React.Children.map(this.props.children, child => {
if (!child) return null;
return React.cloneElement(child, {
condensed: condensed,
});
});
return (
<nav className={classes.nav} style={condensed ? {} : { width: 220 }}>
<List className={classes.list} gacontext="Navigation">
{children}
</List>
{ExpandButton}
</nav>
);
}
}
export default withStyles(styles)(Navigation);
@@ -1,75 +0,0 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Navigation from './Navigation';
import NavItem from './NavItem';
import { wrapInThemedTestApp } from '../../testUtils';
import { default as GcpProjectIcon } from './GcpProjectByTools';
import { default as OverviewIcon } from './InfoSquared';
describe('<Navigation />', () => {
it('renders without exploding', () => {
const nav = (
<Navigation>
<NavItem title="Overview" icon={<OverviewIcon />} href={'/'} />
<NavItem
title="Projects"
icon={<GcpProjectIcon />}
href={'/projects'}
/>
</Navigation>
);
const rendered = render(wrapInThemedTestApp(nav));
expect(rendered.getByText('Overview')).toBeInTheDocument();
expect(rendered.getByText('Projects')).toBeInTheDocument();
});
it('renders a condenseable navigation bar', () => {
const nav = (
<Navigation condensable={true}>
<NavItem title="Overview" icon={<OverviewIcon />} href={'/'} />
<NavItem
title="Projects"
icon={<GcpProjectIcon />}
href={'/projects'}
/>
</Navigation>
);
const rendered = render(wrapInThemedTestApp(nav));
const btn = rendered.getByTestId('nav-expand-toggle');
expect(btn).toBeInTheDocument();
});
it('is condenseable', () => {
const nav = (
<Navigation condensable={true}>
<NavItem title="Overview" icon={<OverviewIcon />} href={'/'} />
<NavItem
title="Projects"
icon={<GcpProjectIcon />}
href={'/projects'}
/>
</Navigation>
);
const rendered = render(wrapInThemedTestApp(nav));
const btn = rendered.getByTestId('nav-expand-toggle');
fireEvent.click(btn);
expect(rendered.queryByText('Overview')).not.toBeInTheDocument();
});
it('should have an anchor with correct href', () => {
const nav = (
<Navigation>
<NavItem
title="Projects"
icon={<GcpProjectIcon />}
href={'/projects'}
/>
</Navigation>
);
const rendered = render(wrapInThemedTestApp(nav));
const anchor = rendered.container.querySelector('a');
expect(anchor.href).toBe('http://localhost/projects');
});
});
@@ -1,52 +1,3 @@
import '../../assets/bursts/bursts.css';
import circle1 from '../../assets/bursts/circle-1.svg';
import circle2 from '../../assets/bursts/circle-2.svg';
import triangle1 from '../../assets/bursts/triangle-1.svg';
import triangle2 from '../../assets/bursts/triangle-2.svg';
import triangle3 from '../../assets/bursts/triangle-3.svg';
import rectangle1 from '../../assets/bursts/rectangle-1.svg';
import rectangle2 from '../../assets/bursts/rectangle-2.svg';
// TODO move this file to shared and out of core
export const shapes = {
circle1: {
backgroundImage: `url(${circle1})`,
width: 'calc(400px + 20%)',
backgroundPosition: '100px 60%',
},
circle2: {
backgroundImage: `url(${circle2})`,
width: 'calc(400px + 20%)',
backgroundPosition: '100px 58%',
},
rectangle1: {
backgroundImage: `url(${rectangle1})`,
width: 'calc(400px + 20%)',
backgroundPosition: '100px 60%',
},
rectangle2: {
backgroundImage: `url(${rectangle2})`,
width: 'calc(400px + 20%)',
backgroundPosition: '30px 35%',
},
triangle1: {
backgroundImage: `url(${triangle1})`,
width: 'calc(500px + 30%)',
backgroundPosition: '100px calc(50px + 45%)',
},
triangle2: {
backgroundImage: `url(${triangle2})`,
width: 'calc(400px + 20%)',
backgroundPosition: '100px 38%',
},
triangle3: {
backgroundImage: `url(${triangle3})`,
width: 'calc(400px + 20%)',
backgroundPosition: '100px 76%',
},
};
export const gradients = {
blue: 'linear-gradient(135deg, #2D46B9 0%, #509BF5 100%)',
darkBlue: 'linear-gradient(44deg, #1E3264 0%, #A0C3D2 100%)',
@@ -71,102 +22,26 @@ export const theme = {
service: {
activeNavLinkColor: '#1D7F6E',
gradient: gradients.tealGreen,
burstShape: shapes.rectangle2,
},
appFeature: {
activeNavLinkColor: '#FF6437',
gradient: gradients.orangeYellow,
burstShape: shapes.triangle1,
},
app: {
activeNavLinkColor: '#A72525',
gradient: gradients.redOrange,
burstShape: shapes.triangle1,
},
endpoint: {
activeNavLinkColor: '#9315b0',
gradient: gradients.purpleBlue,
burstShape: shapes.circle1,
},
pipeline: {
activeNavLinkColor: '#9315b0',
gradient: gradients.purpleBlue,
burstShape: shapes.circle1,
burstShape: null,
},
website: {
activeNavLinkColor: '#765d90',
gradient: gradients.purple,
burstShape: shapes.rectangle2,
},
workflow: {
activeNavLinkColor: '#AF2996',
secondary: '#9315b0',
gradient: gradients.purpleBlue,
burstShape: shapes.circle1,
},
library: {
activeNavLinkColor: '#B39AC8',
gradient: gradients.sunset,
burstShape: shapes.triangle3,
},
other: {
activeNavLinkColor: '#8f6858',
gradient: gradients.brown,
burstShape: shapes.rectangle1,
},
system: {
activeNavLinkColor: '#526C90',
gradient: gradients.darkBlue,
burstShape: shapes.triangle2,
},
project: {
activeNavLinkColor: '#F13DA2',
gradient: gradients.pinkOrange,
burstShape: shapes.triangle3,
},
tool: {
activeNavLinkColor: '#B39AC8',
gradient: gradients.violetPeach,
burstShape: shapes.rectangle1,
burstShape: null,
},
home: {
activeNavLinkColor: '#00814e',
gradient: gradients.green,
burstShape: shapes.triangle2,
burstShape: null,
},
org: {
activeNavLinkColor: '#6044ef',
gradient: gradients.violetGreen,
burstShape: shapes.triangle3,
burstShape: null,
},
documentation: {
activeNavLinkColor: '#04c2ba',
gradient: gradients.tpm,
burstShape: shapes.circle2,
},
blogPost: {
activeNavLinkColor: '#FFF',
gradient: 'linear-gradient(90deg, #033A45 0%, #033A45 100%)',
burstShape: shapes.circle2,
},
machineLearning: {
activeNavLinkColor: '#0033DD',
gradient: gradients.royalBlue,
burstShape: shapes.circle1,
},
partnership: {
activeNavLinkColor: '#000000',
gradient: gradients.grey,
burstShape: shapes.rectangle2,
},
ml: {
activeNavLinkColor: '#B39AC8',
gradient: gradients.blue,
burstShape: shapes.circle1,
},
scienceBox: {
activeNavLinkColor: '#69B9FF', // matches sky gradient beginning point
gradient: gradients.sky,
burstShape: shapes.circle1,
burstShape: null,
},
};
@@ -1,2 +1,2 @@
export { default } from './Page';
export { shapes, gradients, theme } from './PageThemeProvider';
export { gradients, theme } from './PageThemeProvider';
@@ -1,114 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { compose } from 'recompose';
import { List, ListItem, ListItemIcon, Popover, Typography, withStyles } from '@material-ui/core';
import { withPluginContext } from 'core/app/withPluginContext';
import { EmailIcon, SlackIcon, SupportIcon } from 'shared/icons';
import { Button, Link } from 'shared/components';
import { StackOverflow, StackOverflowTag } from 'shared/components/layout';
class SupportButton extends Component {
static defaultProps = {
slackChannel: '#backstage',
};
static propTypes = {
slackChannel: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
};
state = {
popoverOpen: false,
anchorEl: null,
};
button_clickHandler = event => {
this.setState({ anchorEl: event.currentTarget, popoverOpen: true });
};
popover_closeHandler = () => {
this.setState({ popoverOpen: false });
};
render() {
const { slackChannel, email, children, classes, plugin } = this.props;
const tags = plugin ? plugin.stackoverflowTags : undefined; // plugin is undefined on base routes
const { popoverOpen, anchorEl } = this.state;
const slackChannels = slackChannel ? (Array.isArray(slackChannel) ? slackChannel : [slackChannel]) : null;
const contactEmails = email ? (Array.isArray(email) ? email : [email]) : null;
return (
<Fragment>
<Button color="primary" onClick={this.button_clickHandler}>
<SupportIcon className={classes.leftIcon} />
Support
</Button>
<Popover
open={popoverOpen}
anchorEl={anchorEl}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
onClose={this.popover_closeHandler}
gacontext="Support"
>
<List className={classes.popoverList}>
{React.Children.map(children, (child, index) => (
<ListItem alignItems="flex-start" key={index}>
{child}
</ListItem>
))}
{tags && tags.length > 0 && (
<ListItem alignItems="flex-start">
<StackOverflow>
{tags.map((tag, idx) => (
<StackOverflowTag key={idx} tag={tag} />
))}
</StackOverflow>
</ListItem>
)}
{slackChannels && (
<ListItem alignItems="flex-start">
<ListItemIcon>
<SlackIcon />
</ListItemIcon>
<div>
<Typography variant="subtitle1">Support</Typography>
{slackChannels.map((channel, index) => (
<Typography key={index}>
<Link slackChannel={channel}>{channel}</Link>
</Typography>
))}
</div>
</ListItem>
)}
{contactEmails && (
<ListItem alignItems="flex-start">
<ListItemIcon>
<EmailIcon />
</ListItemIcon>
<div>
<Typography variant="subtitle1">Contact</Typography>
{contactEmails.map((email, index) => (
<Typography key={index}>
<Link email={email}>{email}</Link>
</Typography>
))}
</div>
</ListItem>
)}
</List>
</Popover>
</Fragment>
);
}
}
const styles = theme => ({
leftIcon: {
marginRight: theme.spacing(1),
},
popoverList: {
minWidth: 260,
maxWidth: 320,
},
});
export default compose(withStyles(styles), withPluginContext)(SupportButton);
@@ -1,36 +0,0 @@
import React from 'react';
import { render, fireEvent, waitForElement } from '@testing-library/react';
import SupportButton from './SupportButton';
import { wrapInThemedTestApp, Keyboard } from '../../testUtils';
describe('<SupportButton />', () => {
it('renders without exploding', () => {
const rendered = render(<SupportButton />);
rendered.getByText('Support');
fireEvent.click(rendered.baseElement);
});
it('should show popover', () => {
const rendered = render(
wrapInThemedTestApp(<SupportButton email="a@b.com" plugin={{ stackoverflowTags: ['a-tag'] }} />),
);
expect(rendered.queryByText('Contact')).not.toBeInTheDocument();
fireEvent.click(rendered.getByText('Support'));
expect(rendered.queryByText('Contact')).toBeInTheDocument();
rendered.getByText('a@b.com');
rendered.getByText('a-tag');
});
it('should close popover when hitting escape', async () => {
const rendered = render(wrapInThemedTestApp(<SupportButton email="a@b.com" />));
fireEvent.click(rendered.getByText('Support'));
expect(rendered.queryByText('Contact')).toBeInTheDocument();
await Keyboard.type(rendered, '<Esc>');
await waitForElement(() => !rendered.queryByText('Contact'));
expect(rendered.queryByText('Contact')).not.toBeInTheDocument();
});
});
+14 -250
View File
@@ -1,46 +1,31 @@
/**
* Helpers for testing redux components and containers
* Helpers for testing components
*/
import React from 'react';
import { MuiThemeProvider } from '@material-ui/core';
import { ThemeProvider } from '@material-ui/styles';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { ApolloProvider } from '@apollo/react-hooks';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router';
import { Route } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { V1 } from 'core/app/Themes';
import defaultAppState from 'core/defaultAppState';
import store from 'core/store';
import ErrorBoundary from 'shared/components/ErrorBoundary';
import { SearchProvider } from 'plugins/searchPage/SearchContext';
import { render } from '@testing-library/react';
import { FirestoreProvider } from 'shared/apis/firestore';
import MockFirestoreStorage from 'shared/apis/firestore/MockFirestoreStorage';
import { UserInfoProvider, defaultMockUser } from 'shared/apis/user';
import { V1 } from '../theme/BackstageTheme';
import ErrorBoundary from '../layout/ErrorBoundary';
export { default as Keyboard } from './Keyboard';
export { default as mockBreakpoint } from './mockBreakpoint';
export function wrapInTestApp(Component, initialRouterEntries, user = defaultMockUser) {
export function wrapInTestApp(
Component,
initialRouterEntries,
) {
const Wrapper = Component instanceof Function ? Component : () => Component;
return (
<Provider store={store}>
<UserInfoProvider user={user}>
<FirestoreProvider api={new MockFirestoreStorage()}>
<MemoryRouter initialEntries={initialRouterEntries || ['/']}>
<ErrorBoundary>
<Route component={Wrapper} />
</ErrorBoundary>
</MemoryRouter>
</FirestoreProvider>
</UserInfoProvider>
</Provider>
<MemoryRouter initialEntries={initialRouterEntries || ['/']}>
<ErrorBoundary>
<Route component={Wrapper} />
</ErrorBoundary>
</MemoryRouter>
);
}
@@ -53,227 +38,6 @@ export function wrapInThemedTestApp(component, initialRouterEntries) {
return wrapInTestApp(themed, initialRouterEntries);
}
export function wrapInSearchTestApp(component) {
const search = <SearchProvider>{component}</SearchProvider>;
return wrapInThemedTestApp(search);
}
export function getTestStore(state) {
const appState = state ? state : defaultAppState;
const mockStore = configureStore([thunk]);
return mockStore(appState);
}
export function getGoogleProfile() {
return {
getName: () => 'Niklas',
getGivenName: () => 'Niklas',
getEmail: () => 'niklas@spotify.com',
getImageUrl: () => 'http://img.url',
getId: () => '123456',
};
}
const DEFAULT_APOLLO_STATE = {
data: { loading: true },
};
export const ApolloMock = ({ children, state = DEFAULT_APOLLO_STATE }) => {
// Hideous mocking of the ApolloClient to use the ApolloProvider
// The test do not have information about the GraphQL queries made by the components
// and hence we cant use MockedProvider and use the one below.
const mockClient = {
query: async () => state,
watchQuery: () => {
return {
refetch: () => {},
fetchMore: () => {},
updateQuery: () => {},
startPolling: () => {},
stopPolling: () => {},
subscribeToMore: () => {},
options: {
fetchPolicy: {},
},
getLastResult: () => {},
currentResult: () => state,
getCurrentResult: () => state,
resetQueryStoreErrors: () => {},
subscribe: () => {},
setOptions: () => Promise.resolve({}),
};
},
};
return <ApolloProvider client={mockClient}>{children}</ApolloProvider>;
};
/**
* Return a builder for a given Component which will permit fluent immutable
* configuration and access to @testing-library/react rendering functions.
*
* A full example will make the utility of this more clear:
*
* const builder = buildComponentInApp(MyComponent)
* .withProps({myProp: 'myValue'})
*
* // MuiThemeProvider will wrap MyComponent when this is called:
* .withTheme()
*
* // MemoryRouter can be configured with initialEntries:
* .withRouterEntries(['/'])
*
* // ApolloProvider wraps MyComponent with a mock client setup to
* // provide the given data:
* .withApolloData({
* component: {id: 'my-component', system: 'my-system'}
* });
*
* // Test 1: Use the defaults from above when mounting
* const { getByText } = builder.render();
* expect(getByText('Display Text').querySelectorAll('td')).toHaveLength(1);
* // Test 2: ApolloRouter provides loading=true state instead of valid data
* const { queryByText } = builder.withApolloLoading().render();
* queryByText('Content').toBeNull();
*/
export function buildComponentInApp(Component) {
function createBuilder(opt = {}) {
const builder = {
withProps: (props = {}) =>
createBuilder({
...opt,
props: Object.assign(opt.props || {}, props),
}),
withTheme: (theme = V1) =>
createBuilder({
...opt,
theme,
}),
withRouterEntries: routerEntries =>
createBuilder({
...opt,
routerEntries,
}),
withApolloLoading: () =>
// Support `data.loading` (errorOrLoading) or just `loading` (Query)
createBuilder({
...opt,
apolloState: { data: { loading: true }, loading: true },
}),
withApolloData: data =>
createBuilder({
...opt,
apolloState: { data: { loading: false, ...data } },
}),
withApolloError: error =>
createBuilder({
...opt,
apolloState: { error: error },
}),
withUser: user =>
createBuilder({
...opt,
user,
}),
build: (argProps = {}) => {
const props = { ...(opt.props || {}), ...argProps };
const component = <Component {...props} />;
const routed = wrapInTestApp(component, opt.routerEntries, opt.user);
const themed = opt.theme ? <MuiThemeProvider theme={opt.theme}>{routed}</MuiThemeProvider> : routed;
return opt.apolloState ? <ApolloMock state={opt.apolloState}>{themed}</ApolloMock> : themed;
},
render: props => {
return render(builder.build(props));
},
renderWithEffects: async props => {
return renderWithEffects(builder.build(props));
},
};
return builder;
}
return createBuilder();
}
// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async
// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit
// cleaner, since act doesn't return the result of the evaluated function.
// https://github.com/testing-library/react-testing-library/issues/281
// https://github.com/facebook/react/pull/14853
export async function renderWithEffects(nodes) {
let value;
await act(async () => {
value = await render(nodes);
});
return value;
}
export function logBoundaryErrors() {
ErrorBoundary.onError = error => console.error('ErrorBoundary received error', error);
}
// Runs a function with all error logging captured and returned as an array.
// If the callback function is async this one will be too.
export function withLogCollector(logsToCollect, callback) {
if (typeof logsToCollect === 'function') {
callback = logsToCollect;
logsToCollect = ['log', 'warn', 'error'];
}
const logs = {
log: [],
warn: [],
error: [],
};
const origLog = console.log;
const origWarn = console.warn;
const origError = console.error;
if (logsToCollect.includes('log')) {
console.log = message => {
logs.log.push(message);
};
}
if (logsToCollect.includes('warn')) {
console.warn = message => {
logs.warn.push(message);
};
}
if (logsToCollect.includes('error')) {
console.error = message => {
logs.error.push(message);
};
}
const restore = () => {
console.log = origLog;
console.warn = origWarn;
console.error = origError;
};
try {
const ret = callback();
if (!ret || !ret.then) {
restore();
return logs;
}
return ret.then(
() => {
restore();
return logs;
},
error => {
restore();
throw error;
},
);
} catch (error) {
restore();
throw error;
}
}
export const wrapInTheme = (component, theme = V1) => (
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>{component}</ThemeProvider>
@@ -355,4 +355,4 @@ const styles = theme => ({
});
export const withGlobalStyles = withStyles(styles);
export default BackstageTheme;
export default BackstageTheme;
+78 -5
View File
@@ -3596,7 +3596,7 @@ arrify@^1.0.1:
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
asap@^2.0.0, asap@~2.0.6:
asap@^2.0.0, asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -4452,6 +4452,11 @@ chalk@^1.0.0, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
change-emitter@^0.1.2:
version "0.1.6"
resolved "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -5123,6 +5128,11 @@ core-js-pure@^3.0.0:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.4.0, core-js@^2.5.0:
version "2.6.11"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
@@ -6776,6 +6786,19 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fbjs@^0.8.1:
version "0.8.17"
resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -7685,6 +7708,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^2.3.1:
version "2.5.5"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -8572,7 +8600,7 @@ is-ssh@^1.3.0:
dependencies:
protocols "^1.1.0"
is-stream@^1.0.0, is-stream@^1.1.0:
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@@ -8665,6 +8693,14 @@ isobject@^4.0.0:
resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -11053,6 +11089,14 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
node-fetch@^2.3.0, node-fetch@^2.5.0:
version "2.6.0"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
@@ -13015,6 +13059,13 @@ promise-retry@^1.1.1:
err-code "^1.0.0"
retry "^0.10.0"
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
promise@^8.0.3:
version "8.0.3"
resolved "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6"
@@ -13329,6 +13380,11 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i
resolved "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
react-lifecycles-compat@^3.0.2:
version "3.0.4"
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-router-dom@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
@@ -13583,6 +13639,18 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"
recompose@0.30.0:
version "0.30.0"
resolved "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0"
integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==
dependencies:
"@babel/runtime" "^7.0.0"
change-emitter "^0.1.2"
fbjs "^0.8.1"
hoist-non-react-statics "^2.3.1"
react-lifecycles-compat "^3.0.2"
symbol-observable "^1.0.4"
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@@ -14287,7 +14355,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@^1.0.4:
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -15093,7 +15161,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.1.0:
symbol-observable@^1.0.4, symbol-observable@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -15548,6 +15616,11 @@ typescript@^3.7.4, typescript@^3.7.5:
resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
ua-parser-js@^0.7.18:
version "0.7.21"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
uglify-js@3.4.x:
version "3.4.10"
resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
@@ -16057,7 +16130,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
whatwg-fetch@^3.0.0:
whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==