Merge branch 'master' into AddBalanceController

This commit is contained in:
Dan Finlay 2017-09-13 15:11:03 -07:00
commit c17c657693
14 changed files with 313 additions and 187 deletions

View File

@ -2,6 +2,10 @@
## Current Master ## Current Master
- Add ability to export private keys as a file.
- Add ability to export seed words as a file.
- Changed state logs to a file download than a clipboard copy.
## 3.10.0 2017-9-11 ## 3.10.0 2017-9-11
- Readded loose keyring label back into the account list. - Readded loose keyring label back into the account list.

View File

@ -4,3 +4,14 @@ machine:
test: test:
override: override:
- "npm run ci" - "npm run ci"
dependencies:
pre:
- sudo apt-get update
# get latest stable firefox
- sudo apt-get install firefox
- firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd
# get latest stable chrome
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
- sudo apt-get update
- sudo apt-get install google-chrome-stable

61
karma.conf.js Normal file
View File

@ -0,0 +1,61 @@
// Karma configuration
// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: process.cwd(),
browserConsoleLogOptions: {
terminal: false,
},
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['qunit'],
// list of files / patterns to load in the browser
files: [
'development/bundle.js',
'test/integration/jquery-3.1.0.min.js',
'test/integration/bundle.js',
{ pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true },
{ pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true },
],
proxies: {
'/images/': '/base/dist/chrome/images/',
'/fonts/': '/base/dist/chrome/fonts/',
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome', 'Firefox'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}

View File

@ -85,40 +85,47 @@ actions.update = function(stateName) {
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
const container = document.querySelector('#app-content')
// parse opts // parse opts
var store = configureStore(firstState) var store = configureStore(firstState)
// start app // start app
render( startApp()
h('.super-dev-container', [
h('button', { function startApp(){
onClick: (ev) => { const body = document.body
ev.preventDefault() const container = document.createElement('div')
store.dispatch(actions.update('terms')) container.id = 'app-content'
}, body.appendChild(container)
style: { console.log('container', container)
margin: '19px 19px 0px 19px',
},
}, 'Reset State'),
h(Selector, { actions, selectedKey: selectedView, states, store }), render(
h('.super-dev-container', [
h('.mock-app-root', { h('button', {
style: { onClick: (ev) => {
height: '500px', ev.preventDefault()
width: '360px', store.dispatch(actions.update('terms'))
boxShadow: 'grey 0px 2px 9px', },
margin: '20px', style: {
}, margin: '19px 19px 0px 19px',
}, [ },
h(Root, { }, 'Reset State'),
store: store,
}),
]),
] h(Selector, { actions, selectedKey: selectedView, states, store }),
), container)
h('.mock-app-root', {
style: {
height: '500px',
width: '360px',
boxShadow: 'grey 0px 2px 9px',
margin: '20px',
},
}, [
h(Root, {
store: store,
}),
]),
]
), container)
}

View File

@ -12,8 +12,8 @@
"test": "npm run lint && npm run test-unit && npm run test-integration", "test": "npm run lint && npm run test-unit && npm run test-integration",
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"single-test": "METAMASK_ENV=test mocha --require test/helper.js", "single-test": "METAMASK_ENV=test mocha --require test/helper.js",
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "test-integration": "npm run buildMock && npm run buildCiUnits && karma start",
"test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls", "test-coverage": "nyc npm run test-unit && if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"ci": "npm run lint && npm run test-coverage && npm run test-integration", "ci": "npm run lint && npm run test-coverage && npm run test-integration",
"lint": "gulp lint", "lint": "gulp lint",
"buildCiUnits": "node test/integration/index.js", "buildCiUnits": "node test/integration/index.js",
@ -22,7 +22,6 @@
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js", "buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
"testem": "npm run buildMock && testem",
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"generateNotice": "node notices/notice-generator.js", "generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js", "deleteNotice": "node notices/notice-delete.js",
@ -138,7 +137,7 @@
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.24.1", "babel-core": "^6.24.1",
"babel-eslint": "^7.2.3", "babel-eslint": "^8.0.0",
"babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0", "babel-polyfill": "^6.23.0",
@ -172,6 +171,11 @@
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1", "jshint-stylish": "~2.2.1",
"json-rpc-engine": "^3.0.1", "json-rpc-engine": "^3.0.1",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1",
"karma-firefox-launcher": "^1.0.1",
"karma-qunit": "^1.2.1",
"lodash.assign": "^4.0.6", "lodash.assign": "^4.0.6",
"mocha": "^3.4.2", "mocha": "^3.4.2",
"mocha-eslint": "^4.0.0", "mocha-eslint": "^4.0.0",

View File

@ -1,7 +0,0 @@
function wait(time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
})
}

View File

