diff --git a/.gitmodules b/.gitmodules index 61c462c2a..c1923368c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,7 @@ path = submodules/poa-token-market-net-ico url = https://github.com/poanetwork/ico branch = wizard +[submodule "submodules/token-wizard-test-automation"] + path = submodules/token-wizard-test-automation + url = https://github.com/poanetwork/token-wizard-test-automation + branch = master diff --git a/.travis.yml b/.travis.yml index 77bc3da15..84b2dc5ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,15 @@ install: before_script: - npm run installWeb3 + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - sleep 3 + - wget -N http://chromedriver.storage.googleapis.com/2.30/chromedriver_linux64.zip -P ~/ + - unzip ~/chromedriver_linux64.zip -d ~/ + - rm ~/chromedriver_linux64.zip + - sudo mv -f ~/chromedriver /usr/local/share/ + - sudo chmod +x /usr/local/share/chromedriver + - sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver script: - npm run lint diff --git a/package.json b/package.json index e41096b43..4c8917ce3 100755 --- a/package.json +++ b/package.json @@ -123,8 +123,9 @@ "deployRegistry": "node scripts/deployRegistry.js", "startWin": "npm run installWeb3 && npm run generateFlatSoliditySafeMathLibContractWin && npm run generateFlatSolidityCrowdsaleNullFinalizeAgentContractWin && npm run generateFlatSolidityCrowdsaleFinalizeAgentContractWin && npm run generateFlatSolidityCrowdsaleContractWin && npm run generateFlatSolidityCrowdsaleTokenContractWin && npm run generateFlatSolidityCrowdsalePricingStrategyContractWin && npm run compileSafeMathLibExtContractWin && npm run compileCrowdsaleNullFinalizeAgentContractWin && npm run compileCrowdsaleFinalizeAgentContractWin && npm run compileCrowdsaleContractWin && npm run compileCrowdsaleTokenContractWin && npm run compileCrowdsalePricingStrategyContractWin && node scripts/start.js", "build": "git submodule update --init --recursive --remote && cd submodules/solidity-flattener && npm install && cd ../../ && npm install && cd submodules/poa-web3-1.0 && npm install && cd ../../ && npm install --no-save submodules/poa-web3-1.0/packages/web3 && npm run generateContracts && npm run compileContracts && node scripts/build.js && cp ./build/index.html ./build/invest.html && cp ./build/index.html ./build/crowdsale.html && cp ./build/index.html ./build/manage.html", - "test": "bash ./start_testrpc.sh && cd ./submodules/poa-token-market-net-ico/ && npm install && node_modules/.bin/truffle migrate --network testrpc && node_modules/.bin/truffle test --network testrpc", + "test": "npm run test:e2e && bash ./start_testrpc.sh && cd ./submodules/poa-token-market-net-ico/ && npm install && node_modules/.bin/truffle migrate --network testrpc && node_modules/.bin/truffle test --network testrpc", "test:dapp": "jest --env=jsdom", + "test:e2e": "cd submodules/token-wizard-test-automation && npm i && npm run test1", "coveralls": "jest --env=jsdom --coverage && cat coverage/lcov.info | coveralls", "generateContracts": "npm run generateFlatSoliditySafeMathLibContract && npm run generateFlatSolidityCrowdsaleNullFinalizeAgentContract && npm run generateFlatSolidityCrowdsaleFinalizeAgentContract && npm run generateFlatSolidityCrowdsaleContract && npm run generateFlatSolidityCrowdsaleTokenContract && npm run generateFlatSolidityCrowdsalePricingStrategyContract && npm run generateFlatSolidityRegistryContract", "generateFlatSoliditySafeMathLibContract": "node $npm_package_config_combine_solidity_script $npm_package_config_tokenmarketnet_path/$npm_package_config_safe_math_lib_contract_name.sol $npm_package_config_contract_folder SafeMathLibExt", diff --git a/src/components/Common/WhitelistInputBlock.js b/src/components/Common/WhitelistInputBlock.js index bf9fc06fc..33ade245c 100644 --- a/src/components/Common/WhitelistInputBlock.js +++ b/src/components/Common/WhitelistInputBlock.js @@ -6,9 +6,10 @@ import Papa from 'papaparse' import '../../assets/stylesheets/application.css'; import { InputField } from './InputField' import { TEXT_FIELDS, VALIDATION_TYPES } from '../../utils/constants' -import { validateAddress } from '../../utils/utils' import { WhitelistItem } from './WhitelistItem' import { inject, observer } from 'mobx-react' +import { whitelistImported } from '../../utils/alerts' +import processWhitelist from '../../utils/processWhitelist' const { ADDRESS, MIN, MAX } = TEXT_FIELDS const {VALID, INVALID} = VALIDATION_TYPES; @@ -80,23 +81,16 @@ export class WhitelistInputBlock extends React.Component { this.setState(newState) } - isAddress = (address) => validateAddress(address) - isNumber = (number) => !isNaN(parseFloat(number)) - onDrop = (acceptedFiles, rejectedFiles) => { acceptedFiles.forEach(file => { Papa.parse(file, { skipEmptyLines: true, complete: results => { - results.data.forEach((row) => { - if (row.length !== 3) return - - const [addr, min, max] = row - - if (!this.isAddress(addr) || !this.isNumber(min) || !this.isNumber(max)) return - - this.props.tierStore.addWhitelistItem({ addr, min, max }, this.props.num) + const { called } = processWhitelist(results.data, item => { + this.props.tierStore.addWhitelistItem(item, this.props.num) }) + + whitelistImported(called) } }) }) diff --git a/src/components/manage/index.js b/src/components/manage/index.js index ca9ebfe57..d85017850 100644 --- a/src/components/manage/index.js +++ b/src/components/manage/index.js @@ -193,15 +193,24 @@ export class Manage extends Component { const lastCrowdsaleAddress = contractStore.crowdsale.addr.slice(-1)[0] return attachToContract(contractStore.crowdsale.abi, lastCrowdsaleAddress) - .then(crowdsaleContract => crowdsaleContract.methods.isCrowdsaleFull().call()) - .then( - (isCrowdsaleFull) => { - const { crowdsaleHasEnded, shouldDistribute, canDistribute } = this.state - const wasDistributed = shouldDistribute && !canDistribute + .then(crowdsaleContract => { + const whenIsFinalized = crowdsaleContract.methods.finalized().call() + const whenIsCrowdsaleFull = crowdsaleContract.methods.isCrowdsaleFull().call() - this.setState({ - canFinalize: (crowdsaleHasEnded || isCrowdsaleFull) && (wasDistributed || !shouldDistribute) - }) + return Promise.all([whenIsFinalized, whenIsCrowdsaleFull]) + }) + .then( + ([isFinalized, isCrowdsaleFull]) => { + if (isFinalized) { + this.setState({ canFinalize: false }) + } else { + const { crowdsaleHasEnded, shouldDistribute, canDistribute } = this.state + const wasDistributed = shouldDistribute && !canDistribute + + this.setState({ + canFinalize: (crowdsaleHasEnded || isCrowdsaleFull) && (wasDistributed || !shouldDistribute) + }) + } }, () => this.setState({ canFinalize: false }) ) @@ -293,9 +302,13 @@ export class Manage extends Component { return sendTXToContract(finalizeMethod.send(opts)) }) .then(() => { - successfulFinalizeAlert() crowdsaleStore.setSelectedProperty('finalized', true) - this.setState({ canFinalize: false }) + this.setState({ canFinalize: false }, () => { + successfulFinalizeAlert().then(() => { + this.setState({ loading: true }) + setTimeout(() => window.location.reload(), 500) + }) + }) }) .catch((err) => { console.log(err) @@ -446,7 +459,7 @@ export class Manage extends Component { tierHasStarted = (index) => { const initialTierValues = this.props.crowdsaleStore.selected.initialTiersValues[index] - return initialTierValues ? Date.now() > new Date(initialTierValues.startTime).getTime() : true + return initialTierValues && new Date(initialTierValues.startTime).getTime() < Date.now() } tierHasEnded = (index) => { @@ -458,7 +471,8 @@ export class Manage extends Component { const { formPristine, canFinalize, shouldDistribute, canDistribute, crowdsaleHasEnded, ownerCurrentUser } = this.state const { generalStore, tierStore, tokenStore, crowdsaleStore } = this.props const { address: crowdsaleAddress, finalized, updatable } = crowdsaleStore.selected - let disabled = !ownerCurrentUser || canDistribute || canFinalize || finalized + + const canEditTier = ownerCurrentUser && !canDistribute && !canFinalize && !finalized const distributeTokensStep = (
@@ -528,7 +542,7 @@ export class Manage extends Component { } const tierStartAndEndTime = (tier, index) => { - disabled = disabled || !tier.updatable || this.tierHasEnded(index) + const disabled = !canEditTier || !tier.updatable || this.tierHasEnded(index) return
{ - disabled = disabled || !tier.updatable || this.tierHasEnded(index) || this.tierHasStarted(index) + const disabled = !canEditTier || !tier.updatable || this.tierHasEnded(index) || this.tierHasStarted(index) return
{ diff --git a/src/utils/alerts.js b/src/utils/alerts.js index dcad846d9..886846269 100644 --- a/src/utils/alerts.js +++ b/src/utils/alerts.js @@ -130,7 +130,7 @@ export function warningOnFinalizeCrowdsale() { } export function successfulFinalizeAlert() { - sweetAlert2({ + return sweetAlert2({ title: "Success", html: "Congrats! You've successfully finalized the Crowdsale!", type: "success" @@ -213,3 +213,10 @@ export function skippingTransaction() { reverseButtons: true }) } +export function whitelistImported(count) { + return sweetAlert2({ + title: 'Addresses imported', + html: `${count} addresses were added to the whitelist`, + type: 'info' + }) +} diff --git a/src/utils/processWhitelist.js b/src/utils/processWhitelist.js new file mode 100644 index 000000000..6d220e2e0 --- /dev/null +++ b/src/utils/processWhitelist.js @@ -0,0 +1,29 @@ +import Web3 from 'web3' + +const isNumber = (number) => !isNaN(parseFloat(number)) + +/** + * Execute a callback with each valid whitelist item in the given list + * + * @param {Array} rows Array of whitelist items. Each element in the array has the structure `[address, min, max]`, for + * example: `['0x1234567890123456789012345678901234567890', '1', '10']` + * @param {Function} cb The function to be called with each valid item + * @returns {Object} Object with a `called` property, indicating the number of times the callback was called + */ +export default function (rows, cb) { + let called = 0 + rows.forEach((row) => { + if (row.length !== 3) return + + const [addr, min, max] = row + + if (!Web3.utils.isAddress(addr) || !isNumber(min) || !isNumber(max)) return + + cb({ addr, min, max }) + + called++ + }) + + return { called } +} + diff --git a/src/utils/processWhitelist.spec.js b/src/utils/processWhitelist.spec.js new file mode 100644 index 000000000..b03d9fa42 --- /dev/null +++ b/src/utils/processWhitelist.spec.js @@ -0,0 +1,91 @@ +import processWhitelist from './processWhitelist' + +describe('processWhitelist function', () => { + it('should call the callback for each whitelist item', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', '1', '10'], + ['0x2222222222222222222222222222222222222222', '1', '10'], + ['0x3333333333333333333333333333333333333333', '1', '10'] + ] + const cb = jest.fn() + + // When + processWhitelist(rows, cb) + + // Then + expect(cb).toHaveBeenCalledTimes(3) + expect(cb.mock.calls[0]).toEqual([{ addr: rows[0][0], min: rows[0][1], max: rows[0][2] }]) + expect(cb.mock.calls[1]).toEqual([{ addr: rows[1][0], min: rows[1][1], max: rows[1][2] }]) + expect(cb.mock.calls[2]).toEqual([{ addr: rows[2][0], min: rows[2][1], max: rows[2][2] }]) + }) + + it('should ignore items that don\t have 3 elements', () => { + // Given + const rows = [ + ['1', '10'], + ['0x2222222222222222222222222222222222222222', '10'], + ['0x3333333333333333333333333333333333333333', '1'], + ['0x4444444444444444444444444444444444444444'], + [], + ['0x4444444444444444444444444444444444444444', '1', '10', '100'], + ] + const cb = jest.fn() + + // When + processWhitelist(rows, cb) + + // Then + expect(cb).toHaveBeenCalledTimes(0) + }) + + it('should return the number of times the callback was called', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', '1', '10'], + ['0x2222222222222222222222222222222222222222', '1', '10'], + ['0x3333333333333333333333333333333333333333', '1', '10'] + ] + const cb = jest.fn() + + // When + const { called } = processWhitelist(rows, cb) + + // Then + expect(called).toBe(3) + }) + + it('should ignore invalid numbers', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', 'foo', '10'], + ['0x2222222222222222222222222222222222222222', '1', 'bar'], + ['0x3333333333333333333333333333333333333333', '', '10'], + ['0x4444444444444444444444444444444444444444', '1', ''] + ] + const cb = jest.fn() + + // When + const { called } = processWhitelist(rows, cb) + + // Then + expect(called).toBe(0) + }) + + it('should ignore invalid addresses', () => { + // Given + const rows = [ + ['0x123456789012345678901234567890123456789', '1', '10'], // 41 characters + ['0x12345678901234567890123456789012345678901', '1', '10'], // 43 characters + ['0x90F8bf6A479f320ead074411a4B0e7944Ea8c9CG', '1', '10'], // invalid character + ['0x90F8bf6A479f320ead074411a4B0e7944Ea8c9c1', '1', '10'] // invalid checksum + ] + const cb = jest.fn() + + // When + const { called } = processWhitelist(rows, cb) + + // Then + expect(called).toBe(0) + }) +}) diff --git a/src/utils/utils.js b/src/utils/utils.js index b7aae9442..cab7febe6 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -70,7 +70,7 @@ export const getStepClass = (step, activeStep) => step === activeStep ? "step-na export const validateTier = (tier) => typeof tier === 'string' && tier.length > 0 && tier.length < 30 -export const validateName = (name) => typeof name === 'string' && name.length > 0 && name.length < 30 +export const validateName = (name) => typeof name === 'string' && name.length > 0 && name.length <= 30 export const validateSupply = (supply) => isNaN(Number(supply)) === false && Number(supply) > 0 diff --git a/src/utils/utils.spec.js b/src/utils/utils.spec.js index 29e75bde7..de24df6f5 100644 --- a/src/utils/utils.spec.js +++ b/src/utils/utils.spec.js @@ -1,4 +1,4 @@ -import { countDecimalPlaces, validateTicker } from './utils' +import { countDecimalPlaces, validateName, validateTicker } from './utils' describe('countDecimalPlaces', () => { [ @@ -55,3 +55,22 @@ describe('validateTicker', () => { }) }) }) + +describe('validateName', () => { + [ + {value: '', expected: false}, + {value: 'T', expected: true}, + {value: 'MyToken', expected: true}, + {value: '123456789012345678901234567890', expected: true}, + {value: '1234567890123456789012345678901', expected: false}, + {value: 23, expected: false}, + {value: ['my', 'token'], expected: false}, + {value: { a: 1 }, expected: false}, + ].forEach(testCase => { + const action = testCase.expected ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + expect(validateName(testCase.value)).toBe(testCase.expected) + }) + }) +}) diff --git a/submodules/token-wizard-test-automation b/submodules/token-wizard-test-automation new file mode 160000 index 000000000..03c628515 --- /dev/null +++ b/submodules/token-wizard-test-automation @@ -0,0 +1 @@ +Subproject commit 03c6285158aa1fca89c19f7ce9163c39d00fe5ce