Pull from dev, conflict fix

This commit is contained in:
Mikhail Mikheev 2019-06-13 16:55:30 +04:00
commit 8ff899dce1
108 changed files with 1977 additions and 4110 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules/
build_webpack/
build_storybook/
.DS_Store
.DS_Store
build/

View File

@ -102,8 +102,9 @@ module.exports = {
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true,
localIdentName: '[name]__[local]___[hash:base64:5]',
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]',
},
},
},
{

View File

@ -0,0 +1,6 @@
pragma solidity ^0.5.2;
import "../src/test/contracts/TokenOMG.sol";
import "../src/test/contracts/TokenRDN.sol";
contract DevDependenciesGetter {}

23
contracts/Migrations.sol Normal file
View File

@ -0,0 +1,23 @@
pragma solidity ^0.5.2;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
constructor () public {
owner = msg.sender;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

View File

@ -14,7 +14,7 @@ module.exports = {
'<rootDir>/config/jest/LocalStorageMock.js',
'<rootDir>/config/jest/Web3Mock.js',
],
setupFilesAfterEnv: ['<rootDir>/config/jest/jest.setup.js', 'react-testing-library/cleanup-after-each'],
setupFilesAfterEnv: ['<rootDir>/config/jest/jest.setup.js', '@testing-library/react/cleanup-after-each'],
testEnvironment: 'node',
testMatch: ['<rootDir>/src/**/__tests__/**/*.js?(x)', '<rootDir>/src/**/?(*.)(spec|test).js?(x)'],
testURL: 'http://localhost:8000',

View File

@ -0,0 +1,4 @@
// @flow
const Migrations = artifacts.require('./Migrations.sol')
module.exports = deployer => deployer.deploy(Migrations)

View File

@ -0,0 +1,21 @@
// @flow
/* eslint-disable no-console */
const TokenOMG = artifacts.require('TokenOMG')
const TokenRDN = artifacts.require('TokenRDN')
module.exports = (deployer, network) => {
let toBN
if (typeof web3.version === 'string') {
// 1.X.xx Web3
({ toBN } = web3.utils)
} else {
toBN = web3.toBigNumber
}
if (network === 'development') {
return deployer
.deploy(TokenOMG, toBN(50000).mul(toBN(10).pow(toBN(18))))
.then(() => deployer.deploy(TokenRDN, toBN(50000).mul(toBN(10).pow(toBN(18)))))
}
return console.log('Not running on development, skipping.')
}

View File

@ -35,10 +35,10 @@
"@material-ui/core": "4.1.0",
"@material-ui/icons": "4.1.0",
"@welldone-software/why-did-you-render": "^3.0.9",
"axios": "^0.18.0",
"axios": "0.19.0",
"bignumber.js": "9.0.0",
"connected-react-router": "^6.3.1",
"final-form": "4.13.1",
"final-form": "4.14.1",
"history": "^4.7.2",
"immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9",
@ -47,11 +47,11 @@
"qrcode.react": "^0.9.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-final-form": "6.0.1",
"react-final-form": "6.1.0",
"react-final-form-listeners": "^1.0.2",
"react-hot-loader": "4.11.0",
"react-infinite-scroll-component": "^4.5.2",
"react-redux": "7.0.3",
"react-redux": "7.1.0",
"react-router-dom": "^4.3.1",
"recompose": "^0.30.0",
"redux": "^4.0.1",
@ -86,10 +86,11 @@
"@babel/preset-flow": "^7.0.0-beta.40",
"@babel/preset-react": "^7.0.0-beta.40",
"@sambego/storybook-state": "^1.0.7",
"@storybook/addon-actions": "5.1.3",
"@storybook/addon-knobs": "5.1.3",
"@storybook/addon-links": "5.1.3",
"@storybook/react": "5.1.3",
"@storybook/addon-actions": "5.1.4",
"@storybook/addon-knobs": "5.1.4",
"@storybook/addon-links": "5.1.4",
"@storybook/react": "5.1.4",
"@testing-library/react": "^8.0.1",
"autoprefixer": "9.6.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
@ -99,23 +100,24 @@
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0",
"classnames": "^2.2.5",
"css-loader": "^2.1.0",
"css-loader": "3.0.0",
"detect-port": "^1.2.2",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-flowtype": "3.10.1",
"eslint-plugin-flowtype": "3.10.2",
"eslint-plugin-import": "2.17.3",
"eslint-plugin-jest": "22.6.4",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "7.13.0",
"ethereumjs-abi": "^0.6.7",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"flow-bin": "0.100.0",
"file-loader": "4.0.0",
"flow-bin": "0.101.0",
"fs-extra": "8.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.0.4",
"jest": "24.8.0",
"jest-dom": "^3.4.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "0.7.0",
"postcss-loader": "^3.0.0",
@ -123,18 +125,17 @@
"postcss-simple-vars": "^5.0.2",
"pre-commit": "^1.2.2",
"prettier-eslint-cli": "^4.7.1",
"react-testing-library": "^7.0.1",
"run-with-testrpc": "0.3.1",
"storybook-host": "^5.0.3",
"storybook-router": "^0.3.3",
"style-loader": "^0.23.1",
"truffle": "5.0.21",
"truffle-contract": "4.0.19",
"truffle-solidity-loader": "0.1.20",
"truffle": "5.0.22",
"truffle-contract": "4.0.20",
"truffle-solidity-loader": "0.1.21",
"uglifyjs-webpack-plugin": "2.1.3",
"webpack": "4.33.0",
"webpack": "4.34.0",
"webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "3.3.3",
"webpack-cli": "3.3.4",
"webpack-dev-server": "3.7.1",
"webpack-manifest-plugin": "^2.0.0-rc.2"
}

View File

@ -35,6 +35,23 @@ yarn start
## Running the tests
To run the test, you'll need to migrate contracts `safe-contracts` to the local ganache-cli
1. Migrating Safe Contracts:
```
git clone https://github.com/gnosis/safe-contracts.git
cd safe-contracts
yarn
ganache-cli -l 7000000
npx truffle compile
npx truffle migrate
```
2. Compiling Token Contracts for the tests:
Inside `safe-react` directory
```
npx truffle compile
```
3. Run the tests:
```
yarn test
```

View File

@ -19,8 +19,8 @@ const logo = require('../assets/gnosis-safe-logo.svg')
type Props = Open & {
classes: Object,
providerDetails: React$Node,
providerInfo: React$Node,
providerDetails: React.Node,
providerInfo: React.Node,
}
const styles = () => ({

View File

@ -10,8 +10,8 @@ import { sm, md } from '~/theme/variables'
type Props = Open & {
classes: Object,
popupDetails: React$Node,
info: React$Node,
popupDetails: React.Node,
info: React.Node,
children: Function,
}

View File

@ -2,7 +2,7 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { logComponentStack, type Info } from '~/utils/logBoundaries'
import { SharedSnackbarConsumer, type Variant } from '~/components/SharedSnackBar/Context'
import { SharedSnackbarConsumer, type Variant } from '~/components/SharedSnackBar'
import { WALLET_ERROR_MSG } from '~/logic/wallets/store/actions'
import { getProviderInfo } from '~/logic/wallets/getWeb3'
import ProviderAccesible from './component/ProviderInfo/ProviderAccesible'
@ -53,7 +53,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
let currentProvider: ProviderProps = await getProviderInfo()
fetchProvider(currentProvider, openSnackbar)
this.providerListener = setInterval(async () => {
this.providerListener = setInterval(async () => {
const newProvider: ProviderProps = await getProviderInfo()
if (JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, openSnackbar)

View File

@ -9,7 +9,7 @@ type Props = {
description: string,
open: boolean,
handleClose: Function,
children: React$Node,
children: React.Node,
classes: Object,
modalClassName: ?string,
paperClassName: ?string,

View File

@ -1,67 +0,0 @@
// @flow
import * as React from 'react'
import SharedSnackBar from './index'
const SharedSnackbarContext = React.createContext({
openSnackbar: undefined,
closeSnackbar: undefined,
snackbarIsOpen: false,
message: '',
variant: 'info',
})
type Props = {
children: React$Node,
}
export type Variant = 'success' | 'error' | 'warning' | 'info'
type State = {
isOpen: boolean,
message: string,
variant: Variant,
}
export class SharedSnackbarProvider extends React.Component<Props, State> {
state = {
isOpen: false,
message: '',
variant: 'info',
}
openSnackbar = (message: string, variant: Variant) => {
this.setState({
message,
variant,
isOpen: true,
})
}
closeSnackbar = () => {
this.setState({
message: '',
isOpen: false,
})
}
render() {
const { children } = this.props
return (
<SharedSnackbarContext.Provider
value={{
openSnackbar: this.openSnackbar,
closeSnackbar: this.closeSnackbar,
snackbarIsOpen: this.state.isOpen,
message: this.state.message,
variant: this.state.variant,
}}
>
<SharedSnackBar />
{children}
</SharedSnackbarContext.Provider>
)
}
}
export const SharedSnackbarConsumer = SharedSnackbarContext.Consumer

View File

@ -2,9 +2,8 @@
import * as React from 'react'
import { Snackbar } from '@material-ui/core'
import SnackbarContent from '~/components/SnackbarContent'
import { SharedSnackbarConsumer } from './Context'
const SharedSnackbar = () => (
export const SharedSnackbar = () => (
<SharedSnackbarConsumer>
{(value) => {
const {
@ -32,4 +31,75 @@ const SharedSnackbar = () => (
</SharedSnackbarConsumer>
)
export default SharedSnackbar
type SnackbarContext = {
openSnackbar: Function,
closeSnackbar: Function,
snackbarIsOpen: boolean,
message: string,
variant: string,
}
const SharedSnackbarContext = React.createContext<SnackbarContext>({
openSnackbar: undefined,
closeSnackbar: undefined,
snackbarIsOpen: false,
message: '',
variant: 'info',
})
type Props = {
children: React.Node,
}
export type Variant = 'success' | 'error' | 'warning' | 'info'
type State = {
isOpen: boolean,
message: string,
variant: Variant,
}
export class SharedSnackbarProvider extends React.Component<Props, State> {
state = {
isOpen: false,
message: '',
variant: 'info',
}
openSnackbar = (message: string, variant: Variant) => {
this.setState({
message,
variant,
isOpen: true,
})
}
closeSnackbar = () => {
this.setState({
message: '',
isOpen: false,
})
}
render() {
const { children } = this.props
const { message, variant, isOpen } = this.state
return (
<SharedSnackbarContext.Provider
value={{
openSnackbar: this.openSnackbar,
closeSnackbar: this.closeSnackbar,
snackbarIsOpen: isOpen,
message,
variant,
}}
>
<SharedSnackbar />
{children}
</SharedSnackbarContext.Provider>
)
}
}
export const SharedSnackbarConsumer = SharedSnackbarContext.Consumer

View File

@ -21,8 +21,8 @@ const styles = () => ({
type Props = {
classes: Object,
children: React$Node,
controls: React$Node,
children: React.Node,
controls: React.Node,
container?: number,
padding?: boolean,
}

View File

@ -2,7 +2,7 @@
import * as React from 'react'
type Props = {
children: React$Node,
children: React.Node,
}
const Step = ({ children }: Props) => (

View File

@ -16,7 +16,7 @@ export { default as Step } from './Step'
type Props = {
steps: string[],
onSubmit: (values: Object) => Promise<void>,
children: React$Node,
children: React.Node,
classes: Object,
onReset?: () => void,
initialValues?: Object,
@ -73,13 +73,13 @@ class GnoStepper extends React.PureComponent<Props, State> {
}))
}
getPageProps = (pages: React$Node): PageProps => {
getPageProps = (pages: React.Node): PageProps => {
const { page } = this.state
return React.Children.toArray(pages)[page].props
}
getActivePageFrom = (pages: React$Node) => {
getActivePageFrom = (pages: React.Node) => {
const activePageProps = this.getPageProps(pages)
const { children, ...props } = activePageProps

View File

@ -15,6 +15,7 @@ export type Column = {
label: string,
custom: boolean, // If content will be rendered by user manually
width?: number,
static?: boolean, // If content can't be sorted by values in the column
}
export const cellWidth = (width: number | typeof undefined) => {
@ -54,13 +55,17 @@ class GnoTableHead extends React.PureComponent<Props> {
padding={column.disablePadding ? 'none' : 'default'}
sortDirection={orderBy === column.id ? order : false}
>
<TableSortLabel
active={orderBy === column.id}
direction={order}
onClick={this.changeSort(column.id, column.order)}
>
{column.label}
</TableSortLabel>
{column.static ? (
column.label
) : (
<TableSortLabel
active={orderBy === column.id}
direction={order}
onClick={this.changeSort(column.id, column.order)}
>
{column.label}
</TableSortLabel>
)}
</TableCell>
))}
</TableRow>

View File

@ -1,4 +1,5 @@
// @flow
import { List } from 'immutable'
export const FIXED = 'fixed'
type Fixed = {
@ -23,13 +24,14 @@ const desc = (a: Object, b: Object, orderBy: string, orderProp: boolean) => {
}
// eslint-disable-next-line
export const stableSort = <SortRow>(array: Array<SortRow>, cmp: any, fixed: boolean): Array<SortRow> => {
const fixedElems: Array<SortRow> = fixed ? array.filter((elem: any) => elem.fixed) : []
const data: Array<SortRow> = fixed ? array.filter((elem: any) => !elem[FIXED]) : array
const stabilizedThis = data.map((el, index) => [el, index])
export const stableSort = (dataArray: List<any>, cmp: any, fixed: boolean): List<any> => {
const fixedElems: List<any> = fixed ? dataArray.filter((elem: any) => elem.fixed) : List([])
const data: List<any> = fixed ? dataArray.filter((elem: any) => !elem[FIXED]) : dataArray
let stabilizedThis = data.map((el, index) => [el, index])
stabilizedThis.sort((a, b) => {
stabilizedThis = stabilizedThis.sort((a, b) => {
const order = cmp(a[0], b[0])
if (order !== 0) {
return order
}
@ -37,7 +39,7 @@ export const stableSort = <SortRow>(array: Array<SortRow>, cmp: any, fixed: bool
return a[1] - b[1]
})
const sortedElems: Array<SortRow> = stabilizedThis.map(el => el[0])
const sortedElems: List<any> = stabilizedThis.map(el => el[0])
return fixedElems.concat(sortedElems)
}

View File

@ -29,6 +29,7 @@ class TextField extends React.PureComponent<TextFieldProps> {
text,
inputAdornment,
classes,
testId,
...rest
} = this.props
const helperText = value ? text : undefined
@ -36,7 +37,7 @@ class TextField extends React.PureComponent<TextFieldProps> {
const underline = meta.active || (meta.visited && !meta.valid)
const inputRoot = helperText ? classes.root : undefined
const inputProps = { ...restInput, autoComplete: 'off' }
const inputProps = { ...restInput, autoComplete: 'off', 'data-testid': testId }
const inputRootProps = { ...inputAdornment, disableUnderline: !underline, className: inputRoot }
return (
@ -51,6 +52,7 @@ class TextField extends React.PureComponent<TextFieldProps> {
inputProps={inputProps}
onChange={onChange}
value={value}
// data-testid={testId}
/>
)
}

View File

@ -11,7 +11,7 @@ type Props = {
margin?: Size,
padding?: Size,
align?: 'center' | 'right' | 'left',
children: React$Node,
children: React.Node,
className?: string,
}

View File

@ -2,7 +2,7 @@
import * as React from 'react'
type Props = {
children: React$Node,
children: React.Node,
}
class Bold extends React.PureComponent<Props> {

View File

@ -12,6 +12,7 @@ const styles = {
type Props = {
minWidth?: number,
minHeight?: number,
testId: string,
}
const calculateStyleBased = (minWidth, minHeight) => ({
@ -19,10 +20,12 @@ const calculateStyleBased = (minWidth, minHeight) => ({
minHeight: minHeight && `${minHeight}px`,
})
const GnoButton = ({ minWidth, minHeight, ...props }: Props) => {
const GnoButton = ({
minWidth, minHeight, testId = '', ...props
}: Props) => {
const style = calculateStyleBased(minWidth, minHeight)
return <Button style={style} {...props} />
return <Button style={style} data-testid={testId} {...props} />
}
export default withStyles(styles)(GnoButton)

View File

@ -1,6 +1,5 @@
// @flow
/* eslint-disable react/button-has-type */
/* eslint-disable react/default-props-match-prop-types */
import * as React from 'react'
import cn from 'classnames/bind'
import styles from './index.scss'
@ -12,20 +11,16 @@ type Props = {
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
weight?: 'light' | 'regular' | 'bolder' | 'bold',
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
testId?: string,
}
const GnoButtonLink = ({
type, size, weight, color, ...props
}: Props) => (
<button type={type} className={cx(styles.btnLink, size, color, weight)} {...props} />
)
GnoButtonLink.defaultProps = {
type: 'button',
size: 'md',
weight: 'regular',
color: 'secondary',
}
type = 'button',
size = 'md',
weight = 'regular',
color = 'secondary',
testId = '',
...props
}: Props) => <button type={type} className={cx(styles.btnLink, size, color, weight)} data-testid={testId} {...props} />
export default GnoButtonLink

View File

@ -27,7 +27,7 @@ type Props = {
mdOffset?: number,
lgOffset?: number,
className?: string,
children: React$Node,
children: React.Node,
}
const Col = ({

View File

@ -14,7 +14,7 @@ type Props = {
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
tag: HeadingTag,
truncate?: boolean,
children: React$Node,
children: React.Node,
}
class Heading extends React.PureComponent<Props> {

View File

@ -10,7 +10,7 @@ type Props = {
fullwidth?: boolean,
bordered?: boolean,
className?: string,
style?: React$Node,
style?: React.Node,
}
const Img = ({

View File

@ -10,7 +10,7 @@ const cx = classNames.bind(styles)
type Props = {
padding?: 'xs' | 'sm' | 'md',
to: string,
children: React$Node,
children: React.Node,
color?: 'regular' | 'white',
className?: string,
innerRef: React.ElementRef<any>,

View File

@ -6,7 +6,7 @@ import styles from './index.scss'
const cx = classNames.bind(styles)
type Props = {
children: React$Node,
children: React.Node,
align?: 'center',
overflow?: boolean
}

View File

@ -2,11 +2,11 @@
import React from 'react'
import Footer from '~/components/Footer'
import Header from '~/components/Header'
import { SharedSnackbarProvider } from '~/components/SharedSnackBar/Context'
import { SharedSnackbarProvider } from '~/components/SharedSnackBar'
import styles from './index.scss'
type Props = {
children: React$Node,
children: React.Node,
}
const PageFrame = ({ children }: Props) => (

View File

@ -13,7 +13,7 @@ type Props = {
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
transform?: 'capitalize' | 'lowercase' | 'uppercase',
children: React$Node,
children: React.Node,
dot?: boolean,
className?: string,
}

View File

@ -6,7 +6,7 @@ import styles from './index.scss'
const cx = classNames.bind(styles)
type Props = {
children: React$Node,
children: React.Node,
}
const Pre = ({ children, ...props }: Props) => (

View File

@ -8,7 +8,7 @@ const cx = classNames.bind(styles)
type Props = {
className?: string,
children: React$Node,
children: React.Node,
margin?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
align?: 'center' | 'end' | 'start',
grow?: boolean,

View File

@ -2,7 +2,7 @@
import React, { PureComponent } from 'react'
type Props = {
children: React$Node
children: React.Node
}
class Span extends PureComponent<Props> {

View File

@ -6,10 +6,12 @@ import TableCell from '@material-ui/core/TableCell'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
export { TableBody, TableCell, TableHead, TableRow }
export {
TableBody, TableCell, TableHead, TableRow,
}
type Props = {
children: React$Node,
children: React.Node,
size?: number
}
@ -35,4 +37,3 @@ const GnoTable = ({ size, children }: Props) => {
}
export default GnoTable

View File

@ -7,7 +7,6 @@ import {
OPEN_ADDRESS,
SAFE_PARAM_ADDRESS,
WELCOME_ADDRESS,
SETTINS_ADDRESS,
OPENING_ADDRESS,
LOAD_ADDRESS,
} from './routes'
@ -23,9 +22,6 @@ const Opening = React.lazy(() => import('./opening/container'))
const Load = React.lazy(() => import('./load/container/Load'))
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
const SAFE_SETTINGS = `${SAFE_ADDRESS}${SETTINS_ADDRESS}`
export const settingsUrlFrom = (safeAddress: string) => `${SAFELIST_ADDRESS}/${safeAddress}${SETTINS_ADDRESS}`
const Routes = () => (
<Switch>

View File

@ -122,7 +122,7 @@ const Details = ({ classes, errors }: Props) => (
const DetailsForm = withStyles(styles)(Details)
const DetailsPage = () => (controls: React$Node, { errors }: Object) => (
const DetailsPage = () => (controls: React.Node, { errors }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} container={605}>
<DetailsForm errors={errors} />

View File

@ -66,13 +66,6 @@ const styles = () => ({
address: {
paddingLeft: '6px',
},
open: {
paddingLeft: sm,
width: 'auto',
'&:hover': {
cursor: 'pointer',
},
},
})
type LayoutProps = {
@ -104,8 +97,10 @@ const checkUserAddressOwner = (values: Object, userAddress: string): boolean =>
class ReviewComponent extends React.PureComponent<Props, State> {
render() {
const { values, classes, network, userAddress } = this.props
const {
values, classes, network, userAddress,
} = this.props
const isOwner = checkUserAddressOwner(values, userAddress)
const owners = getAccountsFrom(values)
const safeAddress = values[FIELD_LOAD_ADDRESS]
@ -201,7 +196,7 @@ class ReviewComponent extends React.PureComponent<Props, State> {
const ReviewPage = withStyles(styles)(ReviewComponent)
const Review = ({ network, userAddress }: LayoutProps) => (controls: React$Node, { values }: Object) => (
const Review = ({ network, userAddress }: LayoutProps) => (controls: React.Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<ReviewPage network={network} values={values} userAddress={userAddress} />

View File

@ -155,7 +155,7 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
const ReviewPage = withStyles(styles)(ReviewComponent)
const Review = ({ network }: LayoutProps) => (controls: React$Node, { values }: Object) => (
const Review = ({ network }: LayoutProps) => (controls: React.Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<ReviewPage network={network} values={values} />

View File

@ -83,7 +83,7 @@ const SafeName = ({ classes }: Props) => (
const SafeNameForm = withStyles(styles)(SafeName)
const SafeNamePage = () => (controls: React$Node) => (
const SafeNamePage = () => (controls: React.Node) => (
<OpenPaper controls={controls} container={600}>
<SafeNameForm />
</OpenPaper>

View File

@ -194,7 +194,7 @@ class SafeOwners extends React.Component<Props, State> {
const SafeOwnersForm = withStyles(styles)(SafeOwners)
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React$Node, { values, errors }: Object) => (
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<SafeOwnersForm

View File

@ -80,7 +80,7 @@ const SafeThreshold = ({ classes, values }: Props) => {
const SafeThresholdForm = withStyles(styles)(SafeThreshold)
const SafeOwnersPage = () => (controls: React$Node, { values }: Object) => (
const SafeOwnersPage = () => (controls: React.Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} container={450}>
<SafeThresholdForm values={values} />

View File

@ -6,7 +6,6 @@ export const SAFELIST_ADDRESS = '/safes'
export const OPEN_ADDRESS = '/open'
export const LOAD_ADDRESS = '/load'
export const WELCOME_ADDRESS = '/welcome'
export const SETTINS_ADDRESS = '/settings'
export const OPENING_ADDRESS = '/opening'
export const stillInOpeningView = () => {

View File

@ -32,7 +32,7 @@ type Props = {
addresses: string[],
}
const AddOwnerForm = ({ addresses, numOwners, threshold }: Props) => (controls: React$Node) => (
const AddOwnerForm = ({ addresses, numOwners, threshold }: Props) => (controls: React.Node) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
Add Owner

View File

@ -17,7 +17,7 @@ const spinnerStyle = {
minHeight: '50px',
}
const Review = () => (controls: React$Node, { values, submitting }: FormProps) => {
const Review = () => (controls: React.Node, { values, submitting }: FormProps) => {
const text = values[INCREASE_PARAM]
? 'This operation will increase the threshold of the safe'
: 'This operation will not modify the threshold of the safe'
@ -26,10 +26,14 @@ const Review = () => (controls: React$Node, { values, submitting }: FormProps) =
<OpenPaper controls={controls}>
<Heading tag="h2">Review the Add Owner operation</Heading>
<Paragraph align="left">
<Bold>Owner Name: </Bold> {values[NAME_PARAM]}
<Bold>Owner Name: </Bold>
{' '}
{values[NAME_PARAM]}
</Paragraph>
<Paragraph align="left">
<Bold>Owner Address: </Bold> {values[OWNER_ADDRESS_PARAM]}
<Bold>Owner Address: </Bold>
{' '}
{values[OWNER_ADDRESS_PARAM]}
</Paragraph>
<Paragraph align="left">
<Bold>{text}</Bold>

View File

@ -2,7 +2,7 @@
import React from 'react'
import OpenInNew from '@material-ui/icons/OpenInNew'
import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar/Context'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton'
import Paragraph from '~/components/layout/Paragraph'
@ -124,6 +124,7 @@ const ReviewTx = ({
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
>
SUBMIT
</Button>

View File

@ -161,7 +161,14 @@ const SendFunds = ({
<Button className={classes.button} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button type="submit" className={classes.button} variant="contained" minWidth={140} color="primary">
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={140}
color="primary"
data-testid="review-tx-btn"
>
Review
</Button>
</Row>

View File

@ -14,6 +14,8 @@ import { type Token } from '~/logic/tokens/store/model/token'
import actions, { type Actions } from './actions'
import { styles } from './style'
export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn'
type Props = Actions & {
onClose: () => void,
classes: Object,
@ -43,7 +45,7 @@ const Tokens = (props: Props) => {
<Paragraph className={classes.manage} noMargin>
Manage Tokens
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<IconButton onClick={onClose} disableRipple data-testid={MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID}>
<Close className={classes.close} />
</IconButton>
</Row>

View File

@ -23,6 +23,11 @@ import { addressIsTokenContract, doesntExistInTokenList } from './validators'
import { styles } from './style'
import { getSymbolAndDecimalsFromContract } from './utils'
export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input'
export const ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID = 'add-custom-token-symbols-input'
export const ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID = 'add-custom-token-decimals-input'
export const ADD_CUSTOM_TOKEN_FORM = 'add-custom-token-form'
type Props = {
classes: Object,
addToken: Function,
@ -115,7 +120,7 @@ const AddCustomToken = (props: Props) => {
return (
<React.Fragment>
<GnoForm onSubmit={handleSubmit} initialValues={formValues}>
<GnoForm onSubmit={handleSubmit} initialValues={formValues} testId={ADD_CUSTOM_TOKEN_FORM}>
{() => (
<React.Fragment>
<Block className={classes.formContainer}>
@ -135,6 +140,7 @@ const AddCustomToken = (props: Props) => {
placeholder="Token contract address*"
text="Token contract address*"
className={classes.addressInput}
testId={ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID}
/>
<FormSpy
subscription={{
@ -156,6 +162,7 @@ const AddCustomToken = (props: Props) => {
placeholder="Token symbol*"
text="Token symbol"
className={classes.addressInput}
testId={ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID}
/>
<Field
name="decimals"
@ -165,6 +172,7 @@ const AddCustomToken = (props: Props) => {
placeholder="Token decimals*"
text="Token decimals*"
className={classes.addressInput}
testId={ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID}
/>
<Block align="left">
<Field name="showForAllSafes" component={Checkbox} type="checkbox" className={classes.checkbox} />

View File

@ -24,6 +24,9 @@ import { type Token } from '~/logic/tokens/store/model/token'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { styles } from './style'
export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn'
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
type Props = {
classes: Object,
tokens: List<Token>,
@ -141,6 +144,7 @@ class Tokens extends React.Component<Props, State> {
color="secondary"
className={classes.add}
onClick={switchToAddCustomTokenScreen}
testId={ADD_CUSTOM_TOKEN_BUTTON_TEST_ID}
>
+ ADD CUSTOM TOKEN
</Button>
@ -164,7 +168,11 @@ class Tokens extends React.Component<Props, State> {
<ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== ETH_ADDRESS && (
<ListItemSecondaryAction>
<Switch onChange={this.onSwitch(token)} checked={isActive} />
<Switch
onChange={this.onSwitch(token)}
checked={isActive}
inputProps={{ 'data-testid': `${token.symbol}_${TOGGLE_TOKEN_TEST_ID}` }}
/>
</ListItemSecondaryAction>
)}
</ListItem>

View File

@ -18,6 +18,7 @@ export type BalanceRow = SortRow<BalanceData>
export const getBalanceData = (activeTokens: List<Token>): List<BalanceRow> => {
const rows = activeTokens.map((token: Token) => ({
[BALANCE_TABLE_ASSET_ID]: { name: token.name, logoUri: token.logoUri },
[buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name,
[BALANCE_TABLE_BALANCE_ID]: `${token.balance} ${token.symbol}`,
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance),
[FIXED]: token.get('symbol') === 'ETH',
@ -27,16 +28,16 @@ export const getBalanceData = (activeTokens: List<Token>): List<BalanceRow> => {
}
export const generateColumns = () => {
const assetRow: Column = {
const assetColumn: Column = {
id: BALANCE_TABLE_ASSET_ID,
order: false,
order: true,
disablePadding: false,
label: 'Asset',
custom: false,
width: 250,
}
const balanceRow: Column = {
const balanceColumn: Column = {
id: BALANCE_TABLE_BALANCE_ID,
align: 'right',
order: true,
@ -53,7 +54,7 @@ export const generateColumns = () => {
custom: true,
}
return List([assetRow, balanceRow, actions])
return List([assetColumn, balanceColumn, actions])
}
export const filterByZero = (data: List<BalanceRow>, hideZero: boolean): List<BalanceRow> => data.filter((row: BalanceRow) => (hideZero ? row[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)] !== 0 : true))

View File

@ -12,6 +12,7 @@ import TableCell from '@material-ui/core/TableCell'
import { withStyles } from '@material-ui/core/styles'
import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
import ButtonLink from '~/components/layout/ButtonLink'
import Paragraph from '~/components/layout/Paragraph'
import Modal from '~/components/Modal'
import { type Column, cellWidth } from '~/components/Table/TableHead'
@ -25,6 +26,9 @@ import SendModal from './SendModal'
import Receive from './Receive'
import { styles } from './style'
export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn'
export const BALANCE_ROW_TEST_ID = 'balance-row'
type State = {
hideZero: boolean,
showToken: boolean,
@ -127,9 +131,7 @@ class Balances extends React.Component<Props, State> {
<Paragraph className={classes.zero}>Hide zero balances</Paragraph>
</Col>
<Col xs={6} end="sm">
<Paragraph noMargin size="md" color="secondary" className={classes.links} onClick={this.onShow('Token')}>
Manage Tokens
</Paragraph>
<ButtonLink onClick={this.onShow('Token')} testId="manage-tokens-btn">Manage Tokens</ButtonLink>
<Modal
title="Manage Tokens"
description="Enable and disable tokens to be listed"
@ -154,7 +156,7 @@ class Balances extends React.Component<Props, State> {
defaultFixed
>
{(sortedData: Array<BalanceRow>) => sortedData.map((row: any, index: number) => (
<TableRow tabIndex={-1} key={index} className={classes.hide}>
<TableRow tabIndex={-1} key={index} className={classes.hide} data-testid={BALANCE_ROW_TEST_ID}>
{autoColumns.map((column: Column) => (
<TableCell key={column.id} style={cellWidth(column.width)} align={column.align} component="td">
{column.id === BALANCE_TABLE_ASSET_ID ? <AssetTableCell asset={row[column.id]} /> : row[column.id]}
@ -169,6 +171,7 @@ class Balances extends React.Component<Props, State> {
color="secondary"
className={classes.send}
onClick={() => this.showSendFunds(row.asset.name)}
data-testid="balance-send-btn"
>
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
Send

View File

@ -25,6 +25,9 @@ export const styles = (theme: Object) => ({
'&:hover $actions': {
visibility: 'initial',
},
'&:focus $actions': {
visibility: 'initial',
},
},
actions: {
justifyContent: 'flex-end',

View File

@ -20,7 +20,7 @@ type Props = {
const RemoveOwnerForm = ({
numOwners, threshold, name, disabled, pendingTransactions,
}: Props) => (
controls: React$Node,
controls: React.Node,
) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">

View File

@ -21,7 +21,7 @@ const spinnerStyle = {
minHeight: '50px',
}
const Review = ({ name }: Props) => (controls: React$Node, { values, submitting }: FormProps) => {
const Review = ({ name }: Props) => (controls: React.Node, { values, submitting }: FormProps) => {
const text = values[DECREASE_PARAM]
? 'This operation will decrease the threshold of the safe'
: 'This operation will not modify the threshold of the safe'
@ -30,7 +30,9 @@ const Review = ({ name }: Props) => (controls: React$Node, { values, submitting
<OpenPaper controls={controls}>
<Heading tag="h2">Review the Remove Owner operation</Heading>
<Paragraph align="left">
<Bold>Owner Name: </Bold> {name}
<Bold>Owner Name: </Bold>
{' '}
{name}
</Paragraph>
<Paragraph align="left">
<Bold>{text}</Bold>

View File

@ -1,9 +1,7 @@
// @flow
import * as React from 'react'
import classNames from 'classnames'
import Link from '~/components/layout/Link'
import AccountBalance from '@material-ui/icons/AccountBalance'
import Settings from '@material-ui/icons/Settings'
import Avatar from '@material-ui/core/Avatar'
import Collapse from '@material-ui/core/Collapse'
import IconButton from '@material-ui/core/IconButton'
@ -20,10 +18,8 @@ import Button from '~/components/layout/Button'
import openHoc, { type Open } from '~/components/hoc/OpenHoc'
import { type WithStyles } from '~/theme/mui'
import { type Token } from '~/logic/tokens/store/model/token'
import { settingsUrlFrom } from '~/routes'
type Props = Open & WithStyles & {
safeAddress: string,
tokens: Map<string, Token>,
onMoveFunds: (token: Token) => void,
}
@ -37,10 +33,9 @@ const styles = {
export const MOVE_FUNDS_BUTTON_TEXT = 'Move'
const BalanceComponent = openHoc(({
open, toggle, tokens, classes, onMoveFunds, safeAddress,
open, toggle, tokens, classes, onMoveFunds,
}: Props) => {
const hasBalances = tokens.count() > 0
const settingsUrl = settingsUrlFrom(safeAddress)
return (
<React.Fragment>
@ -49,11 +44,6 @@ const BalanceComponent = openHoc(({
<AccountBalance />
</Avatar>
<ListItemText primary="Balance" secondary="List of different token balances" />
<ListItemIcon>
<IconButton to={settingsUrl} disabled={!hasBalances} component={Link} className={classes.button}>
<Settings />
</IconButton>
</ListItemIcon>
<ListItemIcon>
{open
? <IconButton disableRipple><ExpandLess /></IconButton>

View File

@ -32,7 +32,7 @@ type SafeProps = {
}
type State = {
component?: React$Node,
component?: React.Node,
}
const listStyle = {

View File

@ -21,7 +21,7 @@ const spinnerStyle = {
minHeight: '50px',
}
const ReviewTx = ({ symbol }: Props) => (controls: React$Node, { values, submitting }: FormProps) => (
const ReviewTx = ({ symbol }: Props) => (controls: React.Node, { values, submitting }: FormProps) => (
<OpenPaper controls={controls}>
<Heading tag="h2">Review the move token funds</Heading>
<Paragraph align="left">

View File

@ -2,7 +2,9 @@
import * as React from 'react'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { composeValidators, inLimit, mustBeFloat, required, greaterThan, mustBeEthereumAddress } from '~/components/forms/validator'
import {
composeValidators, inLimit, mustBeFloat, required, greaterThan, mustBeEthereumAddress,
} from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Heading from '~/components/layout/Heading'
@ -17,7 +19,7 @@ type Props = {
symbol: string,
}
const SendTokenForm = ({ funds, symbol }: Props) => (controls: React$Node) => (
const SendTokenForm = ({ funds, symbol }: Props) => (controls: React.Node) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
Send tokens Transaction

View File

@ -17,11 +17,13 @@ const spinnerStyle = {
minHeight: '50px',
}
const Review = () => (controls: React$Node, { values, submitting }: FormProps) => (
const Review = () => (controls: React.Node, { values, submitting }: FormProps) => (
<OpenPaper controls={controls}>
<Heading tag="h2">Review the Threshold operation</Heading>
<Paragraph align="left">
<Bold>The new threshold will be: </Bold> {values[THRESHOLD_PARAM]}
<Bold>The new threshold will be: </Bold>
{' '}
{values[THRESHOLD_PARAM]}
</Paragraph>
<Block style={spinnerStyle}>
{ submitting && <CircularProgress size={50} /> }

View File

@ -5,7 +5,9 @@ import Heading from '~/components/layout/Heading'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator'
import {
composeValidators, minValue, maxValue, mustBeInteger, required,
} from '~/components/forms/validator'
import { type Safe } from '~/routes/safe/store/models/safe'
export const THRESHOLD_PARAM = 'threshold'
@ -15,7 +17,7 @@ type ThresholdProps = {
safe: Safe,
}
const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => (controls: React$Node) => (
const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => (controls: React.Node) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
{'Change safe\'s threshold'}

View File

@ -50,11 +50,6 @@ export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = crea
},
)
type UserToken = {
address: string,
balance: string,
}
const safeEthAsTokenSelector: Selector<GlobalState, RouterProps, ?Token> = createSelector(
safeSelector,
(safe: Safe) => {

View File

@ -1,54 +0,0 @@
// @flow
/*
import addBalances from '~/routes/safe/store/actions/addBalances'
import { aNewStore } from '~/store'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { balanceSelector } from '../selectors'
const balanceSelectorTests = () => {
describe('Safe Selector[balanceSelector]', () => {
it('should return 0 when safe address is not found', () => {
// GIVEN
const safeAddress = 'foo'
const match = buildMathPropsFrom(safeAddress)
const store = aNewStore()
// WHEN
const balance = balanceSelector(store.getState(), { match })
// THEN
expect(balance).toBe('0')
})
it('should return 0 when safe has no funds', async () => {
// GIVEN
const safeAddress = 'foo'
const match = buildMathPropsFrom(safeAddress)
const store = aNewStore()
// WHEN
await store.dispatch(addBalances('bar', '1'))
const balance = balanceSelector(store.getState(), { match })
// THEN
expect(balance).toBe('0')
})
it('should return safe funds', async () => {
// GIVEN
const safeAddress = 'foo'
const match = buildMathPropsFrom(safeAddress)
const store = aNewStore()
// WHEN
await store.dispatch(addBalances(safeAddress, '1.3456'))
const balance = balanceSelector(store.getState(), { match })
// THEN
expect(balance).toBe('1.3456')
})
})
}
export default balanceSelectorTests
*/

View File

@ -1,59 +0,0 @@
// @flow
import SafeRecord, { type Safe } from '~/routes/safe/store/models/safe'
import { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
class SafeBuilder {
safe: Safe
constructor() {
this.safe = SafeRecord()
}
withAddress(address: string) {
this.safe = this.safe.set('address', address)
return this
}
withName(name: string) {
this.safe = this.safe.set('name', name)
return this
}
withConfirmations(confirmations: number) {
this.safe = this.safe.set('threshold', confirmations)
return this
}
withOwner(names: string[], adresses: string[]) {
const owners = buildOwnersFrom(names, adresses)
this.safe = this.safe.set('owners', owners)
return this
}
get() {
return this.safe
}
}
const aSafe = () => new SafeBuilder()
export class SafeFactory {
static oneOwnerSafe = (ownerAddress: string = '0x03db1a8b26d08df23337e9276a36b474510f0023') => aSafe()
.withAddress('0x03db1a8b26d08df23337e9276a36b474510f0025')
.withName('Adol ICO Safe')
.withConfirmations(1)
.withOwner(['Adol Metamask'], [ownerAddress])
.get()
static twoOwnersSafe = (
firstOwner: string = '0x03db1a8b26d08df23337e9276a36b474510f0023',
secondOwner: string = '0x03db1a8b26d08df23337e9276a36b474510f0024',
) => aSafe()
.withAddress('0x03db1a8b26d08df23337e9276a36b474510f0026')
.withName('Adol & Tobias Safe')
.withConfirmations(2)
.withOwner(['Adol Metamask', 'Tobias Metamask'], [firstOwner, secondOwner])
.get()
}
export default aSafe

View File

@ -1,102 +0,0 @@
// @flow
/*
import { List, Map } from 'immutable'
import { makeTransaction, type Transaction } from '~/routes/safe/store/model/transaction'
import { type Confirmation, makeConfirmation } from '~/routes/safe/store/model/confirmation'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { confirmationsTransactionSelector } from '~/routes/safe/store/selectors/index'
import { makeProvider } from '~/wallets/store/model/provider'
const grantedSelectorTests = () => {
describe('Safe Selector[confirmationsTransactionSelector]', () => {
it('returns 1 confirmation if safe has only one owner when tx is created', () => {
// GIVEN
const firstConfirmation: Confirmation = makeConfirmation({
owner: makeOwner(),
status: true,
hash: 'asdf',
})
const transaction: Transaction = makeTransaction({
name: 'Buy batteries',
nonce: 1,
value: 2,
confirmations: List([firstConfirmation]),
destination: 'destAddress',
threshold: 2,
tx: '',
})
const reduxStore = {
safes: Map(),
providers: makeProvider(),
tokens: Map(),
transactions: Map(),
}
// WHEN
const threshold = confirmationsTransactionSelector(reduxStore, { transaction })
// THEN
expect(threshold).toBe(1)
})
it('returns 1 confirmation if safe has two or more owners when multisig tx is created', () => {
// GIVEN
const firstConfirmation: Confirmation = makeConfirmation({
owner: makeOwner(),
status: true,
hash: 'asdf',
})
const secondConfirmation: Confirmation = makeConfirmation({
owner: makeOwner(),
status: false,
hash: '',
})
const transaction: Transaction = makeTransaction({
name: 'Buy batteries',
nonce: 1,
value: 2,
confirmations: List([firstConfirmation, secondConfirmation]),
destination: 'destAddress',
threshold: 2,
tx: '',
})
const reduxStore = {
safes: Map(),
providers: makeProvider(),
tokens: Map(),
transactions: Map(),
}
// WHEN
const threshold = confirmationsTransactionSelector(reduxStore, { transaction })
// THEN
expect(threshold).toBe(1)
})
it('should return 0 confirmations if not transaction is sent as prop to component', () => {
const reduxStore = {
safes: Map(),
providers: makeProvider(),
tokens: Map(),
transactions: Map(),
}
// WHEN
// $FlowFixMe
const threshold = confirmationsTransactionSelector(reduxStore, { transaction: undefined })
// THEN
expect(threshold).toBe(0)
})
})
}
export default grantedSelectorTests
*/

View File

@ -1,84 +0,0 @@
// @flow
import { Map } from 'immutable'
import { type Match } from 'react-router-dom'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { getProviderInfo } from '~/logic/wallets/getWeb3'
import { grantedSelector } from '~/routes/safe/container/selector'
import { makeProvider } from '~/logic/wallets/store/model/provider'
const grantedSelectorTests = () => {
let provider
beforeEach(async () => {
provider = await getProviderInfo()
})
describe('Safe Selector[grantedSelector]', () => {
it('should be granted to operate a safe when the user is owner', () => {
// GIVEN
let map: Map<string, Safe> = Map()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe(provider.account))
const match: Match = buildMathPropsFrom('fooAddress')
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: makeProvider(provider),
tokens: undefined,
transactions: undefined,
}
// WHEN
const granted = grantedSelector(reduxStore, { match })
// THEN
expect(granted).toBe(true)
})
it('should be granted to operate a safe when the user is owner in case-insensitive', () => {
// GIVEN
let map: Map<string, Safe> = Map()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe(provider.account.toUpperCase()))
const match: Match = buildMathPropsFrom('fooAddress')
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: makeProvider(provider),
tokens: undefined,
transactions: undefined,
}
// WHEN
const granted = grantedSelector(reduxStore, { match })
// THEN
expect(granted).toBe(true)
})
it('should NOT be granted to operate with a Safe when the user is NOT owner', () => {
// GIVEN
let map: Map<string, Safe> = Map()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe('inventedOwner'))
const match: Match = buildMathPropsFrom('fooAddress')
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: makeProvider(provider),
tokens: undefined,
transactions: undefined,
}
// WHEN
const granted = grantedSelector(reduxStore, { match })
// THEN
expect(granted).toBe(false)
})
})
}
export default grantedSelectorTests

View File

@ -1,60 +0,0 @@
// @flow
import {
combineReducers, createStore, applyMiddleware, compose,
} from 'redux'
import thunk from 'redux-thunk'
import safeReducer, { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import addSafe from '~/routes/safe/store/actions/addSafe'
import * as SafeFields from '~/routes/open/components/fields'
import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor'
import { SafeFactory } from './builder/safe.builder'
const aStore = (initState) => {
const reducers = combineReducers({
[SAFE_REDUCER_ID]: safeReducer,
})
const middlewares = [thunk]
const enhancers = [applyMiddleware(...middlewares)]
return createStore(reducers, initState, compose(...enhancers))
}
const providerReducerTests = () => {
describe('Safe Actions[addSafe]', () => {
let store
let address
let formValues
beforeEach(() => {
store = aStore()
address = '0x03db1a8b26d08df23337e9276a36b474510f0025'
formValues = {
[SafeFields.FIELD_NAME]: 'Adol ICO Safe',
[SafeFields.FIELD_CONFIRMATIONS]: 1,
[SafeFields.FIELD_OWNERS]: 1,
[SafeFields.getOwnerAddressBy(0)]: '0x03db1a8b26d08df23337e9276a36b474510f0023',
[SafeFields.getOwnerNameBy(0)]: 'Adol Metamask',
address,
}
})
it('reducer should return SafeRecord from form values', () => {
// GIVEN in beforeEach method
// WHEN
store.dispatch(
addSafe(
formValues[SafeFields.FIELD_NAME],
formValues.address,
formValues[SafeFields.FIELD_CONFIRMATIONS],
getNamesFrom(formValues),
getAccountsFrom(formValues),
),
)
const safes = store.getState()[SAFE_REDUCER_ID]
// THEN
expect(safes.get(address)).toEqual(SafeFactory.oneOwnerSafe())
})
})
}
export default providerReducerTests

View File

@ -1,56 +0,0 @@
// @flow
import { Map } from 'immutable'
import { type Match } from 'react-router-dom'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { safeSelector } from '../selectors'
const safeSelectorTests = () => {
describe('Safe Selector[safeSelector]', () => {
it('should return empty list when no safes', () => {
// GIVEN
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: undefined,
tokens: undefined,
transactions: undefined,
}
const match: Match = buildMathPropsFrom('fooAddress')
// WHEN
const safes = safeSelector(reduxStore, { match })
// THEN
expect(safes).toBe(undefined)
})
it('should return a list of size 2 when 2 safes are created', () => {
// GIVEN
let map: Map<string, Safe> = Map()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe())
map = map.set('barAddress', SafeFactory.twoOwnersSafe())
const match: Match = buildMathPropsFrom('fooAddress')
const undefMatch: Match = buildMathPropsFrom('inventedAddress')
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: undefined,
tokens: undefined,
transactions: undefined,
}
// WHEN
const oneOwnerSafe = safeSelector(reduxStore, { match })
const undefinedSafe = safeSelector(reduxStore, { match: undefMatch })
// THEN
expect(oneOwnerSafe).toEqual(SafeFactory.oneOwnerSafe())
expect(undefinedSafe).toBe(undefined)
})
})
}
export default safeSelectorTests

View File

@ -1,27 +0,0 @@
// @flow
import safeReducerTests from './safe.reducer'
// import balanceSelectorTests from './balance.selector'
import safeSelectorTests from './safe.selector'
import grantedSelectorTests from './granted.selector'
// import confirmationsSelectorTests from './confirmations.selector'
// import transactionsSelectorTests from './transactions.selector'
describe('Safe Test suite', () => {
// ACTIONS AND REDUCERS
safeReducerTests()
// SAFE SELECTOR
safeSelectorTests()
// BALANCE SELECTOR
// balanceSelectorTests()
// GRANTED SELECTOR
grantedSelectorTests()
// CONFIRMATIONS SELECTOR
// confirmationsSelectorTests()
// TRANSACTIONS SELECTOR
// transactionsSelectorTests()
})

View File

@ -1,131 +0,0 @@
// @flow
/*
import { List, Map } from 'immutable'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
import { makeProvider } from '~/wallets/store/model/provider'
import { makeConfirmation, type Confirmation } from '~/routes/safe/store/model/confirmation'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { makeTransaction, type Transaction } from '~/routes/safe/store/model/transaction'
const grantedSelectorTests = () => {
describe('Safe Selector[safeTransactionsSelector]', () => {
it('should return empty list if no transactions in store', () => {
// GIVEN
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
tokens: undefined,
transactions: Map(),
}
// WHEN
const transactions = safeTransactionsSelector(reduxStore, { safeAddress: 'fooAddress' })
// THEN
expect(transactions).toEqual(List([]))
})
it('should return empty list if transactions in store but not safe address in props', () => {
// GIVEN
const firstConfirmation: Confirmation = makeConfirmation({
owner: makeOwner(),
status: true,
hash: 'asdf',
})
const transaction: Transaction = makeTransaction({
name: 'Buy batteries',
nonce: 1,
value: 2,
confirmations: List([firstConfirmation]),
destination: 'destAddress',
threshold: 2,
tx: '',
})
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
tokens: undefined,
transactions: Map({ fooAddress: List([transaction]) }),
}
// WHEN
const transactionsEmpty = safeTransactionsSelector(reduxStore, { safeAddress: '' })
// $FlowFixMe
const transactionsUndefined = safeTransactionsSelector(reduxStore, { safeAddress: undefined })
// THEN
expect(transactionsEmpty).toEqual(List([]))
expect(transactionsUndefined).toEqual(List([]))
})
it('should return empty list if there are transactions belonging to different address', () => {
// GIVEN
const firstConfirmation: Confirmation = makeConfirmation({
owner: makeOwner(),
status: true,
hash: 'asdf',
})
const transaction: Transaction = makeTransaction({
name: 'Buy batteries',
nonce: 1,
value: 2,
confirmations: List([firstConfirmation]),
destination: 'destAddress',
threshold: 2,
tx: '',
})
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
tokens: undefined,
transactions: Map({ fooAddress: List([transaction]) }),
}
// WHEN
const transactions = safeTransactionsSelector(reduxStore, { safeAddress: 'invented' })
// THEN
expect(transactions).toEqual(List([]))
})
it('should return transactions of safe', () => {
// GIVEN
const firstConfirmation: Confirmation = makeConfirmation({
owner: makeOwner(),
status: true,
hash: 'asdf',
})
const transaction: Transaction = makeTransaction({
name: 'Buy batteries',
nonce: 1,
value: 2,
confirmations: List([firstConfirmation]),
destination: 'destAddress',
threshold: 2,
tx: '',
})
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
tokens: undefined,
transactions: Map({ fooAddress: List([transaction]) }),
}
// WHEN
const transactions = safeTransactionsSelector(reduxStore, { safeAddress: 'fooAddress' })
// THEN
expect(transactions).toEqual(List([transaction]))
})
})
}
export default grantedSelectorTests
*/

View File

@ -3,7 +3,6 @@ import { storiesOf } from '@storybook/react'
import { List } from 'immutable'
import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import Component from './Layout'
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
@ -12,7 +11,3 @@ storiesOf('Routes /safes', module)
.addDecorator(FrameDecorator)
.add('Safe List whithout safes and connected', () => <Component provider="METAMASK" safes={List([])} />)
.add('Safe List whithout safes and NOT connected', () => <Component provider="" safes={List([])} />)
.add('Safe List whith 2 safes', () => {
const safes = List([SafeFactory.oneOwnerSafe(), SafeFactory.twoOwnersSafe()])
return <Component provider="METAMASK" safes={safes} />
})

View File

@ -1,7 +0,0 @@
// @flow
import safesSelectorTests from './safes.selector'
describe('SafeList Test suite', () => {
// safesSelector SELECTOR
safesSelectorTests()
})

View File

@ -1,122 +0,0 @@
// @flow
import { List, Map } from 'immutable'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getProviderInfo } from '~/logic/wallets/getWeb3'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider'
import { makeProvider, type Provider } from '~/logic/wallets/store/model/provider'
import { safesByOwnerSelector } from '../selectors'
const safesListSelectorTests = () => {
let walletRecord: Provider
beforeEach(async () => {
const provider = await getProviderInfo()
walletRecord = makeProvider(provider)
})
describe('Safes Selector[safesByOwnerSelector]', () => {
it('should return empty list when no safes', () => {
// GIVEN
const reduxStore = {
[PROVIDER_REDUCER_ID]: walletRecord,
[SAFE_REDUCER_ID]: Map(),
tokens: undefined,
transactions: undefined,
}
const emptyList = List([])
// WHEN
const safes = safesByOwnerSelector(reduxStore, {})
// THEN
expect(safes).toEqual(emptyList)
})
it('should return a list of size 0 when 0 of 2 safes contains the user as owner', () => {
// GIVEN
let map: Map<string, Safe> = Map()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe('foo'))
map = map.set('barAddress', SafeFactory.twoOwnersSafe('foo', 'bar'))
const reduxStore = {
[PROVIDER_REDUCER_ID]: walletRecord,
[SAFE_REDUCER_ID]: map,
tokens: undefined,
transactions: undefined,
}
// WHEN
const safes = safesByOwnerSelector(reduxStore, {})
// THEN
expect(safes.count()).toEqual(0)
})
it('should return a list of size 1 when 1 of 2 safes contains the user as owner', () => {
// GIVEN
let map: Map<string, Safe> = Map()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe(walletRecord.account))
map = map.set('barAddress', SafeFactory.twoOwnersSafe('foo', 'bar'))
const reduxStore = {
[PROVIDER_REDUCER_ID]: walletRecord,
[SAFE_REDUCER_ID]: map,
tokens: undefined,
transactions: undefined,
}
// WHEN
const safes = safesByOwnerSelector(reduxStore, {})
// THEN
expect(safes.count()).toEqual(1)
})
it('should return a list of size 2 when 2 of 2 safes contains the user as owner', () => {
// GIVEN
let map: Map<string, Safe> = Map()
const userAccount = walletRecord.account
map = map.set('fooAddress', SafeFactory.oneOwnerSafe(userAccount))
map = map.set('barAddress', SafeFactory.twoOwnersSafe('foo', userAccount))
const reduxStore = {
[SAFE_REDUCER_ID]: map,
[PROVIDER_REDUCER_ID]: walletRecord,
tokens: undefined,
transactions: undefined,
}
// WHEN
const safes = safesByOwnerSelector(reduxStore, {})
// THEN
expect(safes.count()).toEqual(2)
expect(safes.get(0)).not.toEqual(safes.get(1))
})
it('should return safes under owners case-insensitive', () => {
// GIVEN
let map: Map<string, Safe> = Map()
const userAccountUpper = walletRecord.account.toUpperCase()
map = map.set('fooAddress', SafeFactory.oneOwnerSafe(userAccountUpper))
map = map.set('barAddress', SafeFactory.twoOwnersSafe('foo', userAccountUpper))
const reduxStore = {
[SAFE_REDUCER_ID]: map,
[PROVIDER_REDUCER_ID]: walletRecord,
tokens: undefined,
transactions: undefined,
}
// WHEN
const safes = safesByOwnerSelector(reduxStore, {})
// THEN
expect(safes.count()).toEqual(2)
expect(safes.get(0)).not.toEqual(safes.get(1))
})
})
}
export default safesListSelectorTests

View File

@ -1,27 +1,19 @@
// @flow
import { type Store } from 'redux'
import TestUtils from 'react-dom/test-utils'
import SafeView from '~/routes/safe/components/Safe'
import { aNewStore, type GlobalState } from '~/store'
import { sleep } from '~/utils/timer'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { addEtherTo } from '~/test/utils/tokenMovements'
import { sendEtherTo } from '~/test/utils/tokenMovements'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToSafe } from '~/test/builder/safe.dom.utils'
import { MOVE_FUNDS_BUTTON_TEXT } from '~/routes/safe/components/Safe/BalanceInfo'
import { renderSafeView } from '~/test/builder/safe.dom.utils'
export type DomSafe = {
address: string,
safeButtons: Element[],
safe: React$Component<any, any>,
accounts: string[],
store: Store<GlobalState>,
SafeDom: any,
}
export const filterMoveButtonsFrom = (buttons: Element[]) => buttons.filter(
(button: Element): boolean => button.getElementsByTagName('span')[0].textContent !== MOVE_FUNDS_BUTTON_TEXT,
)
export const renderSafeInDom = async (owners: number = 1, threshold: number = 1): Promise<DomSafe> => {
// create store
const store = aNewStore()
@ -30,23 +22,16 @@ export const renderSafeInDom = async (owners: number = 1, threshold: number = 1)
// have available accounts
const accounts = await getWeb3().eth.getAccounts()
// navigate to SAFE route
const SafeDom = travelToSafe(store, address)
const SafeDom = renderSafeView(store, address)
// add funds to safe
await addEtherTo(address, '0.1')
await sendEtherTo(address, '0.1')
// wait until funds are displayed and buttons are enabled
await sleep(3000)
// $FlowFixMe
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
// $FlowFixMe
const buttons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button')
const filteredButtons = filterMoveButtonsFrom(buttons)
return {
address,
safeButtons: filteredButtons,
safe: SafeDom,
SafeDom,
accounts,
store,
}

View File

@ -1,15 +1,18 @@
// @flow
import * as React from 'react'
import TestUtils from 'react-dom/test-utils'
import { type Store } from 'redux'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import PageFrame from '~/components/layout/PageFrame'
import { render } from '@testing-library/react'
import ListItemText from '~/components/List/ListItemText/index'
import { SEE_MULTISIG_BUTTON_TEXT } from '~/routes/safe/components/Safe/MultisigTx'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import { sleep } from '~/utils/timer'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import { history } from '~/store'
import AppRoutes from '~/routes'
import { SAFELIST_ADDRESS, SETTINS_ADDRESS } from '~/routes/routes'
import { history, type GlobalState } from '~/store'
import { SAFELIST_ADDRESS } from '~/routes/routes'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
export const EXPAND_BALANCE_INDEX = 0
@ -30,7 +33,7 @@ export const listTxsClickingOn = async (store: Store, seeTxsButton: Element, saf
await sleep(800)
}
export const checkMinedTx = (Transaction: React$Component<any, any>, name: string) => {
export const checkMinedTx = (Transaction: React.Component<any, any>, name: string) => {
const paragraphs = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
const status = 'Already executed'
@ -46,9 +49,9 @@ export const checkMinedTx = (Transaction: React$Component<any, any>, name: strin
expect(hashParagraph).toContain(EMPTY_DATA)
}
export const getListItemsFrom = (Transaction: React$Component<any, any>) => TestUtils.scryRenderedComponentsWithType(Transaction, ListItemText)
export const getListItemsFrom = (Transaction: React.Component<any, any>) => TestUtils.scryRenderedComponentsWithType(Transaction, ListItemText)
export const expand = async (Transaction: React$Component<any, any>) => {
export const expand = async (Transaction: React.Component<any, any>) => {
const listItems = getListItemsFrom(Transaction)
if (listItems.length > 4) {
return
@ -65,7 +68,7 @@ export const expand = async (Transaction: React$Component<any, any>) => {
}
export const checkPendingTx = async (
Transaction: React$Component<any, any>,
Transaction: React.Component<any, any>,
safeThreshold: number,
name: string,
statusses: string[],
@ -92,25 +95,28 @@ export const refreshTransactions = async (store: Store<GlobalState>, safeAddress
await sleep(1500)
}
const createDom = (store: Store): React$Component<{}> => TestUtils.renderIntoDocument(
<Provider store={store}>
<ConnectedRouter history={history}>
<AppRoutes />
</ConnectedRouter>
</Provider>,
)
const renderApp = (store: Store) => ({
...render(
<Provider store={store}>
<ConnectedRouter history={history}>
<PageFrame>
<React.Suspense fallback={<div />}>
<AppRoutes />
</React.Suspense>
</PageFrame>
</ConnectedRouter>
</Provider>,
),
history,
})
export const travelToSafe = (store: Store, address: string): React$Component<{}> => {
history.push(`${SAFELIST_ADDRESS}/${address}`)
export const renderSafeView = (store: Store<GlobalState>, address: string) => {
const app = renderApp(store)
return createDom(store)
}
export const travelToTokens = (store: Store, address: string): React$Component<{}> => {
const url = `${SAFELIST_ADDRESS}/${address}${SETTINS_ADDRESS}`
const url = `${SAFELIST_ADDRESS}/${address}`
history.push(url)
return createDom(store)
return app
}
const INTERVAL = 500

View File

@ -1,28 +1,7 @@
// @flow
import * as TestUtils from 'react-dom/test-utils'
import { travelToTokens } from '~/test/builder/safe.dom.utils'
import { sleep } from '~/utils/timer'
import { type Token } from '~/logic/tokens/store/model/token'
export const enableFirstToken = async (store: Store, safeAddress: string) => {
const TokensDom = await travelToTokens(store, safeAddress)
await sleep(400)
// WHEN
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'input')
const ethTokenInput = inputs[2]
expect(ethTokenInput.hasAttribute('disabled')).toBe(true)
const firstTokenInput = inputs[0]
expect(firstTokenInput.hasAttribute('disabled')).toBe(false)
TestUtils.Simulate.change(firstTokenInput, { target: { checked: 'true' } })
}
export const testToken = (token: Token | typeof undefined, symbol: string, status: boolean, funds?: string) => {
export const testToken = (token: Token | typeof undefined, symbol: string) => {
if (!token) throw new Error()
expect(token.get('symbol')).toBe(symbol)
expect(token.get('status')).toBe(status)
if (funds) {
expect(token.get('funds')).toBe(funds)
}
}

View File

@ -0,0 +1,17 @@
pragma solidity ^0.5.2;
import "@gnosis.pm/util-contracts/contracts/GnosisStandardToken.sol";
contract TokenOMG is GnosisStandardToken {
string public constant symbol = "OMG";
string public constant name = "Omisego Token";
uint8 public constant decimals = 18;
constructor(
uint amount
)
public
{
balances[msg.sender] = amount;
}
}

View File

@ -0,0 +1,17 @@
pragma solidity ^0.5.2;
import "@gnosis.pm/util-contracts/contracts/GnosisStandardToken.sol";
contract TokenRDN is GnosisStandardToken {
string public constant symbol = "RDN";
string public constant name = "Raiden Token";
uint8 public constant decimals = 18;
constructor(
uint amount
)
public
{
balances[msg.sender] = amount;
}
}

View File

@ -1,7 +1,7 @@
// @flow
import * as React from 'react'
import { type Store } from 'redux'
import { render, fireEvent, cleanup } from 'react-testing-library'
import { render, fireEvent, cleanup } from '@testing-library/react'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersForm'
@ -16,7 +16,7 @@ import { whenSafeDeployed } from './builder/safe.dom.utils'
afterEach(cleanup)
// https://github.com/testing-library/react-testing-library/issues/281
// https://github.com/testing-library/@testing-library/react/issues/281
const originalError = console.error
beforeAll(() => {
console.error = (...args) => {
@ -81,10 +81,12 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num
fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } })
}
fireEvent.submit(form)
await sleep(400)
await sleep(600)
// Fill Threshold
const thresholdSelect = createSafeForm.getByRole('button')
// The test is fragile here, MUI select btn is hard to find
const thresholdSelect = createSafeForm.getAllByRole('button')[1]
fireEvent.click(thresholdSelect)
const thresholdOptions = createSafeForm.getAllByRole('option')
fireEvent.click(thresholdOptions[numOwners - 1])
@ -101,7 +103,7 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num
}
const aDeployedSafe = async (specificStore: Store<GlobalState>, threshold?: number = 1, numOwners?: number = 1) => {
const safe: React$Component<{}> = await renderOpenSafeForm(specificStore)
const safe: React.Component<{}> = await renderOpenSafeForm(specificStore)
const safeAddress = await deploySafe(safe, threshold, numOwners)
return safeAddress

View File

@ -0,0 +1,122 @@
// @flow
import { fireEvent, cleanup } from '@testing-library/react'
import { List } from 'immutable'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { sendTokenTo, sendEtherTo } from '~/test/utils/tokenMovements'
import { renderSafeView } from '~/test/builder/safe.dom.utils'
import { getWeb3, getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import { dispatchAddTokenToList } from '~/test/utils/transactions/moveTokens.helper'
import { sleep } from '~/utils/timer'
import TokenBalanceRecord from '~/routes/safe/store/models/tokenBalance'
import { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances'
import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens'
import 'jest-dom/extend-expect'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
afterEach(cleanup)
describe('DOM > Feature > Funds', () => {
let store
let safeAddress: string
let accounts
beforeEach(async () => {
store = aNewStore()
// using 4th account because other accounts were used in other tests and paid gas
safeAddress = await aMinedSafe(store)
accounts = await getWeb3().eth.getAccounts()
})
it('Sends ETH with threshold = 1', async () => {
// GIVEN
const ethAmount = '5'
await sendEtherTo(safeAddress, ethAmount)
const balanceAfterSendingEthToSafe = await getBalanceInEtherOf(accounts[0])
// WHEN
const SafeDom = renderSafeView(store, safeAddress)
await sleep(1300)
// Open send funds modal
const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows[0]).toHaveTextContent(`${ethAmount} ETH`)
const sendButton = SafeDom.getByTestId('balance-send-btn')
fireEvent.click(sendButton)
// Fill first send funds screen
const recipientInput = SafeDom.getByPlaceholderText('Recipient*')
const amountInput = SafeDom.getByPlaceholderText('Amount*')
const reviewBtn = SafeDom.getByTestId('review-tx-btn')
fireEvent.change(recipientInput, { target: { value: accounts[0] } })
fireEvent.change(amountInput, { target: { value: ethAmount } })
await sleep(200)
fireEvent.click(reviewBtn)
// Submit the tx (Review Tx screen)
const submitBtn = SafeDom.getByTestId('submit-tx-btn')
fireEvent.click(submitBtn)
await sleep(1000)
// THEN
const safeFunds = await getBalanceInEtherOf(safeAddress)
expect(Number(safeFunds)).toBe(0)
const receiverFunds = await getBalanceInEtherOf(accounts[0])
const ESTIMATED_GASCOSTS = 0.3
expect(Number(parseInt(receiverFunds, 10) - parseInt(balanceAfterSendingEthToSafe, 10))).toBeGreaterThan(
parseInt(ethAmount, 10) - ESTIMATED_GASCOSTS,
)
})
it('Sends Tokens with threshold = 1', async () => {
// GIVEN
const tokensAmount = '100'
const tokenReceiver = accounts[1]
const tokenAddress = await sendTokenTo(safeAddress, tokensAmount)
await dispatchAddTokenToList(store, tokenAddress)
// WHEN
const SafeDom = await renderSafeView(store, safeAddress)
await sleep(1300)
// Activate token
const safeTokenBalance = await calculateBalanceOf(tokenAddress, safeAddress, 18)
expect(safeTokenBalance).toBe(tokensAmount)
const balanceAsRecord = TokenBalanceRecord({
address: tokenAddress,
balance: safeTokenBalance,
})
store.dispatch(updateActiveTokens(safeAddress, List([tokenAddress])))
store.dispatch(updateSafe({ address: safeAddress, balances: List([balanceAsRecord]) }))
await sleep(1000)
// Open send funds modal
const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows.length).toBe(2)
const sendButtons = SafeDom.getAllByTestId('balance-send-btn')
expect(sendButtons.length).toBe(2)
fireEvent.click(sendButtons[1])
// Fill first send funds screen
const recipientInput = SafeDom.getByPlaceholderText('Recipient*')
const amountInput = SafeDom.getByPlaceholderText('Amount*')
const reviewBtn = SafeDom.getByTestId('review-tx-btn')
fireEvent.change(recipientInput, { target: { value: tokenReceiver } })
fireEvent.change(amountInput, { target: { value: tokensAmount } })
await sleep(200)
fireEvent.click(reviewBtn)
// Submit the tx (Review Tx screen)
const submitBtn = SafeDom.getByTestId('submit-tx-btn')
fireEvent.click(submitBtn)
await sleep(1000)
// THEN
const safeFunds = await calculateBalanceOf(tokenAddress, safeAddress, 18)
expect(Number(safeFunds)).toBe(0)
const receiverFunds = await calculateBalanceOf(tokenAddress, tokenReceiver, 18)
expect(receiverFunds).toBe(tokensAmount)
})
})

View File

@ -2,7 +2,7 @@
import * as React from 'react'
import { type Store } from 'redux'
import { Provider } from 'react-redux'
import { render, fireEvent, cleanup } from 'react-testing-library'
import { render, fireEvent, cleanup } from '@testing-library/react'
import { ConnectedRouter } from 'connected-react-router'
import Load from '~/routes/load/container/Load'
import { aNewStore, history, type GlobalState } from '~/store'
@ -15,7 +15,7 @@ import { whenSafeDeployed } from './builder/safe.dom.utils'
afterEach(cleanup)
// https://github.com/testing-library/react-testing-library/issues/281
// https://github.com/testing-library/@testing-library/react/issues/281
const originalError = console.error
beforeAll(() => {
console.error = (...args) => {
@ -57,7 +57,6 @@ describe('DOM > Feature > LOAD a safe', () => {
fireEvent.change(safeNameInput, { target: { value: 'A Safe To Load' } })
fireEvent.change(safeAddressInput, { target: { value: address } })
await sleep(400)
// Click next
fireEvent.submit(form)
await sleep(400)

View File

@ -1,74 +0,0 @@
// @flow
import TestUtils from 'react-dom/test-utils'
import * as fetchBalancesAction from '~/logic/tokens/store/actions/fetchTokens'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { addTknTo, getFirstTokenContract } from '~/test/utils/tokenMovements'
import { EXPAND_BALANCE_INDEX, travelToSafe } from '~/test/builder/safe.dom.utils'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { sendMoveTokensForm, dispatchTknBalance } from '~/test/utils/transactions/moveTokens.helper'
import { sleep } from '~/utils/timer'
describe('DOM > Feature > SAFE ERC20 TOKENS', () => {
let store
let safeAddress: string
let accounts
beforeEach(async () => {
store = aNewStore()
safeAddress = await aMinedSafe(store)
accounts = await getWeb3().eth.getAccounts()
})
it('sends ERC20 tokens', async () => {
// GIVEN
const numTokens = '100'
const tokenAddress = await addTknTo(safeAddress, numTokens)
await dispatchTknBalance(store, tokenAddress, safeAddress)
// const StandardToken = await fetchBalancesAction.getStandardTokenContract()
// const myToken = await StandardToken.at(tokenAddress)
// console.log(await myToken.allowance(safeAddress, accounts[2]))
// console.log(await myToken.balanceOf(safeAddress))
// WHEN
const SafeDom = await travelToSafe(store, safeAddress)
await sleep(800)
// $FlowFixMe
const buttons = TestUtils.scryRenderedDOMComponentsWithTag(SafeDom, 'button')
const expandBalance = buttons[EXPAND_BALANCE_INDEX]
const receiver = accounts[2]
await sendMoveTokensForm(SafeDom, expandBalance, 20, accounts[2])
// THEN
const safeFunds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, safeAddress, 18)
expect(Number(safeFunds)).toBe(80)
const receiverFunds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, receiver, 18)
expect(Number(receiverFunds)).toBe(20)
const token = await getFirstTokenContract(getWeb3(), accounts[0])
const nativeSafeFunds = await token.balanceOf(safeAddress)
expect(Number(nativeSafeFunds.valueOf())).toEqual(80 * 10 ** 18)
})
it('disables send token button when balance is 0', async () => {
// GIVEN
const token = await getFirstTokenContract(getWeb3(), accounts[0])
await dispatchTknBalance(store, token.address, safeAddress)
// WHEN
const SafeDom = travelToSafe(store, safeAddress)
// $FlowFixMe
const buttons = TestUtils.scryRenderedDOMComponentsWithTag(SafeDom, 'button')
const expandBalance = buttons[EXPAND_BALANCE_INDEX]
TestUtils.Simulate.click(expandBalance)
await sleep(800)
// $FlowFixMe
const balanceButtons = TestUtils.scryRenderedDOMComponentsWithTag(SafeDom, 'button')
const tokenButton = balanceButtons[EXPAND_BALANCE_INDEX + 1] // expand button, and the next one is for sending
expect(tokenButton.hasAttribute('disabled')).toBe(true)
})
})

View File

@ -1,147 +1,17 @@
// @flow
import TestUtils from 'react-dom/test-utils'
import { List } from 'immutable'
import Transaction from '~/routes/safe/components/Transactions/Transaction'
import {
listTxsClickingOn,
LIST_TXS_INDEX,
ADD_OWNERS_INDEX,
EXPAND_OWNERS_INDEX,
EDIT_THRESHOLD_INDEX,
refreshTransactions,
EXPAND_BALANCE_INDEX,
} from '~/test/builder/safe.dom.utils'
import { renderSafeInDom, type DomSafe } from '~/test/builder/safe.dom.builder'
import {
sendMoveFundsForm,
checkMinedMoveFundsTx,
checkPendingMoveFundsTx,
} from '~/test/utils/transactions/moveFunds.helper'
import {
sendAddOwnerForm,
checkMinedAddOwnerTx,
checkPendingAddOwnerTx,
} from '~/test/utils/transactions/addOwner.helper'
import {
sendRemoveOwnerForm,
checkMinedRemoveOwnerTx,
checkPendingRemoveOwnerTx,
} from '~/test/utils/transactions/removeOwner.helper'
import {
checkMinedThresholdTx,
sendChangeThresholdForm,
checkThresholdOf,
} from '~/test/utils/transactions/threshold.helper'
import { checkBalanceOf } from '~/test/utils/tokenMovements'
import { sleep } from '~/utils/timer'
import { processTransaction } from '~/logic/safe/safeFrontendOperations'
// TBD
describe('DOM > Feature > SAFE MULTISIG Transactions', () => {
let domSafe: DomSafe
it.only('mines correctly all multisig txs in a 1 owner & 1 threshold safe', async () => {
// GIVEN one safe with 1 owner and 1 threshold
const owners = 1
const threshold = 1
domSafe = await renderSafeInDom(owners, threshold)
const {
address, safe: SafeDom, safeButtons, accounts, store,
} = domSafe
// WHEN
await sendMoveFundsForm(SafeDom, safeButtons[EXPAND_BALANCE_INDEX], '0.01', accounts[1])
await sendAddOwnerForm(SafeDom, safeButtons[ADD_OWNERS_INDEX], 'Adol Metamask 2', accounts[1])
await sleep(1200)
await sendChangeThresholdForm(SafeDom, safeButtons[EDIT_THRESHOLD_INDEX], '2')
// THEN
await listTxsClickingOn(store, safeButtons[LIST_TXS_INDEX], address)
const transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
checkMinedMoveFundsTx(transactions[0], 'Send 0.01 ETH to')
checkMinedAddOwnerTx(transactions[1], 'Add Owner Adol Metamask 2')
checkMinedThresholdTx(transactions[2], "Change Safe's threshold")
})
it.only('mines withdraw process correctly all multisig txs in a 2 owner & 2 threshold safe', async () => {
// GIVEN reusing the state from previous test
const {
address, safe: SafeDom, safeButtons, accounts, store,
} = domSafe
// WHEN
await sendMoveFundsForm(SafeDom, safeButtons[EXPAND_BALANCE_INDEX], '0.01', accounts[1])
const increaseThreshold = true
await sendAddOwnerForm(SafeDom, safeButtons[ADD_OWNERS_INDEX], 'Adol Metamask 3', accounts[2], increaseThreshold)
// THEN
await listTxsClickingOn(store, safeButtons[LIST_TXS_INDEX], address)
const transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
const statusses = ['Adol 1 Eth Account [Confirmed]']
await checkPendingMoveFundsTx(transactions[3], 2, 'Send 0.01 ETH to', statusses)
await checkPendingAddOwnerTx(transactions[4], 2, 'Add Owner Adol Metamask 3', statusses)
await checkBalanceOf(address, '0.09')
})
it.only('approves and executes pending transactions', async () => {
// GIVEN reusing the state from previous test
const {
address, safe: SafeDom, safeButtons, accounts, store,
} = domSafe
let transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
expect(transactions.length).toBe(5)
await checkThresholdOf(address, 2)
// WHEN... processing pending TXs
await processTransaction(address, transactions[3].props.transaction, 1, accounts[1], 2, List([accounts[0]]))
await processTransaction(address, transactions[4].props.transaction, 1, accounts[1], 2, List([accounts[0]]))
await refreshTransactions(store, address)
// THEN
checkMinedMoveFundsTx(transactions[3], 'Send 0.01 ETH to')
await checkBalanceOf(address, '0.08')
checkMinedAddOwnerTx(transactions[4], 'Add Owner Adol Metamask 3')
await checkThresholdOf(address, 3)
// WHEN... reducing threshold
await sendRemoveOwnerForm(SafeDom, safeButtons[EXPAND_OWNERS_INDEX])
// THEN
await listTxsClickingOn(store, safeButtons[LIST_TXS_INDEX], address)
transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
expect(transactions.length).toBe(6)
let statusses = ['Adol 1 Eth Account [Confirmed]']
await checkPendingRemoveOwnerTx(transactions[5], 3, 'Remove Owner Adol Metamask 3', statusses)
await processTransaction(address, transactions[5].props.transaction, 1, accounts[2], 3, List([accounts[0]]))
await refreshTransactions(store, address)
transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
statusses = ['Adol Metamask 3 [Confirmed]', 'Adol 1 Eth Account [Confirmed]']
await checkPendingRemoveOwnerTx(transactions[5], 3, 'Remove Owner Adol Metamask 3', statusses)
await checkThresholdOf(address, 3)
await processTransaction(
address,
transactions[5].props.transaction,
2,
accounts[1],
3,
List([accounts[0], accounts[2]]),
)
await refreshTransactions(store, address)
await checkThresholdOf(address, 2)
transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
await checkMinedRemoveOwnerTx(transactions[5], 'Remove Owner')
// WHEN... changing threshold
await sendChangeThresholdForm(SafeDom, safeButtons[EDIT_THRESHOLD_INDEX], '1')
await listTxsClickingOn(store, safeButtons[LIST_TXS_INDEX], address)
// THEN
transactions = TestUtils.scryRenderedComponentsWithType(SafeDom, Transaction)
await processTransaction(address, transactions[6].props.transaction, 1, accounts[1], 2, List([accounts[0]]))
await checkThresholdOf(address, 1)
})
})

View File

@ -1,72 +0,0 @@
// @flow
import { Map } from 'immutable'
import * as fetchTokensAction from '~/logic/tokens/store/actions/fetchTokens'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { type Token } from '~/logic/tokens/store/model/token'
import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens'
import { addEtherTo, addTknTo } from '~/test/utils/tokenMovements'
import { dispatchTknBalance } from '~/test/utils/transactions/moveTokens.helper'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
describe('Safe - redux balance property', () => {
let store
let address: string
beforeEach(async () => {
store = aNewStore()
address = await aMinedSafe(store)
})
it('reducer should return 0 to just deployed safe', async () => {
// WHEN
await store.dispatch(fetchTokensAction.fetchTokens(address))
// THEN
const tokens: Map<string, Map<string, Token>> | typeof undefined = store.getState()[TOKEN_REDUCER_ID]
if (!tokens) throw new Error()
const safeBalances: Map<string, Token> | typeof undefined = tokens.get(address)
if (!safeBalances) throw new Error('No tokens available, probably failed to fetch')
expect(safeBalances.size).toBe(11)
// safeBalances.forEach((token: string) => {
// const record = safeBalances.get(token)
// if (!record) throw new Error()
// expect(record.get('funds')).toBe('0')
// })
})
it('reducer should return 0.03456 ETH as funds to safe with 0.03456 ETH', async () => {
// WHEN
await addEtherTo(address, '0.03456')
await store.dispatch(fetchTokensAction.fetchTokens(address))
// THEN
const tokens: Map<string, Map<string, Token>> | typeof undefined = store.getState()[TOKEN_REDUCER_ID]
if (!tokens) throw new Error()
const safeBalances: Map<string, Token> | typeof undefined = tokens.get(address)
if (!safeBalances) throw new Error()
expect(safeBalances.size).toBe(11)
const ethBalance = safeBalances.get(ETH_ADDRESS)
if (!ethBalance) throw new Error()
expect(ethBalance.get('funds')).toBe('0.03456')
})
it('reducer should return 100 TKN when safe has 100 TKN', async () => {
// GIVEN
const numTokens = '100'
const tokenAddress = await addTknTo(address, numTokens)
// WHEN
await dispatchTknBalance(store, tokenAddress, address)
// THEN
const safeBalances = store.getState()[TOKEN_REDUCER_ID].get(address)
expect(safeBalances.size).toBe(1)
const tknBalance = safeBalances.get('TKN')
expect(tknBalance.get('funds')).toBe(String(numTokens))
})
})

View File

@ -1,112 +0,0 @@
// @flow
import { Map, List } from 'immutable'
import { type Safe } from '~/routes/safe/store/models/safe'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { loadSafe } from '~/routes/load/container/Load'
import { safesMapSelector } from '~/routes/safeList/store/selectors'
import { makeOwner, type Owner } from '~/routes/safe/store/models/owner'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { safesInitialState } from '~/routes/safe/store/reducer/safe'
import { setOwners, OWNERS_KEY } from '~/utils/storage'
describe('Safe - redux load safe', () => {
let store
let address: string
let accounts
beforeEach(async () => {
store = aNewStore()
address = await aMinedSafe(store)
localStorage.clear()
accounts = await getWeb3().eth.getAccounts()
})
it('if safe is not present, store and persist it with default names', async () => {
const safeName = 'Loaded Safe'
const safeAddress = address
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
await loadSafe(safeName, safeAddress, updateSafeFn)
const safes: Map<string, Safe> = safesMapSelector(store.getState())
expect(safes.size).toBe(1)
if (!safes) throw new Error()
const safe = safes.get(safeAddress)
if (!safe) throw new Error()
expect(safe.get('name')).toBe(safeName)
expect(safe.get('threshold')).toBe(1)
expect(safe.get('address')).toBe(safeAddress)
expect(safe.get('owners')).toEqual(List([makeOwner({ name: 'UNKNOWN', address: accounts[0] })]))
expect(await safesInitialState()).toEqual(safes)
})
it('if safe is not present but owners, store and persist it with stored names', async () => {
const safeName = 'Loaded Safe'
const safeAddress = address
const ownerName = 'Foo Bar Restores'
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
const owner: Owner = makeOwner({ name: ownerName, address: accounts[0] })
setOwners(safeAddress, List([owner]))
await loadSafe(safeName, safeAddress, updateSafeFn)
const safes: Map<string, Safe> = safesMapSelector(store.getState())
expect(safes.size).toBe(1)
if (!safes) throw new Error()
const safe = safes.get(safeAddress)
if (!safe) throw new Error()
expect(safe.get('name')).toBe(safeName)
expect(safe.get('threshold')).toBe(1)
expect(safe.get('address')).toBe(safeAddress)
expect(safe.get('owners')).toEqual(List([makeOwner({ name: ownerName, address: accounts[0] })]))
expect(await safesInitialState()).toEqual(safes)
})
it('if safe is present but no owners, store and persist it with default names', async () => {
const safeAddress = await aMinedSafe(store)
localStorage.removeItem(`${OWNERS_KEY}-${safeAddress}`)
const safeName = 'Loaded Safe'
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
await loadSafe(safeName, safeAddress, updateSafeFn)
const safes: Map<string, Safe> = safesMapSelector(store.getState())
expect(safes.size).toBe(2)
if (!safes) throw new Error()
const safe = safes.get(safeAddress)
if (!safe) throw new Error()
expect(safe.get('name')).toBe(safeName)
expect(safe.get('threshold')).toBe(1)
expect(safe.get('address')).toBe(safeAddress)
expect(safe.get('owners')).toEqual(List([makeOwner({ name: 'UNKNOWN', address: accounts[0] })]))
expect(await safesInitialState()).toEqual(safes)
})
it('if safe is present but owners, store and persist it with stored names', async () => {
const safeAddress = await aMinedSafe(store)
const safeName = 'Loaded Safe'
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
await loadSafe(safeName, safeAddress, updateSafeFn)
const safes: Map<string, Safe> = safesMapSelector(store.getState())
expect(safes.size).toBe(2)
if (!safes) throw new Error()
const safe = safes.get(safeAddress)
if (!safe) throw new Error()
expect(safe.get('name')).toBe(safeName)
expect(safe.get('threshold')).toBe(1)
expect(safe.get('address')).toBe(safeAddress)
expect(safe.get('owners')).toEqual(List([makeOwner({ name: 'Adol 1 Eth Account', address: accounts[0] })]))
expect(await safesInitialState()).toEqual(safes)
})
})

View File

@ -1,237 +0,0 @@
// @flow
import { List } from 'immutable'
import { aNewStore } from '~/store'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { confirmationsTransactionSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from '~/routes/safe/components/AddOwner/AddOwnerForm'
import { addOwner } from '~/routes/safe/components/AddOwner/index'
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
import { removeOwner, shouldDecrease, initialValuesFrom } from '~/routes/safe/components/RemoveOwner'
import { DECREASE_PARAM } from '~/routes/safe/components/RemoveOwner/RemoveOwnerForm'
import { getSafeFrom } from '~/test/utils/safeHelper'
import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
import { processTransaction } from '~/logic/safe/safeFrontendOperations'
import { allowedRemoveSenderInTxHistoryService } from '~/config'
import { calculateValuesAfterRemoving } from '~/routes/open/components/SafeOwnersForm'
describe('React DOM TESTS > Add and remove owners', () => {
const processOwnerModification = async (store, safeAddress, executor, threshold, alreadyConfirmed) => {
const reduxTransactions = safeTransactionsSelector(store.getState(), { safeAddress })
const tx = reduxTransactions.get(0)
if (!tx) throw new Error()
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
const data = tx.get('data')
expect(data).not.toBe(null)
expect(data).not.toBe(undefined)
expect(data).not.toBe('')
return processTransaction(safeAddress, tx, confirmed, executor, threshold, alreadyConfirmed)
}
const assureThresholdIs = async (gnosisSafe, threshold: number) => {
const safeThreshold = await gnosisSafe.getThreshold()
expect(Number(safeThreshold)).toEqual(threshold)
}
const assureOwnersAre = async (gnosisSafe, ...owners) => {
const safeOwners = await gnosisSafe.getOwners()
expect(safeOwners.length).toEqual(owners.length)
for (let i = 0; i < owners.length; i += 1) {
expect(safeOwners[i]).toBe(owners[i])
}
}
const getAddressesFrom = (safe: Safe) => safe.get('owners').map(owner => owner.get('address'))
it.only('creates initialValues removing last owner', () => {
const numOwners = 3
const values = {
moe: 'Bart',
[getOwnerNameBy(0)]: 'Foo',
[getOwnerAddressBy(0)]: '0x1',
[getOwnerNameBy(1)]: 'Bar',
[getOwnerAddressBy(1)]: '0x2',
[getOwnerNameBy(2)]: 'Baz',
[getOwnerAddressBy(2)]: '0x3',
}
const indexToRemove = 2
const initialValues = calculateValuesAfterRemoving(indexToRemove, numOwners, values)
expect(initialValues).toEqual({
moe: 'Bart',
[getOwnerNameBy(0)]: 'Foo',
[getOwnerAddressBy(0)]: '0x1',
[getOwnerNameBy(1)]: 'Bar',
[getOwnerAddressBy(1)]: '0x2',
})
})
it.only('creates initialValues removing middle owner', () => {
const numOwners = 3
const values = {
moe: 'Bart',
[getOwnerNameBy(0)]: 'Foo',
[getOwnerAddressBy(0)]: '0x1',
[getOwnerNameBy(1)]: 'Bar',
[getOwnerAddressBy(1)]: '0x2',
[getOwnerNameBy(2)]: 'Baz',
[getOwnerAddressBy(2)]: '0x3',
}
const indexToRemove = 1
const initialValues = calculateValuesAfterRemoving(indexToRemove, numOwners, values)
expect(initialValues).toEqual({
moe: 'Bart',
[getOwnerNameBy(0)]: 'Foo',
[getOwnerAddressBy(0)]: '0x1',
[getOwnerNameBy(1)]: 'Baz',
[getOwnerAddressBy(1)]: '0x3',
})
})
it('adds owner without increasing the threshold', async () => {
// GIVEN
const numOwners = 2
const threshold = 1
const store = aNewStore()
const address = await aMinedSafe(store, numOwners, threshold)
const accounts = await getWeb3().eth.getAccounts()
const gnosisSafe = await getGnosisSafeInstanceAt(address)
const values = {
[NAME_PARAM]: 'Adol 3 Metamask',
[OWNER_ADDRESS_PARAM]: accounts[2],
[INCREASE_PARAM]: false,
}
// WHEN
let safe = getSafeFrom(store.getState(), address)
await addOwner(values, safe, threshold, accounts[0])
// THEN
await assureThresholdIs(gnosisSafe, 1)
await assureOwnersAre(gnosisSafe, accounts[2], accounts[0], accounts[1])
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), address)
expect(safe.get('owners').count()).toBe(3)
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
})
it('adds owner increasing the threshold', async () => {
// GIVEN
const numOwners = 2
const threshold = 1
const store = aNewStore()
const address = await aMinedSafe(store, numOwners, threshold)
const accounts = await getWeb3().eth.getAccounts()
const gnosisSafe = await getGnosisSafeInstanceAt(address)
const values = {
[NAME_PARAM]: 'Adol 3 Metamask',
[OWNER_ADDRESS_PARAM]: accounts[2],
[INCREASE_PARAM]: true,
}
// WHEN
let safe = getSafeFrom(store.getState(), address)
await addOwner(values, safe, threshold, accounts[0])
// THEN
await assureThresholdIs(gnosisSafe, 2)
await assureOwnersAre(gnosisSafe, accounts[2], accounts[0], accounts[1])
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), address)
expect(safe.get('owners').count()).toBe(3)
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
})
it('remove owner decreasing owner automatically', async () => {
if (!allowedRemoveSenderInTxHistoryService()) {
return
}
const numOwners = 2
const threshold = 2
const store = aNewStore()
const address = await aMinedSafe(store, numOwners, threshold)
const accounts = await getWeb3().eth.getAccounts()
const gnosisSafe = await getGnosisSafeInstanceAt(address)
const decrease = shouldDecrease(numOwners, threshold)
const values = initialValuesFrom(decrease)
expect(values[DECREASE_PARAM]).toBe(true)
let safe = getSafeFrom(store.getState(), address)
await removeOwner(values, safe, threshold, accounts[1], 'Adol Metamask 2', accounts[0])
await store.dispatch(fetchTransactions(address))
await processOwnerModification(store, address, accounts[1], 2, List([accounts[0]]))
await assureThresholdIs(gnosisSafe, 1)
await assureOwnersAre(gnosisSafe, accounts[0])
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), address)
expect(safe.get('owners').count()).toBe(1)
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
})
it('remove owner decreasing threshold', async () => {
const numOwners = 3
const threshold = 2
const store = aNewStore()
const address = await aMinedSafe(store, numOwners, threshold)
const accounts = await getWeb3().eth.getAccounts()
const gnosisSafe = await getGnosisSafeInstanceAt(address)
const decrease = true
const values = initialValuesFrom(decrease)
let safe = getSafeFrom(store.getState(), address)
await removeOwner(values, safe, threshold, accounts[2], 'Adol Metamask 3', accounts[0])
await store.dispatch(fetchTransactions(address))
await processOwnerModification(store, address, accounts[1], 2, List([accounts[0]]))
await assureThresholdIs(gnosisSafe, 1)
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), address)
expect(safe.get('owners').count()).toBe(2)
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
})
it('remove owner without decreasing threshold', async () => {
const numOwners = 3
const threshold = 2
const store = aNewStore()
const address = await aMinedSafe(store, numOwners, threshold)
const accounts = await getWeb3().eth.getAccounts()
const gnosisSafe = await getGnosisSafeInstanceAt(address)
const decrease = shouldDecrease(numOwners, threshold)
const values = initialValuesFrom(decrease)
expect(values[DECREASE_PARAM]).toBe(false)
let safe = getSafeFrom(store.getState(), address)
await removeOwner(values, safe, threshold, accounts[2], 'Adol Metamask 3', accounts[0])
await store.dispatch(fetchTransactions(address))
await processOwnerModification(store, address, accounts[1], 2, List([accounts[0]]))
await assureThresholdIs(gnosisSafe, 2)
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), address)
expect(safe.get('owners').count()).toBe(2)
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
})
})

View File

@ -1,178 +0,0 @@
// @flow
import { List } from 'immutable'
import { createTransaction } from '~/logic/safe/safeFrontendOperations'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { type Safe } from '~/routes/safe/store/models/safe'
import { makeOwner } from '~/routes/safe/store/models/owner'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { getSafeFrom } from '~/test/utils/safeHelper'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors'
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
import { testTransactionFrom, testSizeOfTransactions } from './utils/historyServiceHelper'
describe('Transactions Suite', () => {
let store: Store
let safeAddress: string
let accounts: string[]
beforeAll(async () => {
accounts = await getWeb3().eth.getAccounts()
})
beforeEach(async () => {
localStorage.clear()
store = aNewStore()
safeAddress = await aMinedSafe(store)
})
it('retrieves tx info from service having subject available', async () => {
let safe: Safe = getSafeFrom(store.getState(), safeAddress)
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const firstTxData = gnosisSafe.contract.methods.addOwnerWithThreshold(accounts[1], 2).encodeABI()
const executor = accounts[0]
const nonce = await gnosisSafe.nonce()
const firstTxHash = await createTransaction(
safe,
'Add Owner Second account',
safeAddress,
'0',
nonce,
executor,
firstTxData,
)
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), safeAddress)
const secondTxData = gnosisSafe.contract.methods.addOwnerWithThreshold(accounts[2], 2).encodeABI()
const secondTxHash = await createTransaction(
safe,
'Add Owner Third account',
safeAddress,
'0',
nonce + 100,
executor,
secondTxData,
)
await store.dispatch(fetchSafe(safe.get('address')))
safe = getSafeFrom(store.getState(), safeAddress)
// WHEN
await store.dispatch(fetchTransactions(safeAddress))
let transactions = safeTransactionsSelector(store.getState(), { safeAddress })
testSizeOfTransactions(transactions, 2)
// THEN
const firstTxConfirmations = List([
makeConfirmation({
owner: makeOwner({ address: getWeb3().toChecksumAddress(executor), name: 'Adol 1 Eth Account' }),
type: 'execution',
hash: firstTxHash,
}),
])
testTransactionFrom(
transactions,
0,
'Add Owner Second account',
nonce,
0,
safeAddress,
firstTxData,
true,
firstTxConfirmations,
)
const secondTxConfirmations = List([
makeConfirmation({
owner: makeOwner({ address: getWeb3().toChecksumAddress(accounts[0]), name: 'Adol 1 Eth Account' }),
type: 'confirmation',
hash: secondTxHash,
}),
])
testTransactionFrom(
transactions,
1,
'Add Owner Third account',
nonce + 100,
0,
safeAddress,
secondTxData,
false,
secondTxConfirmations,
)
localStorage.clear()
await store.dispatch(fetchTransactions(safeAddress))
transactions = safeTransactionsSelector(store.getState(), { safeAddress })
testSizeOfTransactions(transactions, 2)
const firstTxConfWithoutStorage = List([
makeConfirmation({
owner: makeOwner({ address: getWeb3().toChecksumAddress(executor), name: 'UNKNOWN' }),
type: 'execution',
hash: firstTxHash,
}),
])
testTransactionFrom(transactions, 0, 'Unknown', nonce, 0, safeAddress, firstTxData, true, firstTxConfWithoutStorage)
const secondTxConfWithoutStorage = List([
makeConfirmation({
owner: makeOwner({ address: getWeb3().toChecksumAddress(executor), name: 'UNKNOWN' }),
type: 'confirmation',
hash: secondTxHash,
}),
])
testTransactionFrom(
transactions,
1,
'Unknown',
nonce + 100,
0,
safeAddress,
secondTxData,
false,
secondTxConfWithoutStorage,
)
})
it('returns empty list of trnsactions when safe is not configured', async () => {
// routes/safe/transactions.selector.js the 4 cases
// confirmations.selector.js the last one
})
it('pending transactions are treated correctly', async () => {
// create a safe 3 owners 3 threshold
// create a tx adding 4th owner
// confirm tx and check on every step
})
it('returns count of confirmed but not executed txs', async () => {
// pendingTransactionSelector
})
it('returns count of executed txs', async () => {
// confirmationsTransactionSelector
})
it('returns correctly transaction list when safe is not available', async () => {
// routes/safe/test/transactions.selector.js
})
it('process only updated txs', async () => {
// Basically I would like when I call the GET TXs endpoint to retrieve those transactions ORDERED based on
// when they have been updated (just created, or just added another extra confirmation).
// In that way I do not need to parse and threat all txs in client side and also we mitigate the risk of
// do not get old txs updates. For doing that I would need to keep stored a number indicating
// if the tx has been updated in DB.
// For instance:
/*
create tx1 ---> [{ tx:1, updated: 1 }]
create tx2 ---> [{ tx:2, updated: 1 }, { tx:1, updated: 1 }]
user 2 confirms tx1 ---> [{ tx:1, updated: 2 }, { tx:2, updated: 1 }]
In that way I keep stored tx1 -> 1 and if I see tx2 -> 2 I do not skip it
*/
})
})

View File

@ -1,67 +1,83 @@
// @flow
import * as TestUtils from 'react-dom/test-utils'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type Match } from 'react-router-dom'
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
import { fireEvent } from '@testing-library/react'
import { getFirstTokenContract } from '~/test/utils/tokenMovements'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToTokens } from '~/test/builder/safe.dom.utils'
import { renderSafeView } from '~/test/builder/safe.dom.utils'
import { sleep } from '~/utils/timer'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { tokenListSelector } from '~/logic/tokens/store/selectors'
import { testToken } from '~/test/builder/tokens.dom.utils'
import { clickOnManageTokens, clickOnAddCustomToken } from '~/test/utils/DOMNavigation'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import { clickOnAddToken, fillAddress, fillHumanReadableInfo } from '~/test/utils/tokens/addToken.helper'
import {
ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID,
ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID,
ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID,
ADD_CUSTOM_TOKEN_FORM,
} from '~/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances/'
import 'jest-dom/extend-expect'
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
// let web3
// let accounts
// let firstErc20Token
// let secondErc20Token
// https://github.com/testing-library/@testing-library/react/issues/281
const originalError = console.error
beforeAll(() => {
console.error = (...args) => {
if (/Warning.*not wrapped in act/.test(args[0])) {
return
}
originalError.call(console, ...args)
}
})
// beforeAll(async () => {
// web3 = getWeb3()
// accounts = await web3.eth.getAccounts()
// firstErc20Token = await getFirstTokenContract(web3, accounts[0])
// secondErc20Token = await getSecondTokenContract(web3, accounts[0])
afterAll(() => {
console.error = originalError
})
// // $FlowFixMe
// enhancedFetchModule.enhancedFetch = jest.fn()
// enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve({
// results: [
// {
// address: firstErc20Token.address,
// name: 'First Token Example',
// symbol: 'FTE',
// decimals: 18,
// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
// },
// ],
// }))
// })
describe('DOM > Feature > Add custom ERC 20 Tokens', () => {
let web3
let accounts
let erc20Token
it('adds a second erc 20 token filling the form', async () => {
// // GIVEN
// const store = aNewStore()
// const safeAddress = await aMinedSafe(store)
// await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
// const TokensDom = await travelToTokens(store, safeAddress)
// await sleep(400)
// const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent)
// expect(tokens.length).toBe(2)
// testToken(tokens[0].props.token, 'FTE', false)
// testToken(tokens[1].props.token, 'ETH', true)
// // WHEN
// await clickOnAddToken(TokensDom)
// await fillAddress(TokensDom, secondErc20Token)
// await fillHumanReadableInfo(TokensDom)
// // THEN
// const match: Match = buildMathPropsFrom(safeAddress)
// const tokenList = tokenListSelector(store.getState(), { match })
// expect(tokenList.count()).toBe(3)
// testToken(tokenList.get(0), 'FTE', false)
// testToken(tokenList.get(1), 'ETH', true)
// testToken(tokenList.get(2), 'TKN', true)
beforeAll(async () => {
web3 = getWeb3()
accounts = await web3.eth.getAccounts()
erc20Token = await getFirstTokenContract(web3, accounts[0])
})
it('adds and displays an erc 20 token after filling the form', async () => {
// GIVEN
const store = aNewStore()
const safeAddress = await aMinedSafe(store)
await store.dispatch(fetchTokensModule.fetchTokens())
const TokensDom = renderSafeView(store, safeAddress)
await sleep(400)
// WHEN
clickOnManageTokens(TokensDom)
clickOnAddCustomToken(TokensDom)
await sleep(200)
// Fill address
const addTokenForm = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_FORM)
const addressInput = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID)
fireEvent.change(addressInput, { target: { value: erc20Token.address } })
await sleep(500)
// Check if it loaded symbol/decimals correctly
const symbolInput = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID)
const decimalsInput = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID)
const tokenSymbol = await erc20Token.symbol()
const tokenDecimals = await erc20Token.decimals()
expect(symbolInput.value).toBe(tokenSymbol)
expect(decimalsInput.value).toBe(tokenDecimals.toString())
// Submit form
fireEvent.submit(addTokenForm)
await sleep(300)
// check if token is displayed
const balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows.length).toBe(2)
expect(balanceRows[1]).toHaveTextContent(tokenSymbol)
})
})

View File

@ -1,130 +1,83 @@
// @flow
import * as TestUtils from 'react-dom/test-utils'
import { List } from 'immutable'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type Match } from 'react-router-dom'
import Checkbox from '@material-ui/core/Checkbox'
import { getFirstTokenContract, getSecondTokenContract, addTknTo } from '~/test/utils/tokenMovements'
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToTokens } from '~/test/builder/safe.dom.utils'
import { renderSafeView } from '~/test/builder/safe.dom.utils'
import { sleep } from '~/utils/timer'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { tokenListSelector } from '~/logic/tokens/store/selectors'
import { getActiveTokenAddresses } from '~/logic/tokens/utils/tokensStorage'
import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import saveTokens from '~/logic/tokens/store/actions/saveTokens'
import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './utils/DOMNavigation'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
import { makeToken } from '~/logic/tokens/store/model/token'
import 'jest-dom/extend-expect'
describe('DOM > Feature > Enable and disable default tokens', () => {
let web3
let accounts
let firstErc20Token
let secondErc20Token
let testTokens
beforeAll(async () => {
web3 = getWeb3()
accounts = await web3.eth.getAccounts()
firstErc20Token = await getFirstTokenContract(web3, accounts[0])
secondErc20Token = await getSecondTokenContract(web3, accounts[0])
// $FlowFixMe
enhancedFetchModule.enhancedFetch = jest.fn()
enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve({
results: [
{
address: firstErc20Token.address,
name: 'First Token Example',
symbol: 'FTE',
decimals: 18,
logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
},
{
address: secondErc20Token.address,
name: 'Second Token Example',
symbol: 'STE',
decimals: 18,
logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
},
],
}))
testTokens = List([
makeToken({
address: firstErc20Token.address,
name: 'First Token Example',
symbol: 'FTE',
decimals: 18,
logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
}),
makeToken({
address: secondErc20Token.address,
name: 'Second Token Example',
symbol: 'STE',
decimals: 18,
logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
}),
])
})
it('retrieves only ether as active token in first moment', async () => {
it('allows to enable and disable tokens', async () => {
// GIVEN
const store = aNewStore()
const safeAddress = await aMinedSafe(store)
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
await store.dispatch(saveTokens(testTokens))
// WHEN
const TokensDom = await travelToTokens(store, safeAddress)
const TokensDom = await renderSafeView(store, safeAddress)
await sleep(400)
// THEN
const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent)
expect(tokens.length).toBe(3)
testToken(tokens[0].props.token, 'FTE', false)
testToken(tokens[1].props.token, 'STE', false)
testToken(tokens[2].props.token, 'ETH', true)
const ethCheckbox = TestUtils.findRenderedComponentWithType(tokens[2], Checkbox)
if (!ethCheckbox) throw new Error()
expect(ethCheckbox.props.disabled).toBe(true)
})
it('fetch balances of only enabled tokens', async () => {
// GIVEN
const store = aNewStore()
const safeAddress = await aMinedSafe(store)
await addTknTo(safeAddress, '50', firstErc20Token)
await addTknTo(safeAddress, '50', secondErc20Token)
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
const match: Match = buildMathPropsFrom(safeAddress)
let tokenList = tokenListSelector(store.getState(), { match })
expect(tokenList.count()).toBe(3)
await enableFirstToken(store, safeAddress)
tokenList = tokenListSelector(store.getState(), { match })
expect(tokenList.count()).toBe(3) // assuring the enableToken do not add extra info
// Check if only ETH is enabled
let balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows.length).toBe(1)
// THEN
testToken(tokenList.get(0), 'FTE', true)
testToken(tokenList.get(1), 'STE', false)
testToken(tokenList.get(2), 'ETH', true)
clickOnManageTokens(TokensDom)
toggleToken(TokensDom, 'FTE')
toggleToken(TokensDom, 'STE')
closeManageTokensModal(TokensDom)
const activeTokenList = activeTokensSelector(store.getState(), { match })
expect(activeTokenList.count()).toBe(2)
// Check if tokens were enabled
balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows.length).toBe(3)
expect(balanceRows[1]).toHaveTextContent('FTE')
expect(balanceRows[2]).toHaveTextContent('STE')
testToken(activeTokenList.get(0), 'FTE', true)
testToken(activeTokenList.get(1), 'ETH', true)
// disable tokens
clickOnManageTokens(TokensDom)
toggleToken(TokensDom, 'FTE')
toggleToken(TokensDom, 'STE')
closeManageTokensModal(TokensDom)
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
const fundedTokenList = tokenListSelector(store.getState(), { match })
expect(fundedTokenList.count()).toBe(3)
testToken(fundedTokenList.get(0), 'FTE', true, '50')
testToken(fundedTokenList.get(1), 'STE', false, '0')
testToken(fundedTokenList.get(2), 'ETH', true, '0')
})
it('localStorage always returns a list', async () => {
const store = aNewStore()
const safeAddress = await aMinedSafe(store)
let tokens: List<string> = getActiveTokenAddresses(safeAddress)
expect(tokens).toEqual(List([]))
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
tokens = getActiveTokenAddresses(safeAddress)
expect(tokens.count()).toBe(0)
await enableFirstToken(store, safeAddress)
tokens = getActiveTokenAddresses(safeAddress)
expect(tokens.count()).toBe(1)
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
tokens = getActiveTokenAddresses(safeAddress)
expect(tokens.count()).toBe(1)
// check if tokens were disabled
balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows.length).toBe(1)
expect(balanceRows[0]).toHaveTextContent('ETH')
})
})

View File

@ -1,71 +0,0 @@
// @flow
import * as TestUtils from 'react-dom/test-utils'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToTokens } from '~/test/builder/safe.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken'
import { sleep } from '~/utils/timer'
import { testToken } from '~/test/builder/tokens.dom.utils'
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
// let web3
// let accounts
// let firstErc20Token
// let secondErc20Token
// beforeAll(async () => {
// web3 = getWeb3()
// accounts = await web3.eth.getAccounts()
// firstErc20Token = await getFirstTokenContract(web3, accounts[0])
// secondErc20Token = await getSecondTokenContract(web3, accounts[0])
// // $FlowFixMe
// enhancedFetchModule.enhancedFetch = jest.fn()
// enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve({
// results: [
// {
// address: firstErc20Token.address,
// name: 'First Token Example',
// symbol: 'FTE',
// decimals: 18,
// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
// },
// ],
// }))
// })
// it('remove custom ERC 20 tokens', async () => {
// // GIVEN
// const store = aNewStore()
// const safeAddress = await aMinedSafe(store)
// await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
// const values = {
// [TOKEN_ADRESS_PARAM]: secondErc20Token.address,
// [TOKEN_NAME_PARAM]: 'Custom ERC20 Token',
// [TOKEN_SYMBOL_PARAM]: 'CTS',
// [TOKEN_DECIMALS_PARAM]: '10',
// [TOKEN_LOGO_URL_PARAM]: 'https://example.com',
// }
// const customAddTokensFn: any = (...args) => store.dispatch(addToken(...args))
// await addTokenFnc(values, customAddTokensFn, safeAddress)
// const TokensDom = travelToTokens(store, safeAddress)
// await sleep(400)
// // WHEN
// const buttons = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'button')
// expect(buttons.length).toBe(2)
// const removeUserButton = buttons[0]
// expect(removeUserButton.getAttribute('aria-label')).toBe('Delete')
// TestUtils.Simulate.click(removeUserButton)
// await sleep(400)
// const form = TestUtils.findRenderedDOMComponentWithTag(TokensDom, 'form')
// // submit it
// TestUtils.Simulate.submit(form)
// TestUtils.Simulate.submit(form)
// await sleep(400)
// const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent)
// expect(tokens.length).toBe(2)
// testToken(tokens[0].props.token, 'FTE', false)
// testToken(tokens[1].props.token, 'ETH', true)
// })
})

View File

@ -1,64 +0,0 @@
// @flow
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type Match } from 'react-router-dom'
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToSafe } from '~/test/builder/safe.dom.utils'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken'
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
// let web3
// let accounts
// let firstErc20Token
// let secondErc20Token
// beforeAll(async () => {
// web3 = getWeb3()
// accounts = await web3.eth.getAccounts()
// firstErc20Token = await getFirstTokenContract(web3, accounts[0])
// secondErc20Token = await getSecondTokenContract(web3, accounts[0])
// // $FlowFixMe
// enhancedFetchModule.enhancedFetch = jest.fn()
// enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve({
// results: [
// {
// address: firstErc20Token.address,
// name: 'First Token Example',
// symbol: 'FTE',
// decimals: 18,
// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
// },
// ],
// }))
// })
// it('persist added custom ERC 20 tokens as active when reloading the page', async () => {
// // GIVEN
// const store = aNewStore()
// const safeAddress = await aMinedSafe(store)
// await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
// const values = {
// [TOKEN_ADRESS_PARAM]: secondErc20Token.address,
// [TOKEN_NAME_PARAM]: 'Custom ERC20 Token',
// [TOKEN_SYMBOL_PARAM]: 'CTS',
// [TOKEN_DECIMALS_PARAM]: '10',
// [TOKEN_LOGO_URL_PARAM]: 'https://example.com',
// }
// const customAddTokensFn: any = (...args) => store.dispatch(addToken(...args))
// await addTokenFnc(values, customAddTokensFn, safeAddress)
// travelToSafe(store, safeAddress)
// // WHEN
// const reloadedStore = aNewStore()
// await reloadedStore.dispatch(fetchTokensModule.fetchTokens(safeAddress))
// travelToSafe(reloadedStore, safeAddress) // reload
// // THEN
// const match: Match = buildMathPropsFrom(safeAddress)
// const activeTokenList = activeTokensSelector(reloadedStore.getState(), { match })
// expect(activeTokenList.count()).toBe(2)
// testToken(activeTokenList.get(0), 'CTS', true)
// testToken(activeTokenList.get(1), 'ETH', true)
// })
})

View File

@ -1,83 +0,0 @@
// @flow
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type Match } from 'react-router-dom'
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToSafe } from '~/test/builder/safe.dom.utils'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken'
import removeTokenAction from '~/logic/tokens/store/actions/removeToken'
import { makeToken } from '~/logic/tokens/store/model/token'
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
// let web3
// let accounts
// let firstErc20Token
// let secondErc20Token
// beforeAll(async () => {
// web3 = getWeb3()
// accounts = await web3.eth.getAccounts()
// firstErc20Token = await getFirstTokenContract(web3, accounts[0])
// secondErc20Token = await getSecondTokenContract(web3, accounts[0])
// // $FlowFixMe
// enhancedFetchModule.enhancedFetch = jest.fn()
// enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve({
// results: [
// {
// address: firstErc20Token.address,
// name: 'First Token Example',
// symbol: 'FTE',
// decimals: 18,
// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
// },
// ],
// }))
// })
// const checkTokensOf = (store: Store, safeAddress: string) => {
// const match: Match = buildMathPropsFrom(safeAddress)
// const activeTokenList = activeTokensSelector(store.getState(), { match })
// expect(activeTokenList.count()).toBe(1)
// testToken(activeTokenList.get(0), 'ETH', true)
// const tokenList = tokenListSelector(store.getState(), { match })
// expect(tokenList.count()).toBe(2)
// testToken(tokenList.get(0), 'FTE', false)
// testToken(tokenList.get(1), 'ETH', true)
// }
// it('removes custom ERC 20 including page reload', async () => {
// // GIVEN
// const store = aNewStore()
// const safeAddress = await aMinedSafe(store)
// await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
// const values = {
// [TOKEN_ADRESS_PARAM]: secondErc20Token.address,
// [TOKEN_NAME_PARAM]: 'Custom ERC20 Token',
// [TOKEN_SYMBOL_PARAM]: 'CTS',
// [TOKEN_DECIMALS_PARAM]: '10',
// [TOKEN_LOGO_URL_PARAM]: 'https://example.com',
// }
// const customAddTokensFn: any = (...args) => store.dispatch(addToken(...args))
// await addTokenFnc(values, customAddTokensFn, safeAddress)
// const token = makeToken({
// address: secondErc20Token.address,
// name: 'Custom ERC20 Token',
// symbol: 'CTS',
// decimals: 10,
// logoUri: 'https://example.com',
// status: true,
// removable: true,
// })
// const customRemoveTokensFnc: any = (...args) => store.dispatch(removeTokenAction(...args))
// await removeToken(safeAddress, token, customRemoveTokensFnc)
// checkTokensOf(store, safeAddress)
// // WHEN
// const reloadedStore = aNewStore()
// await reloadedStore.dispatch(fetchTokensModule.fetchTokens(safeAddress))
// travelToSafe(reloadedStore, safeAddress) // reload
// // THEN
// checkTokensOf(reloadedStore, safeAddress)
// })
})

View File

@ -0,0 +1,3 @@
// @flow
export * from './tokens'

View File

@ -0,0 +1,29 @@
// @flow
import { fireEvent } from '@testing-library/react'
import { MANAGE_TOKENS_BUTTON_TEST_ID } from '~/routes/safe/components/Balances'
import { ADD_CUSTOM_TOKEN_BUTTON_TEST_ID, TOGGLE_TOKEN_TEST_ID } from '~/routes/safe/components/Balances/Tokens/screens/TokenList'
import { MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID } from '~/routes/safe/components/Balances/Tokens'
export const clickOnManageTokens = (dom: any): void => {
const btn = dom.getByTestId(MANAGE_TOKENS_BUTTON_TEST_ID)
fireEvent.click(btn)
}
export const clickOnAddCustomToken = (dom: any): void => {
const btn = dom.getByTestId(ADD_CUSTOM_TOKEN_BUTTON_TEST_ID)
fireEvent.click(btn)
}
export const toggleToken = (dom: any, symbol: string): void => {
const btn = dom.getByTestId(`${symbol}_${TOGGLE_TOKEN_TEST_ID}`)
fireEvent.click(btn)
}
export const closeManageTokensModal = (dom: any) => {
const btn = dom.getByTestId(MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID)
fireEvent.click(btn)
}

View File

@ -1,10 +0,0 @@
// @flow
import * as React from 'react'
type WrapperProps = {
children: React$Node,
}
const Wrapper = ({ children }: WrapperProps) => <React.Fragment>{children}</React.Fragment>
export default Wrapper

View File

@ -1,7 +1,7 @@
// @flow
import { type Match } from 'react-router-dom'
export const buildMathPropsFrom = (address: string): Match => ({
export const buildMatchPropsFrom = (address: string): Match => ({
params: {
address,
},

View File

@ -1,4 +1,5 @@
// @flow
import React from 'react'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import GnoStepper from '~/components/Stepper'
import Stepper from '@material-ui/core/Stepper'
@ -37,7 +38,7 @@ type FinsihedTx = {
finishedTransaction: boolean,
}
export const whenExecuted = (SafeDom: React$Component<any, any>, ParentComponent: React$ElementType): Promise<void> => new Promise((resolve, reject) => {
export const whenExecuted = (SafeDom: React.Component<any, any>, ParentComponent: React.ElementType): Promise<void> => new Promise((resolve, reject) => {
let times = 0
const interval = setInterval(() => {
if (times >= MAX_TIMES_EXECUTED) {
@ -47,7 +48,7 @@ export const whenExecuted = (SafeDom: React$Component<any, any>, ParentComponent
// $FlowFixMe
const SafeComponent = TestUtils.findRenderedComponentWithType(SafeDom, ParentComponent)
type GnoStepperType = React$Component<FinsihedTx, any>
type GnoStepperType = React.Component<FinsihedTx, any>
// $FlowFixMe
const StepperComponent: GnoStepperType = TestUtils.findRenderedComponentWithType(SafeComponent, GnoStepper)
@ -64,8 +65,8 @@ type MiddleStep = {
}
export const whenOnNext = (
SafeDom: React$Component<any, any>,
ParentComponent: React$ElementType,
SafeDom: React.Component<any, any>,
ParentComponent: React.ElementType,
position: number,
): Promise<void> => new Promise((resolve, reject) => {
let times = 0
@ -77,7 +78,7 @@ export const whenOnNext = (
// $FlowFixMe
const SafeComponent = TestUtils.findRenderedComponentWithType(SafeDom, ParentComponent)
type StepperType = React$Component<MiddleStep, any>
type StepperType = React.Component<MiddleStep, any>
// $FlowFixMe
const StepperComponent: StepperType = TestUtils.findRenderedComponentWithType(SafeComponent, Stepper)
if (StepperComponent.props.activeStep === position) {

View File

@ -1,12 +1,12 @@
// @flow
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { buildMatchPropsFrom } from '~/test/utils/buildReactRouterProps'
import { safeSelector } from '~/routes/safe/store/selectors/index'
import { type Match } from 'react-router-dom'
import { type GlobalState } from '~/store'
import { type Safe } from '~/routes/safe/store/models/safe'
export const getSafeFrom = (state: GlobalState, safeAddress: string): Safe => {
const match: Match = buildMathPropsFrom(safeAddress)
const match: Match = buildMatchPropsFrom(safeAddress)
const safe = safeSelector(state, { match })
if (!safe) throw new Error()

View File

@ -1,14 +1,16 @@
// @flow
import contract from 'truffle-contract'
import { getBalanceInEtherOf, getWeb3 } from '~/logic/wallets/getWeb3'
import Token from '#/test/TestToken.json'
import { ensureOnce } from '~/utils/singleton'
import { toNative } from '~/logic/wallets/tokens'
import TokenOMG from '../../../build/contracts/TokenOMG'
import TokenRDN from '../../../build/contracts/TokenRDN'
export const addEtherTo = async (address: string, eth: string) => {
export const sendEtherTo = async (address: string, eth: string) => {
const web3 = getWeb3()
const accounts = await web3.eth.getAccounts()
const txData = { from: accounts[0], to: address, value: web3.utils.toWei(eth, 'ether') }
const { toBN, toWei } = web3.utils
const txData = { from: accounts[0], to: address, value: toBN(toWei(eth, 'ether')) }
return web3.eth.sendTransaction(txData)
}
@ -17,23 +19,38 @@ export const checkBalanceOf = async (addressToTest: string, value: string) => {
expect(safeBalance).toBe(value)
}
const createTokenContract = async (web3: any, executor: string) => {
const token = contract(Token)
const createTokenOMGContract = async (web3: any, creator: string) => {
const token = contract(TokenOMG)
const { toBN } = web3.utils
const amount = toBN(50000)
.mul(toBN(10).pow(toBN(18)))
.toString()
token.setProvider(web3.currentProvider)
return token.new({ from: executor, gas: '5000000' })
return token.new(amount, { from: creator })
}
export const getFirstTokenContract = ensureOnce(createTokenContract)
export const getSecondTokenContract = ensureOnce(createTokenContract)
const createTokenRDNContract = async (web3: any, creator: string) => {
const token = contract(TokenRDN)
const { toBN } = web3.utils
const amount = toBN(50000)
.mul(toBN(10).pow(toBN(18)))
.toString()
token.setProvider(web3.currentProvider)
export const addTknTo = async (safe: string, value: string, tokenContract?: any) => {
return token.new(amount, { from: creator })
}
export const getFirstTokenContract = ensureOnce(createTokenOMGContract)
export const getSecondTokenContract = ensureOnce(createTokenRDNContract)
export const sendTokenTo = async (safe: string, value: string, tokenContract?: any) => {
const web3 = getWeb3()
const accounts = await web3.eth.getAccounts()
const myToken = tokenContract || (await getFirstTokenContract(web3, accounts[0]))
const OMGToken = tokenContract || (await getFirstTokenContract(web3, accounts[0]))
const nativeValue = toNative(value, 18)
await myToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' })
await OMGToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' })
return myToken.address
return OMGToken.address
}

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