Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E2e tests #65

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"presets": ["es2015", "stage-2", "stage-3"]
}
"presets": ["@babel/preset-env"]
}
46 changes: 29 additions & 17 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,50 +1,62 @@
# Javascript Node CircleCI 2.0 configuration file
# Javascript Node CircleCI 2.1 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
version: 2.1

references:
node_modules_key: &node_modules_key redemptions-dependencies-v3-{{ checksum "package.json" }}

restore_node_modules: &restore_node_modules
restore_cache:
keys:
- *node_modules_key

jobs:
build:
docker:
# specify the version you desire here
- image: circleci/node:8.15.1

# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
- image: circleci/node:lts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to use circleci/node:8.15.1 so the restore-cache works


working_directory: ~/repo

steps:
- checkout

# Download and cache dependencies
- restore_cache:
keys:
- redemptions-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
# - redemptions-

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add this back again and use restore_node_modules
so it would be

- *restore_node_modules

- run:
name: Install dependencies
command: npm install

- save_cache:
paths:
- node_modules
key: redemptions-{{ checksum "package.json" }}
key: *node_modules_key

coverage:
docker:
# specify the version you desire here
- image: circleci/node:lts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- image: circleci/node:lts
- image: circleci/node:8.15.1


working_directory: ~/repo

steps:
- checkout

- *restore_node_modules

- run:
name: Run tests
command: npm run coverage

- run:
name: Report Coverage
Command: npm run coveralls
command: npm run coveralls