@ -1,5 +1,6 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const pump = require('pump')
const browserify = require('browserify') const browserify = require('browserify')
const tests = fs.readdirSync(path.join(__dirname, 'lib')) const tests = fs.readdirSync(path.join(__dirname, 'lib'))
const bundlePath = path.join(__dirname, 'bundle.js') const bundlePath = path.join(__dirname, 'bundle.js')
@ -9,11 +10,17 @@ const b = browserify()
const writeStream = fs.createWriteStream(bundlePath) const writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function (fileName) { tests.forEach(function (fileName) {
b.add(path.join(__dirname, 'lib', fileName)) const filePath = path.join(__dirname, 'lib', fileName)
console.log(`bundling test "${filePath}"`)
b.add(filePath)
}) })
b.bundle() pump(
.pipe(writeStream) b.bundle(),
.on('error', (err) => { writeStream,
throw err (err) => {
}) if (err) throw err
console.log(`Integration test build completed: "${bundlePath}"`)
process.exit(0)
}
)

View File

@ -2,125 +2,137 @@ const PASSWORD = 'password123'
QUnit.module('first time usage') QUnit.module('first time usage')
QUnit.test('render init screen', function (assert) { QUnit.test('render init screen', (assert) => {
var done = assert.async() const done = assert.async()
let app runFirstTimeUsageTest(assert).then(done).catch((err) => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
wait().then(function() {
app = $('iframe').contents().find('#app-content .mock-app-root')
const recurseNotices = function () {
let button = app.find('button')
if (button.html() === 'Accept') {
let termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
return wait().then(() => {
button.click()
return wait()
}).then(() => {
return recurseNotices()
})
} else {
return wait()
}
}
return recurseNotices()
}).then(function() {
// Scroll through terms
var title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
// enter password
var pwBox = app.find('#password-box')[0]
var confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
return wait()
}).then(function() {
// create vault
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1500)
}).then(function() {
var created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button
var button = app.find('button')[0]
assert.ok(button, 'button present')
button.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
var sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
return wait()
}).then(function() {
var sandwich = app.find('.menu-droppo')[0]
var children = sandwich.children
var lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
return wait(1000)
}).then(function() {
var pwBox = app.find('#password-box')[0]
pwBox.value = PASSWORD
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded again.')
return wait()
}).then(function (){
var qrButton = app.find('.fa.fa-ellipsis-h')[0] // open account settings dropdown
qrButton.click()
return wait(1000)
}).then(function (){
var qrButton = app.find('.dropdown-menu-item')[1] // qr code item
qrButton.click()
return wait(1000)
}).then(function (){
var qrHeader = app.find('.qr-header')[0]
var qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
networkMenu.click()
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
var children = networkMenu.children
children.length[3]
assert.ok(children, 'All network options present')
done() done()
}) })
}) })
// QUnit.testDone(({ module, name, total, passed, failed, skipped, todo, runtime }) => {
// if (failed > 0) {
// const app = $('iframe').contents()[0].documentElement
// console.warn('Test failures - dumping DOM:')
// console.log(app.innerHTML)
// }
// })
async function runFirstTimeUsageTest(assert, done) {
await timeout()
const app = $('#app-content .mock-app-root')
// recurse notices
while (true) {
const button = app.find('button')
if (button.html() === 'Accept') {
// still notices to accept
const termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
await timeout()
button.click()
await timeout()
} else {
// exit loop
break
}
}
await timeout()
// Scroll through terms
const title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
// enter password
const pwBox = app.find('#password-box')[0]
const confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
await timeout()
// create vault
const createButton = app.find('button.primary')[0]
createButton.click()
await timeout(1500)
const created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button
const button = app.find('button')[0]
assert.ok(button, 'button present')
button.click()
await timeout(1000)
const detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
const sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
await timeout()
const menu = app.find('.menu-droppo')[0]
const children = menu.children
const lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
await timeout(1000)
const pwBox2 = app.find('#password-box')[0]
pwBox2.value = PASSWORD
const createButton2 = app.find('button.primary')[0]
createButton2.click()
await timeout(1000)
const detail2 = app.find('.account-detail-section')[0]
assert.ok(detail2, 'Account detail section loaded again.')
await timeout()
// open account settings dropdown
const qrButton = app.find('.fa.fa-ellipsis-h')[0]
qrButton.click()
await timeout(1000)
// qr code item
const qrButton2 = app.find('.dropdown-menu-item')[1]
qrButton2.click()
await timeout(1000)
const qrHeader = app.find('.qr-header')[0]
const qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
await timeout()
const networkMenu = app.find('.network-indicator')[0]
networkMenu.click()
await timeout()
const networkMenu2 = app.find('.network-indicator')[0]
const children2 = networkMenu2.children
children2.length[3]
assert.ok(children2, 'All network options present')
}
function timeout(time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
})
}

View File

@ -1,10 +0,0 @@
launch_in_dev:
- Chrome
- Firefox
launch_in_ci:
- Chrome
- Firefox
framework:
- qunit
before_tests: "npm run buildCiUnits"
test_page: "test/integration/index.html"

