fix tests
@@ -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%')}>α</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}>β</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,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>
|
||||
|
||||
<AlphaLabel isShorthand />
|
||||
</Fragment>
|
||||
)}
|
||||
{isBeta && (
|
||||
<Fragment>
|
||||
|
||||
<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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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==
|
||||
|
||||