workflows:
version: 2
build_and_test:
jobs:
- build
- coverage:
requires:
- build
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![CircleCI](https://circleci.com/gh/1Hive/redemptions-app.svg?style=svg)](https://circleci.com/gh/1Hive/redemptions-app)
[![Coverage Status](https://coveralls.io/repos/github/1Hive/redemptions-app/badge.svg?branch=master&service=github)](https://coveralls.io/github/1Hive/redemptions-app?branch=master&service=github)
[![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/1hive/redemptions-app)

1Hive's Redemptions app allows Aragon organizations to grant their token holders the right to redeem tokens in exchange for a proportional share of the organizations treasury assets.

Expand Down
1 change: 0 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"core-js": "^3.1.4",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"regenerator-runtime": "^0.13.2",
"rxjs": "^6.5.2",
"styled-components": "^4.3.2",
"web3-utils": "^1.0.0-beta.30"
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/BalanceToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const BalanceToken = ({
}) => (
<Balance removable={removable}>
<Top>
<Token title={symbol || 'Unknown symbol'}>
<Token id= {`${symbol}Title`} title={symbol || 'Unknown symbol'}>
{verified && symbol && (
<img
alt=""
Expand All @@ -43,7 +43,7 @@ const BalanceToken = ({
</Remove>
</Top>
<Bottom>
<Amount>{splitAmount(amount, decimals)}</Amount>
<Amount id= {`${symbol}Amount`}>{splitAmount(amount, decimals)}</Amount>
</Bottom>
</Balance>
)
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/Balances.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Balances extends Component {
<Viewport>
{({ below }) => (
<section>
<Title>Tokens for redemption</Title>
<Title id="TokensTitle">Tokens for redemption</Title>
<ScrollView>
<List>
{tokens.length > 0 ? (
Expand Down
23 changes: 6 additions & 17 deletions app/src/components/Forms/UpdateTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ import { capitalizeFirst } from '../../lib/utils'
import { ErrorMessage, InfoMessage } from './Message'

const validate = (mode, address, tokens) => {
if (!isAddress(address))
return 'Token address is not a valid Ethereum address'
if (!isAddress(address)) return 'Token address is not a valid Ethereum address'

const exists = tokens.some(t => addressesEqual(t.address, address))
if (mode === 'add' && exists) return 'Token already added to redemption list'

if (mode === 'remove' && !exists)
return 'Token is not added to redemption list'
if (mode === 'remove' && !exists) return 'Token is not added to redemption list'

return null
}
Expand All @@ -37,9 +35,7 @@ class UpdateTokens extends Component {
if (!opened && this.props.opened) {
// setTimeout is needed as a small hack to wait until the input's on
// screen until we call focus
this.props.mode === 'add' &&
this.addressRef &&
setTimeout(() => this.addressRef.focus(), 100)
this.props.mode === 'add' && this.addressRef && setTimeout(() => this.addressRef.focus(), 100)

this.setState(({ address }) => ({
address: { ...address, value: this.props.tokenAddress },
Expand Down Expand Up @@ -81,8 +77,7 @@ class UpdateTokens extends Component {
render() {
const { address } = this.state
const { mode, tokens } = this.props
let token =
mode === 'remove' ? tokens.find(t => t.address === address.value) : {}
let token = mode === 'remove' ? tokens.find(t => t.address === address.value) : {}
let { name, symbol } = token || {}

const errorMessage = address.error
Expand All @@ -108,16 +103,10 @@ class UpdateTokens extends Component {
ref={address => (this.addressRef = address)}
/>
) : (
address.value && (
<TokenBadge
address={address.value}
name={name}
symbol={symbol}
/>
)
address.value && <TokenBadge address={address.value} name={name} symbol={symbol} />
)}
</Field>
<Button mode="strong" wide={true} type="submit">
<Button id="SidePanelButton" mode="strong" wide={true} type="submit">
{`${capitalizeFirst(mode)} token`}
</Button>
{errorMessage && <ErrorMessage message={errorMessage} />}
Expand Down
182 changes: 182 additions & 0 deletions e2eTest/App.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import test from 'ava'
import { startBackgroundProcess } from './util'
const dappeteer = require('dappeteer')
import puppeteer from 'puppeteer-core'

test.before(async t => {
const chromePath = process.env.CHROME_PATH

const browser = await dappeteer.launch(puppeteer, {
headless: false,
executablePath: chromePath,
defaultViewport: null,
args: ['--start-maximized', '--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'],
})
const page = await browser.newPage()
const metamask = await dappeteer.getMetamask(browser)
const { stdout, exit } = await startBackgroundProcess({
cmd: 'npm',
args: ['run', 'start:template'],
execaOpts: {
cwd: `./`,
},
readyOutput: 'Opening http://localhost:3000/#/',
})

// hack so the wrapper has time to start
await new Promise(resolve => setTimeout(resolve, 60 * 1000)) // TODO move to utils

// finding the DAO address
const daoAddress = stdout.match(/DAO address: (0x[a-fA-F0-9]{40})/)[1]

const { stdoutScript, exitScript } = await startBackgroundProcess({
cmd: 'npm',
args: ['run', 'deploy-tokens', daoAddress],
execaOpts: {
cwd: `./`,
},
readyOutput: 'ETH 0x0000000000000000000000000000000000000000',
})
await metamask.switchNetwork('localhost')
await metamask.importPK('a8a54b2d8197bc0b19bb8a084031be71835580a01e70a45a13babd16c9bc1563')
await metamask.switchAccount(2)

t.context = {
browser: browser,
page: page,
daoAddress: daoAddress,
metamask: metamask,
exit: exit,
}
})

test.serial('should display initial screen ', async t => {
const redemptionsText = 'Redemptions'
let addTokenText
const url = `http://localhost:3000/#/${t.context.daoAddress}`
await t.context.page.goto(url)
await t.context.page.reload()
await t.context.page.bringToFront()
await t.context.page.waitForFunction(
redemptionsText => document.querySelector('body').innerText.includes(redemptionsText),
{},
redemptionsText
)
const RedemptionsAppSpan = await t.context.page.$x("//span[contains(text(), 'Redemptions')]")
if (RedemptionsAppSpan.length > 0) {
await RedemptionsAppSpan[0].click()
await t.context.page.waitFor(3000)
} else {
throw new Error('Redemptions app not found')
}

const frame = await t.context.page.frames().find(f => f.name() === 'AppIFrame')
const addTokenButton = await frame.$x("//button[contains(text(), 'Add token')]")
if (addTokenButton.length > 0) {
const button = addTokenButton[0]
addTokenText = await frame.evaluate(button => button.textContent, button)
}
t.is(addTokenText, 'Add token')
})

test.serial('should display add token side panel and create transaction', async t => {
let addTokenText
const frame = await t.context.page.frames().find(f => f.name() === 'AppIFrame')
const addTokenButton = await frame.$x("//button[contains(text(), 'Add token')]")

if (addTokenButton.length > 0) {
await addTokenButton[0].click()
await frame.type('input[name=address]', '0x0000000000000000000000000000000000000000', { delay: 20 })
await frame.waitForSelector('#SidePanelButton')
const AddTokenSidePanelButton = await frame.$('#SidePanelButton')
addTokenText = await frame.evaluate(
AddTokenSidePanelButton => AddTokenSidePanelButton.textContent,
AddTokenSidePanelButton
)

await AddTokenSidePanelButton.click()
await t.context.page.waitFor(4000)
} else {
throw new Error('Add token button not found')
}
t.is(addTokenText, 'Add token')
})

test.serial('should confirm create transaction and vote', async t => {
const createTransaction = await t.context.page.$x("//button[contains(text(), 'Create transaction')]")
let viewVoteButton
let viewVoteText
const votingText = 'Voting'

if (createTransaction.length > 0) {
await createTransaction[0].click()
await t.context.metamask.confirmTransaction()
await t.context.page.waitFor(3000)
await t.context.page.bringToFront()
await t.context.page.waitForFunction(
votingText => document.querySelector('body').innerText.includes(votingText),
{},
votingText
)
const votingAppSpan = await t.context.page.$x("//span[contains(text(), 'Voting')]")

if (votingAppSpan.length > 0) {
await votingAppSpan[0].click()
await t.context.page.waitFor(3000)
const votingFrame = await t.context.page.frames().find(f => f.name() === 'AppIFrame')
const viewVote = await votingFrame.$x("//button[contains(text(), 'View vote')]")

if (viewVote.length > 0) {
viewVoteButton = viewVote[0]
viewVoteText = await votingFrame.evaluate(viewVoteButton => viewVoteButton.textContent, viewVoteButton)
await viewVoteButton.click()
await t.context.page.waitFor(3000)
const voteYes = await votingFrame.$x("//button[contains(text(), 'Yes')]")
if (voteYes.length > 0) {
voteYes[0].click()
await t.context.page.waitFor(3000)
const createVotingTransaction = await t.context.page.$x("//button[contains(text(), 'Create transaction')]")

if (createVotingTransaction.length > 0) {
await createVotingTransaction[0].click()
await t.context.metamask.confirmTransaction()
await t.context.page.waitFor(3000)
await t.context.page.bringToFront()
} else {
throw new Error('Create transaction not found')
}
} else {
throw new Error('Vote Yes button not found')
}
} else {
throw new Error('View vote button not found')
}
} else {
throw new Error('Voting app not found')
}
}
t.is(viewVoteText, 'View vote')
})

test.serial('should add the token ', async t => {
let RedemptionsListTitle
let ETHDiv
let ETHTitle
let frame
const RedemptionsAppSpan = await t.context.page.$x("//span[contains(text(), 'Redemptions')]")

if (RedemptionsAppSpan.length > 0) {
await RedemptionsAppSpan[0].click()
await t.context.page.waitFor(3000)
frame = await t.context.page.frames().find(f => f.name() === 'AppIFrame')
RedemptionsListTitle = await frame.evaluate(el => el.innerHTML, await frame.$('#TokensTitle'))
ETHDiv = await frame.waitForSelector('#ETHTitle')
ETHTitle = await frame.evaluate(ETHDiv => ETHDiv.textContent, ETHDiv)
} else {
throw new Error('Redemptions app not found')
}

t.is(RedemptionsListTitle, 'Tokens for redemption')
t.is(ETHTitle, 'ETH')
await t.context.exit()
})
Loading