View File

@ -1,6 +1,7 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const exportAsFile = require('../util').exportAsFile
const copyToClipboard = require('copy-to-clipboard') const copyToClipboard = require('copy-to-clipboard')
const actions = require('../actions') const actions = require('../actions')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
@ -20,20 +21,21 @@ function mapStateToProps (state) {
} }
ExportAccountView.prototype.render = function () { ExportAccountView.prototype.render = function () {
var state = this.props const state = this.props
var accountDetail = state.accountDetail const accountDetail = state.accountDetail
const nickname = state.identities[state.address].name
if (!accountDetail) return h('div') if (!accountDetail) return h('div')
var accountExport = accountDetail.accountExport const accountExport = accountDetail.accountExport
var notExporting = accountExport === 'none' const notExporting = accountExport === 'none'
var exportRequested = accountExport === 'requested' const exportRequested = accountExport === 'requested'
var accountExported = accountExport === 'completed' const accountExported = accountExport === 'completed'
if (notExporting) return h('div') if (notExporting) return h('div')
if (exportRequested) { if (exportRequested) {
var warning = `Export private keys at your own risk.` const warning = `Export private keys at your own risk.`
return ( return (
h('div', { h('div', {
style: { style: {
@ -89,6 +91,8 @@ ExportAccountView.prototype.render = function () {
} }
if (accountExported) { if (accountExported) {
const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
return h('div.privateKey', { return h('div.privateKey', {
style: { style: {
margin: '0 20px', margin: '0 20px',
@ -105,10 +109,16 @@ ExportAccountView.prototype.render = function () {
onClick: function (event) { onClick: function (event) {
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
}, },
}, ethUtil.stripHexPrefix(accountDetail.privateKey)), }, plainKey),
h('button', { h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
}, 'Done'), }, 'Done'),
h('button', {
style: {
marginLeft: '10px',
},
onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey),
}, 'Save as File'),
]) ])
} }
} }
@ -117,6 +127,6 @@ ExportAccountView.prototype.onExportKeyPress = function (event) {
if (event.key !== 'Enter') return if (event.key !== 'Enter') return
event.preventDefault() event.preventDefault()
var input = document.getElementById('exportAccount').value const input = document.getElementById('exportAccount').value
this.props.dispatch(actions.exportAccount(input, this.props.address)) this.props.dispatch(actions.exportAccount(input, this.props.address))
} }

View File

@ -5,7 +5,8 @@ const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const currencies = require('./conversion.json').rows const currencies = require('./conversion.json').rows
const validUrl = require('valid-url') const validUrl = require('valid-url')
const copyToClipboard = require('copy-to-clipboard') const exportAsFile = require('./util').exportAsFile
module.exports = connect(mapStateToProps)(ConfigScreen) module.exports = connect(mapStateToProps)(ConfigScreen)
@ -110,9 +111,9 @@ ConfigScreen.prototype.render = function () {
alignSelf: 'center', alignSelf: 'center',
}, },
onClick (event) { onClick (event) {
copyToClipboard(window.logState()) exportAsFile('MetaMask State Logs', window.logState())
}, },
}, 'Copy State Logs'), }, 'Download State Logs'),
]), ]),
h('hr.horizontal-line'), h('hr.horizontal-line'),

View File

@ -3,6 +3,7 @@ const Component = require('react').Component
const connect = require('react-redux').connect const connect = require('react-redux').connect
const h = require('react-hyperscript') const h = require('react-hyperscript')
const actions = require('../../actions') const actions = require('../../actions')
const exportAsFile = require('../../util').exportAsFile
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
@ -65,8 +66,17 @@ CreateVaultCompleteScreen.prototype.render = function () {
style: { style: {
margin: '24px', margin: '24px',
fontSize: '0.9em', fontSize: '0.9em',
marginBottom: '10px',
}, },
}, 'I\'ve copied it somewhere safe'), }, 'I\'ve copied it somewhere safe'),
h('button.primary', {
onClick: () => exportAsFile(`MetaMask Seed Words`, seed),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
]) ])
) )
} }

View File

@ -36,6 +36,7 @@ module.exports = {
valueTable: valueTable, valueTable: valueTable,
bnTable: bnTable, bnTable: bnTable,
isHex: isHex, isHex: isHex,
exportAsFile: exportAsFile,
} }
function valuesFor (obj) { function valuesFor (obj) {
@ -215,3 +216,18 @@ function readableDate (ms) {
function isHex (str) { function isHex (str) {
return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/))
} }
function exportAsFile (filename, data) {
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
const blob = new Blob([data], {type: 'text/csv'})
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename)
} else {
const elem = window.document.createElement('a')
elem.href = window.URL.createObjectURL(blob)
elem.download = filename
document.body.appendChild(elem)
elem.click()
document.body.removeChild(elem)
}
}