Merge pull request #4408 from MetaMask/v4.7.0rc2
Version 4.7.0 - rc2feature/default_network_editable
commit
dc5477be3c
@ -1,3 +1,6 @@ |
||||
{ |
||||
"exceptions": ["https://nodesecurity.io/advisories/566"] |
||||
"exceptions": [ |
||||
"https://nodesecurity.io/advisories/566", |
||||
"https://nodesecurity.io/advisories/157" |
||||
] |
||||
} |
||||
|
@ -0,0 +1,10 @@ |
||||
# Storybook |
||||
We're currently using [Storybook](https://storybook.js.org/) as part of our design system. To run Storybook and test some of our UI components, clone the repo and run the following: |
||||
``` |
||||
npm install |
||||
npm run storybook |
||||
``` |
||||
You should then see: |
||||
> info Storybook started on => http://localhost:6006/ |
||||
|
||||
In your browser, navigate to http://localhost:6006/ to see the Storybook application. From here, you'll be able to easily view components and even modify some of their properties. |
@ -0,0 +1,2 @@ |
||||
import '@storybook/addon-knobs/register' |
||||
import '@storybook/addon-actions/register' |
@ -0,0 +1,11 @@ |
||||
import { configure } from '@storybook/react' |
||||
import '../ui/app/css/index.scss' |
||||
|
||||
const req = require.context('../ui/app/components', true, /\.stories\.js$/) |
||||
|
||||
function loadStories () { |
||||
require('./decorators') |
||||
req.keys().forEach((filename) => req(filename)) |
||||
} |
||||
|
||||
configure(loadStories, module) |
@ -0,0 +1,21 @@ |
||||
import React from 'react' |
||||
import { addDecorator } from '@storybook/react' |
||||
import { withInfo } from '@storybook/addon-info' |
||||
import { withKnobs } from '@storybook/addon-knobs/react' |
||||
|
||||
const styles = { |
||||
height: '100vh', |
||||
display: 'flex', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
} |
||||
|
||||
const CenterDecorator = story => ( |
||||
<div style={styles}> |
||||
{ story() } |
||||
</div> |
||||
) |
||||
|
||||
addDecorator((story, context) => withInfo()(story)(context)) |
||||
addDecorator(withKnobs) |
||||
addDecorator(CenterDecorator) |
@ -0,0 +1,37 @@ |
||||
const path = require('path') |
||||
|
||||
module.exports = { |
||||
module: { |
||||
rules: [ |
||||
{ |
||||
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=\d+\.\d+\.\d+)?$/, |
||||
loaders: [{ |
||||
loader: 'file-loader', |
||||
options: { |
||||
name: '[name].[ext]', |
||||
outputPath: 'fonts/', |
||||
}, |
||||
}], |
||||
}, |
||||
{ |
||||
test: /\.scss$/, |
||||
loaders: [ |
||||
'style-loader', |
||||
'css-loader', |
||||
'resolve-url-loader', |
||||
{ |
||||
loader: 'sass-loader', |
||||
options: { |
||||
sourceMap: true, |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
resolve: { |
||||
alias: { |
||||
'./fonts/Font_Awesome': path.resolve(__dirname, '../fonts/Font_Awesome'), |
||||
}, |
||||
}, |
||||
} |
@ -0,0 +1,14 @@ |
||||
# MetaMask Philosophy |
||||
|
||||
## Mission |
||||
|
||||
Making it safe and easy for the most people to use the decentralized web to the greatest degree that is empowering to them. |
||||
|
||||
## Vision |
||||
|
||||
To realize the highest goals achievable for the human race with the twin powers of peer to peer networks and cryptography. To empower users to hold and use their own keys on these new networks as securely and intelligibly as possible, enabling a new world of peer to peer agreements and economies, in hopes that we may collectively overcome the many great problems that we face together, through the power of strong cooperation. |
||||
|
||||
## Strategy |
||||
|
||||
We provide software for users to manage accounts, for sites to easily propose actions to users, and for users to coherently review actions before approving them. We build on this rapidly evolving set of protocols with the goal of empowering the most people to the greatest degree, and aspire to continuously evolve our offering to pursue that goal. |
||||
|
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 5.5 KiB |
@ -0,0 +1,66 @@ |
||||
const log = require('loglevel') |
||||
|
||||
/** |
||||
* JSON-RPC error object |
||||
* |
||||
* @typedef {Object} RpcError |
||||
* @property {number} code - Indicates the error type that occurred |
||||
* @property {Object} [data] - Contains additional information about the error |
||||
* @property {string} [message] - Short description of the error |
||||
*/ |
||||
|
||||
/** |
||||
* Middleware configuration object |
||||
* |
||||
* @typedef {Object} MiddlewareConfig |
||||
* @property {boolean} [override] - Use RPC_ERRORS message in place of provider message |
||||
*/ |
||||
|
||||
/** |
||||
* Map of standard and non-standard RPC error codes to messages |
||||
*/ |
||||
const RPC_ERRORS = { |
||||
1: 'An unauthorized action was attempted.', |
||||
2: 'A disallowed action was attempted.', |
||||
3: 'An execution error occurred.', |
||||
[-32600]: 'The JSON sent is not a valid Request object.', |
||||
[-32601]: 'The method does not exist / is not available.', |
||||
[-32602]: 'Invalid method parameter(s).', |
||||
[-32603]: 'Internal JSON-RPC error.', |
||||
[-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', |
||||
internal: 'Internal server error.', |
||||
unknown: 'Unknown JSON-RPC error.', |
||||
} |
||||
|
||||
/** |
||||
* Modifies a JSON-RPC error object in-place to add a human-readable message, |
||||
* optionally overriding any provider-supplied message |
||||
* |
||||
* @param {RpcError} error - JSON-RPC error object |
||||
* @param {boolean} override - Use RPC_ERRORS message in place of provider message |
||||
*/ |
||||
function sanitizeRPCError (error, override) { |
||||
if (error.message && !override) { return error } |
||||
const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code] |
||||
error.message = message || RPC_ERRORS.unknown |
||||
} |
||||
|
||||
/** |
||||
* json-rpc-engine middleware that both logs standard and non-standard error |
||||
* messages and ends middleware stack traversal if an error is encountered |
||||
* |
||||
* @param {MiddlewareConfig} [config={override:true}] - Middleware configuration |
||||
* @returns {Function} json-rpc-engine middleware function |
||||
*/ |
||||
function createErrorMiddleware ({ override = true } = {}) { |
||||
return (req, res, next) => { |
||||
next(done => { |
||||
const { error } = res |
||||
if (!error) { return done() } |
||||
sanitizeRPCError(error) |
||||
log.error(`MetaMask - RPC Error: ${error.message}`, error) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
module.exports = createErrorMiddleware |
@ -0,0 +1,47 @@ |
||||
const version = 26 |
||||
|
||||
/* |
||||
|
||||
This migration moves the identities stored in the KeyringController |
||||
into the PreferencesController |
||||
|
||||
*/ |
||||
|
||||
const clone = require('clone') |
||||
|
||||
module.exports = { |
||||
version, |
||||
migrate (originalVersionedData) { |
||||
const versionedData = clone(originalVersionedData) |
||||
versionedData.meta.version = version |
||||
try { |
||||
const state = versionedData.data |
||||
versionedData.data = transformState(state) |
||||
} catch (err) { |
||||
console.warn(`MetaMask Migration #${version}` + err.stack) |
||||
return Promise.reject(err) |
||||
} |
||||
return Promise.resolve(versionedData) |
||||
}, |
||||
} |
||||
|
||||
function transformState (state) { |
||||
if (!state.KeyringController || !state.PreferencesController) { |
||||
return |
||||
} |
||||
|
||||
if (!state.KeyringController.walletNicknames) { |
||||
return state |
||||
} |
||||
|
||||
state.PreferencesController.identities = Object.keys(state.KeyringController.walletNicknames) |
||||
.reduce((identities, address) => { |
||||
identities[address] = { |
||||
name: state.KeyringController.walletNicknames[address], |
||||
address, |
||||
} |
||||
return identities |
||||
}, {}) |
||||
delete state.KeyringController.walletNicknames |
||||
return state |
||||
} |
@ -0,0 +1,96 @@ |
||||
# Send screen QA checklist: |
||||
|
||||
This checklist can be to guide QA of the send screen. It can also be used to guide e2e tests for the send screen. |
||||
|
||||
Once all of these are QA verified on master, resolutions to any bugs related to the send screen should include and update to this list. |
||||
|
||||
Additional features or functionality on the send screen should include an update to this list. |
||||
|
||||
## Send Eth mode |
||||
- [ ] **Header** _It should:_ |
||||
- [ ] have title "Send ETH" |
||||
- [ ] have sub title "Only send ETH to an Ethereum address." |
||||
- [ ] return user to main screen when top right X is clicked |
||||
- [ ] **From row** _It should:_ |
||||
- [ ] show the currently selected account by default |
||||
- [ ] show a dropdown with all of the users accounts |
||||
- [ ] contain the following info for each account: identicon, account name, balance in ETH, balance in current currency |
||||
- [ ] change the account selected in the dropdown (but not the app-wide selected account) when one in the dropdown is clicked |
||||
- [ ] close the dropdown, without changing the dropdown selected account, when the dropdown is open and then a click happens outside it |
||||
- [ ] **To row** _It should:_ |
||||
- [ ] Show a placeholder with the text 'Recipient Address' by default |
||||
- [ ] Show, when clicked, a dropdown list of all 'to accounts': the users accounts, plus any other accounts they have previously sent to |
||||
- [ ] Show account address, and account name if it exists, of each item in the dropdown list |
||||
- [ ] Show a dropdown list of all to accounts (see above) whose address matches an address currently being typed in |
||||
- [ ] Set the input text to the address of an account clicked in the dropdown list, and also hide the dropdown |
||||
- [ ] Hide the dropdown without changing what is in the input if the user clicks outside the dropdown list while it is open |
||||
- [ ] Select the text in the input (i.e. the address) if an address is displayed and then clicked |
||||
- [ ] Show a 'required' error if the dropdown is opened but no account is selected |
||||
- [ ] Show an 'invalid address' error if text is entered in the input that cannot be a valid hex address or ens address |
||||
- [ ] Support ens names. (enter dinodan.eth on mainnet) After entering the plain text address, the hex address should appear in the input with a green checkmark beside |
||||
- [ ] Should show a 'no such address' error if a non-existent ens address is entered |
||||
- [ ] **Amount row** _It should:_ |
||||
- [ ] allow user to enter any rational number >= 0 |
||||
- [ ] allow user to copy and paste into the field |
||||
- [ ] show an insufficient funds error if an amount > balance - gas fee |
||||
- [ ] display 'ETH' after the number amount. The position of 'ETH' should change as the length of the input amount text changes |
||||
- [ ] display the value of the amount of ETH in the current currency, formatted in that currency |
||||
- [ ] show a 'max' but if amount < balance - gas fee |
||||
- [ ] show no max button or error if amount === balance - gas fee |
||||
- [ ] set the amount to balance - gas fee if the 'max' button is clicked |
||||
- [ ] **Gas Fee Display row** _It should:_ |
||||
- [ ] Default to the fee given by the estimated gas price |
||||
- [ ] display the fee in ETH and the current currency |
||||
- [ ] update when changes are made using the customize gas modal |
||||
- [ ] **Cancel button** _It should:_ |
||||
- [ ] Take the user back to the main screen |
||||
- [ ] **submit button** _It should:_ |
||||
- [ ] be disabled if no recipient address is provided or if any field is in error |
||||
- [ ] sign a transaction with the info in the above form, and display the details of that transaction on the confirm screen |
||||
|
||||
## Send token mode |
||||
- [ ] **Header** _It should:_ |
||||
- [ ] have title "Send Tokens" |
||||
- [ ] have sub title "Only send [token symbol] to an Ethereum address." |
||||
- [ ] return user to main screen when top right X is clicked |
||||
- [ ] **From row** _It should:_ |
||||
- [ ] Behave the same as 'Send ETH mode' (see above) |
||||
- [ ] **To row** _It should:_ |
||||
- [ ] Behave the same as 'Send ETH mode' (see above) |
||||
- [ ] **Amount row** _It should:_ |
||||
- [ ] allow user to enter any rational number >= 0 |
||||
- [ ] allow user to copy and paste into the field |
||||
- [ ] show an 'insufficient tokens' error if an amount > token balance |
||||
- [ ] show an 'insufficient funds' error if an gas fee > eth balance |
||||
- [ ] display [token symbol] after the number amount. The position of [token symbol] should change as the length of the input amount text changes |
||||
- [ ] display the value of the amount of tokens in the current currency, formatted in that currency |
||||
- [ ] show a 'max' but if amount < token balance |
||||
- [ ] show no max button or error if amount === token balance |
||||
- [ ] set the amount to token balance if the 'max' button is clicked |
||||
- [ ] **Gas Fee Display row** _It should:_ |
||||
- [ ] Behave the same as 'Send ETH mode' (see above) |
||||
- [ ] **Cancel button** _It should:_ |
||||
- [ ] Take the user back to the main screen |
||||
- [ ] **submit button** _It should:_ |
||||
- [ ] be disabled if no recipient address is provided or if any field is in error |
||||
- [ ] sign a token transaction with the info in the above form, and display the details of that transaction on the confirm screen |
||||
|
||||
## Edit send Eth mode |
||||
- [ ] Say 'Editing transaction' in the header |
||||
- [ ] display a button to go back to the confirmation screen without applying update |
||||
- [ ] say 'update transaction' on the submit button |
||||
- [ ] update the existing transaction, instead of signing a new one, when clicking the submit button |
||||
- [ ] Otherwise, behave the same as 'Send ETH mode' (see above) |
||||
|
||||
## Edit send token mode |
||||
- [ ] Behave the same as 'Edit send Eth mode' (see above) |
||||
|
||||
## Specific cases to test |
||||
- [ ] Send eth to a hex address |
||||
- [ ] Send eth to an ENS address |
||||
- [ ] Donate to the faucet at https://faucet.metamask.io/ and edit the transaction before confirming |
||||
- [ ] Send a token that is available on the 'Add Token' screen search to a hex address |
||||
- [ ] Create a custom token at https://tokenfactory.surge.sh/ and send it to a hex address |
||||
- [ ] Send a token to an ENS address |
||||
- [ ] Create a token transaction using https://tokenfactory.surge.sh/#/, and edit the transaction before confirming |
||||
- [ ] Send each of MKR, EOS and ICON using myetherwallet, and edit the transaction before confirming |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,406 @@ |
||||
const path = require('path') |
||||
const assert = require('assert') |
||||
const webdriver = require('selenium-webdriver') |
||||
const { By, Key } = webdriver |
||||
const { |
||||
delay, |
||||
buildChromeWebDriver, |
||||
buildFirefoxWebdriver, |
||||
installWebExt, |
||||
getExtensionIdChrome, |
||||
getExtensionIdFirefox, |
||||
} = require('../func') |
||||
const { |
||||
checkBrowserForConsoleErrors, |
||||
loadExtension, |
||||
verboseReportOnFailure, |
||||
} = require('./helpers') |
||||
|
||||
describe('Using MetaMask with an existing account', function () { |
||||
let extensionId |
||||
let driver |
||||
let tokenAddress |
||||
|
||||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' |
||||
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC' |
||||
const regularDelayMs = 1000 |
||||
const largeDelayMs = regularDelayMs * 2 |
||||
const waitingNewPageDelayMs = regularDelayMs * 10 |
||||
|
||||
this.timeout(0) |
||||
this.bail(true) |
||||
|
||||
before(async function () { |
||||
switch (process.env.SELENIUM_BROWSER) { |
||||
case 'chrome': { |
||||
const extensionPath = path.resolve('dist/chrome') |
||||
driver = buildChromeWebDriver(extensionPath) |
||||
extensionId = await getExtensionIdChrome(driver) |
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`) |
||||
await delay(regularDelayMs) |
||||
break |
||||
} |
||||
case 'firefox': { |
||||
const extensionPath = path.resolve('dist/firefox') |
||||
driver = buildFirefoxWebdriver() |
||||
await installWebExt(driver, extensionPath) |
||||
await delay(regularDelayMs) |
||||
extensionId = await getExtensionIdFirefox(driver) |
||||
await driver.get(`moz-extension://${extensionId}/popup.html`) |
||||
await delay(regularDelayMs) |
||||
break |
||||
} |
||||
} |
||||
}) |
||||
|
||||
afterEach(async function () { |
||||
if (process.env.SELENIUM_BROWSER === 'chrome') { |
||||
const errors = await checkBrowserForConsoleErrors(driver) |
||||
if (errors.length) { |
||||
const errorReports = errors.map(err => err.message) |
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}` |
||||
console.error(new Error(errorMessage)) |
||||
} |
||||
} |
||||
if (this.currentTest.state === 'failed') { |
||||
await verboseReportOnFailure(driver, this.currentTest) |
||||
} |
||||
}) |
||||
|
||||
after(async function () { |
||||
await driver.quit() |
||||
}) |
||||
|
||||
describe('New UI setup', async function () { |
||||
it('switches to first tab', async function () { |
||||
const [firstTab] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(firstTab) |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('use the local network', async function () { |
||||
const [networkSelector] = await driver.findElements(By.css('#network_component')) |
||||
await networkSelector.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`)) |
||||
await localhost.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('selects the new UI option', async () => { |
||||
const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]")) |
||||
await button.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
// Close all other tabs
|
||||
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(oldUi) |
||||
await driver.close() |
||||
await driver.switchTo().window(infoPage) |
||||
await driver.close() |
||||
await driver.switchTo().window(newUi) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button')) |
||||
await continueBtn.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('First time flow starting from an existing seed phrase', () => { |
||||
it('imports a seed phrase', async () => { |
||||
const [seedPhrase] = await driver.findElements(By.xpath(`//a[contains(text(), 'Import with seed phrase')]`)) |
||||
await seedPhrase.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [seedTextArea] = await driver.findElements(By.css('textarea.import-account__secret-phrase')) |
||||
await seedTextArea.sendKeys(testSeedPhrase) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [password] = await driver.findElements(By.id('password')) |
||||
await password.sendKeys('correct horse battery staple') |
||||
const [confirmPassword] = await driver.findElements(By.id('confirm-password')) |
||||
confirmPassword.sendKeys('correct horse battery staple') |
||||
|
||||
const [importButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Import')]`)) |
||||
await importButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks through the privacy notice', async () => { |
||||
const [nextScreen] = await driver.findElements(By.css('.tou button')) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled() |
||||
assert.equal(canClickThrough, false, 'disabled continue button') |
||||
const element = await driver.findElement(By.linkText('Attributions')) |
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', element) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [acceptTos] = await driver.findElements(By.css('.tou button')) |
||||
await acceptTos.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Show account information', () => { |
||||
it('shows the correct account address', async () => { |
||||
await driver.findElement(By.css('.wallet-view__details-button')).click() |
||||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [address] = await driver.findElements(By.css('input.qr-ellip-address')) |
||||
assert.equal(await address.getAttribute('value'), testAddress) |
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()") |
||||
await delay(largeDelayMs) |
||||
}) |
||||
|
||||
it('shows a QR code for the account', async () => { |
||||
await driver.findElement(By.css('.wallet-view__details-button')).click() |
||||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()") |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Log out and log back in', () => { |
||||
it('logs out of the account', async () => { |
||||
await driver.findElement(By.css('.account-menu__icon')).click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) |
||||
assert.equal(await logoutButton.getText(), 'Log out') |
||||
await logoutButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('accepts the account password after lock', async () => { |
||||
await driver.findElement(By.id('password')).sendKeys('correct horse battery staple') |
||||
await driver.findElement(By.id('password')).sendKeys(Key.ENTER) |
||||
await delay(largeDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add an account', () => { |
||||
it('choose Create Account from the account menu', async () => { |
||||
await driver.findElement(By.css('.account-menu__icon')).click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`)) |
||||
await createAccount.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('set account name', async () => { |
||||
const [accountName] = await driver.findElements(By.css('.new-account-create-form input')) |
||||
await accountName.sendKeys('2nd account') |
||||
await delay(regularDelayMs) |
||||
|
||||
const [createButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`)) |
||||
await createButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('should show the correct account name', async () => { |
||||
const [accountName] = await driver.findElements(By.css('.account-name')) |
||||
assert.equal(await accountName.getText(), '2nd account') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Switch back to original account', () => { |
||||
it('chooses the original account from the account menu', async () => { |
||||
await driver.findElement(By.css('.account-menu__icon')).click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [originalAccountMenuItem] = await driver.findElements(By.css('.account-menu__name')) |
||||
await originalAccountMenuItem.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Send ETH from inside MetaMask', () => { |
||||
it('starts to send a transaction', async function () { |
||||
const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`)) |
||||
await sendButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]')) |
||||
const [inputAmount] = await driver.findElements(By.css('.currency-display__input')) |
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') |
||||
await inputAmount.sendKeys('1') |
||||
|
||||
// Set the gas limit
|
||||
const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button')) |
||||
await configureGas.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`)) |
||||
await save.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
// Continue to next screen
|
||||
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('confirms the transaction', async function () { |
||||
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('finds the transaction in the transactions list', async function () { |
||||
const transactions = await driver.findElements(By.css('.tx-list-item')) |
||||
assert.equal(transactions.length, 1) |
||||
|
||||
const txValues = await driver.findElements(By.css('.tx-list-value')) |
||||
assert.equal(txValues.length, 1) |
||||
assert.equal(await txValues[0].getText(), '1 ETH') |
||||
}) |
||||
}) |
||||
|
||||
describe('Send ETH from Faucet', () => { |
||||
it('starts a send transaction inside Faucet', async () => { |
||||
await driver.executeScript('window.open("https://faucet.metamask.io")') |
||||
await delay(waitingNewPageDelayMs) |
||||
|
||||
const [extension, faucet] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(faucet) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`)) |
||||
await send1eth.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(extension) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(faucet) |
||||
await delay(regularDelayMs) |
||||
await driver.close() |
||||
await delay(regularDelayMs) |
||||
await driver.switchTo().window(extension) |
||||
await delay(regularDelayMs) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add existing token using search', () => { |
||||
it('clicks on the Add Token button', async () => { |
||||
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) |
||||
await addToken.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('picks an existing token', async () => { |
||||
const [tokenSearch] = await driver.findElements(By.css('input.add-token__input')) |
||||
await tokenSearch.sendKeys('BAT') |
||||
await delay(regularDelayMs) |
||||
|
||||
const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]")) |
||||
await token.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) |
||||
await addTokens.click() |
||||
await delay(largeDelayMs) |
||||
}) |
||||
|
||||
it('renders the balance for the new token', async () => { |
||||
const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount')) |
||||
const tokenAmount = await balance.getText() |
||||
assert.equal(tokenAmount, '0BAT') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add a custom token from TokenFactory', () => { |
||||
it('creates a new token', async () => { |
||||
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') |
||||
await delay(waitingNewPageDelayMs) |
||||
|
||||
const [extension, tokenFactory] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(tokenFactory) |
||||
const [ |
||||
totalSupply, |
||||
tokenName, |
||||
tokenDecimal, |
||||
tokenSymbol, |
||||
] = await driver.findElements(By.css('input')) |
||||
|
||||
await totalSupply.sendKeys('100') |
||||
await tokenName.sendKeys('Test') |
||||
await tokenDecimal.sendKeys('0') |
||||
await tokenSymbol.sendKeys('TST') |
||||
|
||||
const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`)) |
||||
await createToken.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(extension) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(tokenFactory) |
||||
await delay(regularDelayMs) |
||||
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) |
||||
tokenAddress = await tokenContactAddress.getText() |
||||
await driver.close() |
||||
await driver.switchTo().window(extension) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks on the Add Token button', async () => { |
||||
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) |
||||
await addToken.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('picks the new Test token', async () => { |
||||
const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]")) |
||||
await addCustomToken.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input')) |
||||
await newTokenAddress.sendKeys(tokenAddress) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) |
||||
await addTokens.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('renders the balance for the new token', async () => { |
||||
const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount')) |
||||
const tokenAmount = await balance.getText() |
||||
assert.equal(tokenAmount, '100TST') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,55 @@ |
||||
const fs = require('fs') |
||||
const mkdirp = require('mkdirp') |
||||
const pify = require('pify') |
||||
|
||||
module.exports = { |
||||
checkBrowserForConsoleErrors, |
||||
loadExtension, |
||||
verboseReportOnFailure, |
||||
} |
||||
|
||||
async function loadExtension (driver, extensionId) { |
||||
switch (process.env.SELENIUM_BROWSER) { |
||||
case 'chrome': { |
||||
await driver.get(`chrome-extension://${extensionId}/home.html`) |
||||
break |
||||
} |
||||
case 'firefox': { |
||||
await driver.get(`moz-extension://${extensionId}/home.html`) |
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
async function checkBrowserForConsoleErrors (driver) { |
||||
const ignoredLogTypes = ['WARNING'] |
||||
const ignoredErrorMessages = [ |
||||
// React throws error warnings on "dataset", but still sets the data-* properties correctly
|
||||
'Warning: Unknown prop `dataset` on ', |
||||
// Third-party Favicon 404s show up as errors
|
||||
'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)', |
||||
// React Development build - known issue blocked by test build sys
|
||||
'Warning: It looks like you\'re using a minified copy of the development build of React.', |
||||
// Redux Development build - known issue blocked by test build sys
|
||||
'This means that you are running a slower development build of Redux.', |
||||
] |
||||
const browserLogs = await driver.manage().logs().get('browser') |
||||
const errorEntries = browserLogs.filter(entry => !ignoredLogTypes.includes(entry.level.toString())) |
||||
const errorObjects = errorEntries.map(entry => entry.toJSON()) |
||||
return errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message))) |
||||
} |
||||
|
||||
async function verboseReportOnFailure (driver, test) { |
||||
let artifactDir |
||||
if (process.env.SELENIUM_BROWSER === 'chrome') { |
||||
artifactDir = `./test-artifacts/chrome/${test.title}` |
||||
} else if (process.env.SELENIUM_BROWSER === 'firefox') { |
||||
artifactDir = `./test-artifacts/firefox/${test.title}` |
||||
} |
||||
const filepathBase = `${artifactDir}/test-failure` |
||||
await pify(mkdirp)(artifactDir) |
||||
const screenshot = await driver.takeScreenshot() |
||||
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' }) |
||||
const htmlSource = await driver.getPageSource() |
||||
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource) |
||||
} |
@ -0,0 +1,491 @@ |
||||
const path = require('path') |
||||
const assert = require('assert') |
||||
const webdriver = require('selenium-webdriver') |
||||
const { By, Key } = webdriver |
||||
const { |
||||
delay, |
||||
buildChromeWebDriver, |
||||
buildFirefoxWebdriver, |
||||
installWebExt, |
||||
getExtensionIdChrome, |
||||
getExtensionIdFirefox, |
||||
} = require('../func') |
||||
const { |
||||
checkBrowserForConsoleErrors, |
||||
loadExtension, |
||||
verboseReportOnFailure, |
||||
} = require('./helpers') |
||||
|
||||
describe('MetaMask', function () { |
||||
let extensionId |
||||
let driver |
||||
let tokenAddress |
||||
|
||||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' |
||||
const tinyDelayMs = 500 |
||||
const regularDelayMs = tinyDelayMs * 2 |
||||
const largeDelayMs = regularDelayMs * 2 |
||||
const waitingNewPageDelayMs = regularDelayMs * 10 |
||||
|
||||
this.timeout(0) |
||||
this.bail(true) |
||||
|
||||
before(async function () { |
||||
switch (process.env.SELENIUM_BROWSER) { |
||||
case 'chrome': { |
||||
const extPath = path.resolve('dist/chrome') |
||||
driver = buildChromeWebDriver(extPath) |
||||
extensionId = await getExtensionIdChrome(driver) |
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`) |
||||
break |
||||
} |
||||
case 'firefox': { |
||||
const extPath = path.resolve('dist/firefox') |
||||
driver = buildFirefoxWebdriver() |
||||
await installWebExt(driver, extPath) |
||||
await delay(700) |
||||
extensionId = await getExtensionIdFirefox(driver) |
||||
await driver.get(`moz-extension://${extensionId}/popup.html`) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
afterEach(async function () { |
||||
if (process.env.SELENIUM_BROWSER === 'chrome') { |
||||
const errors = await checkBrowserForConsoleErrors(driver) |
||||
if (errors.length) { |
||||
const errorReports = errors.map(err => err.message) |
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}` |
||||
console.error(new Error(errorMessage)) |
||||
} |
||||
} |
||||
if (this.currentTest.state === 'failed') { |
||||
await verboseReportOnFailure(this.currentTest) |
||||
} |
||||
}) |
||||
|
||||
after(async function () { |
||||
await driver.quit() |
||||
}) |
||||
|
||||
describe('New UI setup', async function () { |
||||
it('switches to first tab', async function () { |
||||
const [firstTab] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(firstTab) |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('use the local network', async function () { |
||||
const [networkSelector] = await driver.findElements(By.css('#network_component')) |
||||
await networkSelector.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`)) |
||||
await localhost.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('selects the new UI option', async () => { |
||||
const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]")) |
||||
await button.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
// Close all other tabs
|
||||
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(oldUi) |
||||
await driver.close() |
||||
await driver.switchTo().window(infoPage) |
||||
await driver.close() |
||||
await driver.switchTo().window(newUi) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button')) |
||||
await continueBtn.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Going through the first time flow', () => { |
||||
it('accepts a secure password', async () => { |
||||
const [passwordBox] = await driver.findElements(By.css('.create-password #create-password')) |
||||
const [passwordBoxConfirm] = await driver.findElements(By.css('.create-password #confirm-password')) |
||||
const [button] = await driver.findElements(By.css('.create-password button')) |
||||
|
||||
await passwordBox.sendKeys('correct horse battery staple') |
||||
await passwordBoxConfirm.sendKeys('correct horse battery staple') |
||||
await button.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks through the unique image screen', async () => { |
||||
const [nextScreen] = await driver.findElements(By.css('.unique-image button')) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks through the privacy notice', async () => { |
||||
const [nextScreen] = await driver.findElements(By.css('.tou button')) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled() |
||||
assert.equal(canClickThrough, false, 'disabled continue button') |
||||
const [bottomOfTos] = await driver.findElements(By.linkText('Attributions')) |
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [acceptTos] = await driver.findElements(By.css('.tou button')) |
||||
await acceptTos.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
let seedPhrase |
||||
|
||||
it('reveals the seed phrase', async () => { |
||||
const [revealSeedPhrase] = await driver.findElements(By.css('.backup-phrase__secret-blocker')) |
||||
await revealSeedPhrase.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText() |
||||
assert.equal(seedPhrase.split(' ').length, 12) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [nextScreen] = await driver.findElements(By.css('.backup-phrase button')) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('can retype the seed phrase', async () => { |
||||
const words = seedPhrase.split(' ') |
||||
|
||||
const [word0] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[0]}')]`)) |
||||
await word0.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word1] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[1]}')]`)) |
||||
await word1.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word2] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[2]}')]`)) |
||||
await word2.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word3] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[3]}')]`)) |
||||
await word3.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word4] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[4]}')]`)) |
||||
await word4.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word5] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[5]}')]`)) |
||||
await word5.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word6] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[6]}')]`)) |
||||
await word6.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word7] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[7]}')]`)) |
||||
await word7.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word8] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[8]}')]`)) |
||||
await word8.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word9] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[9]}')]`)) |
||||
await word9.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word10] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[10]}')]`)) |
||||
await word10.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [word11] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[11]}')]`)) |
||||
await word11.click() |
||||
await delay(tinyDelayMs) |
||||
|
||||
const [confirm] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) |
||||
await confirm.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks through the deposit modal', async () => { |
||||
const [closeModal] = await driver.findElements(By.css('.page-container__header-close')) |
||||
await closeModal.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Show account information', () => { |
||||
it('shows the QR code for the account', async () => { |
||||
await driver.findElement(By.css('.wallet-view__details-button')).click() |
||||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()") |
||||
await delay(regularDelayMs * 4) |
||||
}) |
||||
}) |
||||
|
||||
describe('Log out an log back in', () => { |
||||
it('logs out of the account', async () => { |
||||
await driver.findElement(By.css('.account-menu__icon')).click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) |
||||
assert.equal(await logoutButton.getText(), 'Log out') |
||||
await logoutButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('accepts the account password after lock', async () => { |
||||
await driver.findElement(By.id('password')).sendKeys('correct horse battery staple') |
||||
await driver.findElement(By.id('password')).sendKeys(Key.ENTER) |
||||
await delay(regularDelayMs * 4) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add account', () => { |
||||
it('choose Create Account from the account menu', async () => { |
||||
await driver.findElement(By.css('.account-menu__icon')).click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`)) |
||||
await createAccount.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('set account name', async () => { |
||||
const [accountName] = await driver.findElements(By.css('.new-account-create-form input')) |
||||
await accountName.sendKeys('2nd account') |
||||
await delay(regularDelayMs) |
||||
|
||||
const [create] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`)) |
||||
await create.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('should correct account name', async () => { |
||||
const [accountName] = await driver.findElements(By.css('.account-name')) |
||||
assert.equal(await accountName.getText(), '2nd account') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Import seed phrase', () => { |
||||
it('logs out of the vault', async () => { |
||||
await driver.findElement(By.css('.account-menu__icon')).click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) |
||||
assert.equal(await logoutButton.getText(), 'Log out') |
||||
await logoutButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('imports seed phrase', async () => { |
||||
const [restoreSeedLink] = await driver.findElements(By.css('.unlock-page__link--import')) |
||||
assert.equal(await restoreSeedLink.getText(), 'Import using account seed phrase') |
||||
await restoreSeedLink.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [seedTextArea] = await driver.findElements(By.css('textarea')) |
||||
await seedTextArea.sendKeys(testSeedPhrase) |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.findElement(By.id('password-box')).sendKeys('correct horse battery staple') |
||||
await driver.findElement(By.id('password-box-confirm')).sendKeys('correct horse battery staple') |
||||
await driver.findElement(By.css('button:nth-child(2)')).click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('balance renders', async () => { |
||||
const balance = await driver.findElement(By.css('.balance-display .token-amount')) |
||||
const tokenAmount = await balance.getText() |
||||
assert.equal(tokenAmount, '100.000 ETH') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Send ETH from inside MetaMask', () => { |
||||
it('starts to send a transaction', async function () { |
||||
const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`)) |
||||
await sendButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]')) |
||||
const [inputAmount] = await driver.findElements(By.css('.currency-display__input')) |
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') |
||||
await inputAmount.sendKeys('1') |
||||
|
||||
// Set the gas limit
|
||||
const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button')) |
||||
await configureGas.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`)) |
||||
await save.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
// Continue to next screen
|
||||
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('confirms the transaction', async function () { |
||||
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('finds the transaction in the transactions list', async function () { |
||||
const transactions = await driver.findElements(By.css('.tx-list-item')) |
||||
assert.equal(transactions.length, 1) |
||||
|
||||
const txValues = await driver.findElements(By.css('.tx-list-value')) |
||||
assert.equal(txValues.length, 1) |
||||
assert.equal(await txValues[0].getText(), '1 ETH') |
||||
}) |
||||
}) |
||||
|
||||
describe('Send ETH from Faucet', () => { |
||||
it('starts a send transaction inside Faucet', async () => { |
||||
await driver.executeScript('window.open("https://faucet.metamask.io")') |
||||
await delay(waitingNewPageDelayMs) |
||||
|
||||
const [extension, faucet] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(faucet) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`)) |
||||
await send1eth.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(extension) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(faucet) |
||||
await delay(regularDelayMs) |
||||
await driver.close() |
||||
await delay(regularDelayMs) |
||||
await driver.switchTo().window(extension) |
||||
await delay(regularDelayMs) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add existing token using search', () => { |
||||
it('clicks on the Add Token button', async () => { |
||||
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) |
||||
await addToken.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('can pick a token from the existing options', async () => { |
||||
const [tokenSearch] = await driver.findElements(By.css('input.add-token__input')) |
||||
await tokenSearch.sendKeys('BAT') |
||||
await delay(regularDelayMs) |
||||
|
||||
const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]")) |
||||
await token.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) |
||||
await addTokens.click() |
||||
await delay(largeDelayMs) |
||||
}) |
||||
|
||||
it('renders the balance for the chosen token', async () => { |
||||
const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount')) |
||||
const tokenAmount = await balance.getText() |
||||
assert.equal(tokenAmount, '0BAT') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add a custom token from TokenFactory', () => { |
||||
it('creates a new token', async () => { |
||||
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') |
||||
await delay(waitingNewPageDelayMs) |
||||
|
||||
const [extension, tokenFactory] = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(tokenFactory) |
||||
const [ |
||||
totalSupply, |
||||
tokenName, |
||||
tokenDecimal, |
||||
tokenSymbol, |
||||
] = await driver.findElements(By.css('input')) |
||||
|
||||
await totalSupply.sendKeys('100') |
||||
await tokenName.sendKeys('Test') |
||||
await tokenDecimal.sendKeys('0') |
||||
await tokenSymbol.sendKeys('TST') |
||||
|
||||
const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`)) |
||||
await createToken.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(extension) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) |
||||
await confirmButton.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
await driver.switchTo().window(tokenFactory) |
||||
await delay(regularDelayMs) |
||||
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) |
||||
tokenAddress = await tokenContactAddress.getText() |
||||
await driver.close() |
||||
await driver.switchTo().window(extension) |
||||
await loadExtension(driver, extensionId) |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('clicks on the Add Token button', async () => { |
||||
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) |
||||
await addToken.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('picks the newly created Test token', async () => { |
||||
const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]")) |
||||
await addCustomToken.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input')) |
||||
await newTokenAddress.sendKeys(tokenAddress) |
||||
await delay(regularDelayMs) |
||||
|
||||
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) |
||||
await nextScreen.click() |
||||
await delay(regularDelayMs) |
||||
|
||||
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) |
||||
await addTokens.click() |
||||
await delay(regularDelayMs) |
||||
}) |
||||
|
||||
it('renders the balance for the new token', async () => { |
||||
const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount')) |
||||
const tokenAmount = await balance.getText() |
||||
assert.equal(tokenAmount, '100TST') |
||||
await delay(regularDelayMs) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,10 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
set -e |
||||
set -u |
||||
set -o pipefail |
||||
|
||||
export PATH="$PATH:./node_modules/.bin" |
||||
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec' |
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec' |
@ -1,18 +1,63 @@ |
||||
require('chromedriver') |
||||
require('geckodriver') |
||||
const fs = require('fs') |
||||
const os = require('os') |
||||
const path = require('path') |
||||
const webdriver = require('selenium-webdriver') |
||||
const Command = require('selenium-webdriver/lib/command').Command |
||||
const By = webdriver.By |
||||
|
||||
exports.delay = function delay (time) { |
||||
return new Promise(resolve => setTimeout(resolve, time)) |
||||
module.exports = { |
||||
delay, |
||||
buildChromeWebDriver, |
||||
buildFirefoxWebdriver, |
||||
installWebExt, |
||||
getExtensionIdChrome, |
||||
getExtensionIdFirefox, |
||||
} |
||||
|
||||
function delay (time) { |
||||
return new Promise(resolve => setTimeout(resolve, time)) |
||||
} |
||||
|
||||
exports.buildWebDriver = function buildWebDriver (extPath) { |
||||
function buildChromeWebDriver (extPath) { |
||||
const tmpProfile = path.join(os.tmpdir(), fs.mkdtempSync('mm-chrome-profile')); |
||||
return new webdriver.Builder() |
||||
.withCapabilities({ |
||||
chromeOptions: { |
||||
args: [`load-extension=${extPath}`], |
||||
args: [ |
||||
`load-extension=${extPath}`, |
||||
`user-data-dir=${tmpProfile}`, |
||||
], |
||||
binary: process.env.SELENIUM_CHROME_BINARY, |
||||
}, |
||||
}) |
||||
.forBrowser('chrome') |
||||
.build() |
||||
} |
||||
|
||||
function buildFirefoxWebdriver () { |
||||
return new webdriver.Builder().build() |
||||
} |
||||
|
||||
async function getExtensionIdChrome (driver) { |
||||
await driver.get('chrome://extensions') |
||||
const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")') |
||||
return extensionId |
||||
} |
||||
|
||||
async function getExtensionIdFirefox (driver) { |
||||
await driver.get('about:debugging#addons') |
||||
const extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText() |
||||
return extensionId |
||||
} |
||||
|
||||
async function installWebExt (driver, extension) { |
||||
const cmd = await new Command('moz-install-web-ext') |
||||
.setParameter('path', path.resolve(extension)) |
||||
.setParameter('temporary', true) |
||||
|
||||
await driver.getExecutor() |
||||
.defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install') |
||||
|
||||
return await driver.schedule(cmd, 'installWebExt(' + extension + ')') |
||||
} |
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@ |
||||
// var jsdom = require('mocha-jsdom')
|
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) |
||||
|
||||
describe('SAVE_ACCOUNT_LABEL', function () { |
||||
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { |
||||
var initialState = { |
||||
metamask: { |
||||
identities: { |
||||
foo: { |
||||
name: 'bar', |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const action = { |
||||
type: actions.SAVE_ACCOUNT_LABEL, |
||||
value: { |
||||
account: 'foo', |
||||
label: 'baz', |
||||
}, |
||||
} |
||||
freeze(action) |
||||
|
||||
var resultingState = reducers(initialState, action) |
||||
assert.equal(resultingState.metamask.identities.foo.name, action.value.label) |
||||
}) |
||||
}) |
||||
|
@ -0,0 +1,34 @@ |
||||
const assert = require('assert') |
||||
const freeze = require('deep-freeze-strict') |
||||
const path = require('path') |
||||
|
||||
const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) |
||||
const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) |
||||
|
||||
describe('SET_ACCOUNT_LABEL', function () { |
||||
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { |
||||
const initialState = { |
||||
metamask: { |
||||
identities: { |
||||
foo: { |
||||
name: 'bar', |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const action = { |
||||
type: actions.SET_ACCOUNT_LABEL, |
||||
value: { |
||||
account: 'foo', |
||||
label: 'baz', |
||||
}, |
||||
} |
||||
freeze(action) |
||||
|
||||
const resultingState = reducers(initialState, action) |
||||
assert.equal(resultingState.metamask.identities.foo.name, action.value.label) |
||||
}) |
||||
}) |
||||
|
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const ComposableObservableStore = require('../../app/scripts/lib/ComposableObservableStore') |
||||
const ComposableObservableStore = require('../../../app/scripts/lib/ComposableObservableStore') |
||||
const ObservableStore = require('obs-store') |
||||
|
||||
describe('ComposableObservableStore', () => { |
@ -0,0 +1,31 @@ |
||||
const assert = require('assert') |
||||
const path = require('path') |
||||
const accountImporter = require('../../../app/scripts/account-import-strategies/index') |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
describe('Account Import Strategies', function () { |
||||
const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553' |
||||
const json = '{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}' |
||||
|
||||
it('imports a private key and strips 0x prefix', async function () { |
||||
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ]) |
||||
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey)) |
||||
}) |
||||
|
||||
it('fails when password is incorrect for keystore', async function () { |
||||
const wrongPassword = 'password2' |
||||
|
||||
try { |
||||
await accountImporter.importAccount('JSON File', [ json, wrongPassword]) |
||||
} catch (error) { |
||||
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase') |
||||
} |
||||
}) |
||||
|
||||
it('imports json string and password to return a private key', async function () { |
||||
const fileContentsPassword = 'password1' |
||||
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword]) |
||||
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7') |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,48 @@ |
||||
const assert = require('assert') |
||||
const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') |
||||
|
||||
describe('', function () { |
||||
const mainnet = { |
||||
network: '1', |
||||
amount: 5, |
||||
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', |
||||
} |
||||
const ropsten = { |
||||
network: '3', |
||||
} |
||||
const rinkeby = { |
||||
network: '4', |
||||
} |
||||
const kovan = { |
||||
network: '42', |
||||
} |
||||
|
||||
it('returns coinbase url with amount and address for network 1', function () { |
||||
const coinbaseUrl = getBuyEthUrl(mainnet) |
||||
const coinbase = coinbaseUrl.match(/(https:\/\/buy.coinbase.com)/) |
||||
const amount = coinbaseUrl.match(/(amount)\D\d/) |
||||
const address = coinbaseUrl.match(/(address)(.*)(?=&)/) |
||||
|
||||
assert.equal(coinbase[0], 'https://buy.coinbase.com') |
||||
assert.equal(amount[0], 'amount=5') |
||||
assert.equal(address[0], 'address=0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc') |
||||
|
||||
}) |
||||
|
||||
it('returns metamask ropsten faucet for network 3', function () { |
||||
const ropstenUrl = getBuyEthUrl(ropsten) |
||||
assert.equal(ropstenUrl, 'https://faucet.metamask.io/') |
||||
}) |
||||
|
||||
it('returns rinkeby dapp for network 4', function () { |
||||
const rinkebyUrl = getBuyEthUrl(rinkeby) |
||||
assert.equal(rinkebyUrl, 'https://www.rinkeby.io/') |
||||
}) |
||||
|
||||
it('returns kovan github test faucet for network 42', function () { |
||||
const kovanUrl = getBuyEthUrl(kovan) |
||||
assert.equal(kovanUrl, 'https://github.com/kovan-testnet/faucet') |
||||
}) |
||||
|
||||
}) |
||||
|
@ -1,26 +1,26 @@ |
||||
const assert = require('assert') |
||||
const AddressBookController = require('../../app/scripts/controllers/address-book') |
||||
const AddressBookController = require('../../../../app/scripts/controllers/address-book') |
||||
|
||||
const mockKeyringController = { |
||||
memStore: { |
||||
getState: function () { |
||||
return { |
||||
identities: { |
||||
'0x0aaa': { |
||||
address: '0x0aaa', |
||||
name: 'owned', |
||||
}, |
||||
const stubPreferencesStore = { |
||||
getState: function () { |
||||
return { |
||||
identities: { |
||||
'0x0aaa': { |
||||
address: '0x0aaa', |
||||
name: 'owned', |
||||
}, |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
}, |
||||
} |
||||
}; |
||||
|
||||
describe('address-book-controller', function () { |
||||
var addressBookController |
||||
|
||||
beforeEach(function () { |
||||
addressBookController = new AddressBookController({}, mockKeyringController) |
||||
addressBookController = new AddressBookController({ |
||||
preferencesStore: stubPreferencesStore, |
||||
}) |
||||
}) |
||||
|
||||
describe('addres book management', function () { |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const BlacklistController = require('../../app/scripts/controllers/blacklist') |
||||
const BlacklistController = require('../../../../app/scripts/controllers/blacklist') |
||||
|
||||
describe('blacklist controller', function () { |
||||
let blacklistController |
@ -0,0 +1,550 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const clone = require('clone') |
||||
const nock = require('nock') |
||||
const createThoughStream = require('through2').obj |
||||
const MetaMaskController = require('../../../../app/scripts/metamask-controller') |
||||
const blacklistJSON = require('eth-phishing-detect/src/config') |
||||
const firstTimeState = require('../../../../app/scripts/first-time-state') |
||||
|
||||
const currentNetworkId = 42 |
||||
const DEFAULT_LABEL = 'Account 1' |
||||
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' |
||||
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' |
||||
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
|
||||
describe('MetaMaskController', function () { |
||||
let metamaskController |
||||
const sandbox = sinon.createSandbox() |
||||
const noop = () => {} |
||||
|
||||
beforeEach(function () { |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get('/v2/blacklist') |
||||
.reply(200, blacklistJSON) |
||||
|
||||
nock('https://api.infura.io') |
||||
.get('/v1/ticker/ethusd') |
||||
.reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}') |
||||
|
||||
nock('https://api.infura.io') |
||||
.get('/v1/ticker/ethjpy') |
||||
.reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}') |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get(/.*/) |
||||
.reply(200) |
||||
|
||||
metamaskController = new MetaMaskController({ |
||||
showUnapprovedTx: noop, |
||||
showUnconfirmedMessage: noop, |
||||
encryptor: { |
||||
encrypt: function (password, object) { |
||||
this.object = object |
||||
return Promise.resolve() |
||||
}, |
||||
decrypt: function () { |
||||
return Promise.resolve(this.object) |
||||
}, |
||||
}, |
||||
initState: clone(firstTimeState), |
||||
}) |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
nock.cleanAll() |
||||
sandbox.restore() |
||||
}) |
||||
|
||||
describe('#getGasPrice', function () { |
||||
|
||||
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { |
||||
const realRecentBlocksController = metamaskController.recentBlocksController |
||||
metamaskController.recentBlocksController = { |
||||
store: { |
||||
getState: () => { |
||||
return { |
||||
recentBlocks: [ |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
], |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
const gasPrice = metamaskController.getGasPrice() |
||||
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price') |
||||
|
||||
metamaskController.recentBlocksController = realRecentBlocksController |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndKeychain', function () { |
||||
it('can only create new vault on keyringController once', async function () { |
||||
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity') |
||||
|
||||
const password = 'a-fake-password' |
||||
|
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce) |
||||
|
||||
selectStub.reset() |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndRestore', function () { |
||||
it('should be able to call newVaultAndRestore despite a mistake.', async function () { |
||||
const password = 'what-what-what' |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null) |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) |
||||
}) |
||||
|
||||
it('should clear previous identities after vault restoration', async () => { |
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, |
||||
}) |
||||
|
||||
await metamaskController.preferencesController.setAccountLabel(TEST_ADDRESS, 'Account Foo') |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, |
||||
}) |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL }, |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getApi', function () { |
||||
let getApi, state |
||||
|
||||
beforeEach(function () { |
||||
getApi = metamaskController.getApi() |
||||
}) |
||||
|
||||
it('getState', function (done) { |
||||
getApi.getState((err, res) => { |
||||
if (err) { |
||||
done(err) |
||||
} else { |
||||
state = res |
||||
} |
||||
}) |
||||
assert.deepEqual(state, metamaskController.getState()) |
||||
done() |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('preferencesController', function () { |
||||
|
||||
it('defaults useBlockie to false', function () { |
||||
assert.equal(metamaskController.preferencesController.store.getState().useBlockie, false) |
||||
}) |
||||
|
||||
it('setUseBlockie to true', function () { |
||||
metamaskController.setUseBlockie(true, noop) |
||||
assert.equal(metamaskController.preferencesController.store.getState().useBlockie, true) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#selectFirstIdentity', function () { |
||||
let identities, address |
||||
|
||||
beforeEach(function () { |
||||
address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' |
||||
identities = { |
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { |
||||
'address': address, |
||||
'name': 'Account 1', |
||||
}, |
||||
'0xc42edfcc21ed14dda456aa0756c153f7985d8813': { |
||||
'address': '0xc42edfcc21ed14dda456aa0756c153f7985d8813', |
||||
'name': 'Account 2', |
||||
}, |
||||
} |
||||
metamaskController.preferencesController.store.updateState({ identities }) |
||||
metamaskController.selectFirstIdentity() |
||||
}) |
||||
|
||||
it('changes preferences controller select address', function () { |
||||
const preferenceControllerState = metamaskController.preferencesController.store.getState() |
||||
assert.equal(preferenceControllerState.selectedAddress, address) |
||||
}) |
||||
|
||||
it('changes metamask controller selected address', function () { |
||||
const metamaskState = metamaskController.getState() |
||||
assert.equal(metamaskState.selectedAddress, address) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setCustomRpc', function () { |
||||
const customRPC = 'https://custom.rpc/' |
||||
let rpcTarget |
||||
|
||||
beforeEach(function () { |
||||
|
||||
nock('https://custom.rpc') |
||||
.post('/') |
||||
.reply(200) |
||||
|
||||
rpcTarget = metamaskController.setCustomRpc(customRPC) |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
nock.cleanAll() |
||||
}) |
||||
|
||||
it('returns custom RPC that when called', async function () { |
||||
assert.equal(await rpcTarget, customRPC) |
||||
}) |
||||
|
||||
it('changes the network controller rpc', function () { |
||||
const networkControllerState = metamaskController.networkController.store.getState() |
||||
assert.equal(networkControllerState.provider.rpcTarget, customRPC) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setCurrentCurrency', function () { |
||||
let defaultMetaMaskCurrency |
||||
|
||||
beforeEach(function () { |
||||
defaultMetaMaskCurrency = metamaskController.currencyController.getCurrentCurrency() |
||||
}) |
||||
|
||||
it('defaults to usd', function () { |
||||
assert.equal(defaultMetaMaskCurrency, 'usd') |
||||
}) |
||||
|
||||
it('sets currency to JPY', function () { |
||||
metamaskController.setCurrentCurrency('JPY', noop) |
||||
assert.equal(metamaskController.currencyController.getCurrentCurrency(), 'JPY') |
||||
}) |
||||
}) |
||||
|
||||
describe('#createShapeshifttx', function () { |
||||
let depositAddress, depositType, shapeShiftTxList |
||||
|
||||
beforeEach(function () { |
||||
nock('https://shapeshift.io') |
||||
.get('/txStat/3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc') |
||||
.reply(200, '{"status": "no_deposits", "address": "3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc"}') |
||||
|
||||
depositAddress = '3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc' |
||||
depositType = 'ETH' |
||||
shapeShiftTxList = metamaskController.shapeshiftController.store.getState().shapeShiftTxList |
||||
}) |
||||
|
||||
it('creates a shapeshift tx', async function () { |
||||
metamaskController.createShapeShiftTx(depositAddress, depositType) |
||||
assert.equal(shapeShiftTxList[0].depositAddress, depositAddress) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#addNewAccount', function () { |
||||
let addNewAccount |
||||
|
||||
beforeEach(function () { |
||||
addNewAccount = metamaskController.addNewAccount() |
||||
}) |
||||
|
||||
it('errors when an primary keyring is does not exist', async function () { |
||||
try { |
||||
await addNewAccount |
||||
assert.equal(1 === 0) |
||||
} catch (e) { |
||||
assert.equal(e.message, 'MetamaskController - No HD Key Tree found') |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
describe('#verifyseedPhrase', function () { |
||||
let seedPhrase, getConfigSeed |
||||
|
||||
it('errors when no keying is provided', async function () { |
||||
try { |
||||
await metamaskController.verifySeedPhrase() |
||||
} catch (error) { |
||||
assert.equal(error.message, 'MetamaskController - No HD Key Tree found') |
||||
} |
||||
}) |
||||
|
||||
beforeEach(async function () { |
||||
await metamaskController.createNewVaultAndKeychain('password') |
||||
seedPhrase = await metamaskController.verifySeedPhrase() |
||||
}) |
||||
|
||||
it('#placeSeedWords should match the initially created vault seed', function () { |
||||
|
||||
metamaskController.placeSeedWords((err, result) => { |
||||
if (err) { |
||||
console.log(err) |
||||
} else { |
||||
getConfigSeed = metamaskController.configManager.getSeedWords() |
||||
assert.equal(result, seedPhrase) |
||||
assert.equal(result, getConfigSeed) |
||||
} |
||||
}) |
||||
assert.equal(getConfigSeed, undefined) |
||||
}) |
||||
|
||||
it('#addNewAccount', async function () { |
||||
await metamaskController.addNewAccount() |
||||
const getAccounts = await metamaskController.keyringController.getAccounts() |
||||
assert.equal(getAccounts.length, 2) |
||||
}) |
||||
}) |
||||
|
||||
describe('#resetAccount', function () { |
||||
|
||||
beforeEach(function () { |
||||
const selectedAddressStub = sinon.stub(metamaskController.preferencesController, 'getSelectedAddress') |
||||
const getNetworkstub = sinon.stub(metamaskController.txController.txStateManager, 'getNetwork') |
||||
|
||||
selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc') |
||||
getNetworkstub.returns(42) |
||||
|
||||
metamaskController.txController.txStateManager._saveTxList([ |
||||
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} }, |
||||
{ id: 2, status: 'rejected', metamaskNetworkId: 32, txParams: {} }, |
||||
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4'} }, |
||||
]) |
||||
}) |
||||
|
||||
it('wipes transactions from only the correct network id and with the selected address', async function () { |
||||
await metamaskController.resetAccount() |
||||
assert.equal(metamaskController.txController.txStateManager.getTx(1), undefined) |
||||
}) |
||||
}) |
||||
|
||||
describe('#clearSeedWordCache', function () { |
||||
|
||||
it('should have set seed words', function () { |
||||
metamaskController.configManager.setSeedWords('test words') |
||||
const getConfigSeed = metamaskController.configManager.getSeedWords() |
||||
assert.equal(getConfigSeed, 'test words') |
||||
}) |
||||
|
||||
it('should clear config seed phrase', function () { |
||||
metamaskController.configManager.setSeedWords('test words') |
||||
metamaskController.clearSeedWordCache((err, result) => { |
||||
if (err) console.log(err) |
||||
}) |
||||
const getConfigSeed = metamaskController.configManager.getSeedWords() |
||||
assert.equal(getConfigSeed, null) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#setCurrentLocale', function () { |
||||
|
||||
it('checks the default currentLocale', function () { |
||||
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale |
||||
assert.equal(preferenceCurrentLocale, undefined) |
||||
}) |
||||
|
||||
it('sets current locale in preferences controller', function () { |
||||
metamaskController.setCurrentLocale('ja', noop) |
||||
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale |
||||
assert.equal(preferenceCurrentLocale, 'ja') |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#newUnsignedMessage', function () { |
||||
|
||||
let msgParams, metamaskMsgs, messages, msgId |
||||
|
||||
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
const data = '0x43727970746f6b697474696573' |
||||
|
||||
beforeEach(async function () { |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
|
||||
msgParams = { |
||||
'from': address, |
||||
'data': data, |
||||
} |
||||
|
||||
metamaskController.newUnsignedMessage(msgParams, noop) |
||||
metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs() |
||||
messages = metamaskController.messageManager.messages |
||||
msgId = Object.keys(metamaskMsgs)[0] |
||||
messages[0].msgParams.metamaskId = parseInt(msgId) |
||||
}) |
||||
|
||||
it('persists address from msg params', function () { |
||||
assert.equal(metamaskMsgs[msgId].msgParams.from, address) |
||||
}) |
||||
|
||||
it('persists data from msg params', function () { |
||||
assert.equal(metamaskMsgs[msgId].msgParams.data, data) |
||||
}) |
||||
|
||||
it('sets the status to unapproved', function () { |
||||
assert.equal(metamaskMsgs[msgId].status, 'unapproved') |
||||
}) |
||||
|
||||
it('sets the type to eth_sign', function () { |
||||
assert.equal(metamaskMsgs[msgId].type, 'eth_sign') |
||||
}) |
||||
|
||||
it('rejects the message', function () { |
||||
const msgIdInt = parseInt(msgId) |
||||
metamaskController.cancelMessage(msgIdInt, noop) |
||||
assert.equal(messages[0].status, 'rejected') |
||||
}) |
||||
|
||||
it('errors when signing a message', async function () { |
||||
try { |
||||
await metamaskController.signMessage(messages[0].msgParams) |
||||
} catch (error) { |
||||
assert.equal(error.message, 'message length is invalid') |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
describe('#newUnsignedPersonalMessage', function () { |
||||
|
||||
it('errors with no from in msgParams', function () { |
||||
const msgParams = { |
||||
'data': data, |
||||
} |
||||
metamaskController.newUnsignedPersonalMessage(msgParams, function (error) { |
||||
assert.equal(error.message, 'MetaMask Message Signature: from field is required.') |
||||
}) |
||||
}) |
||||
|
||||
let msgParams, metamaskPersonalMsgs, personalMessages, msgId |
||||
|
||||
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
const data = '0x43727970746f6b697474696573' |
||||
|
||||
beforeEach(async function () { |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
|
||||
msgParams = { |
||||
'from': address, |
||||
'data': data, |
||||
} |
||||
|
||||
metamaskController.newUnsignedPersonalMessage(msgParams, noop) |
||||
metamaskPersonalMsgs = metamaskController.personalMessageManager.getUnapprovedMsgs() |
||||
personalMessages = metamaskController.personalMessageManager.messages |
||||
msgId = Object.keys(metamaskPersonalMsgs)[0] |
||||
personalMessages[0].msgParams.metamaskId = parseInt(msgId) |
||||
}) |
||||
|
||||
it('persists address from msg params', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address) |
||||
}) |
||||
|
||||
it('persists data from msg params', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data) |
||||
}) |
||||
|
||||
it('sets the status to unapproved', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].status, 'unapproved') |
||||
}) |
||||
|
||||
it('sets the type to personal_sign', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign') |
||||
}) |
||||
|
||||
it('rejects the message', function () { |
||||
const msgIdInt = parseInt(msgId) |
||||
metamaskController.cancelPersonalMessage(msgIdInt, noop) |
||||
assert.equal(personalMessages[0].status, 'rejected') |
||||
}) |
||||
|
||||
it('errors when signing a message', async function () { |
||||
await metamaskController.signPersonalMessage(personalMessages[0].msgParams) |
||||
assert.equal(metamaskPersonalMsgs[msgId].status, 'signed') |
||||
assert.equal(metamaskPersonalMsgs[msgId].rawSig, '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b')
|
||||
}) |
||||
}) |
||||
|
||||
describe('#setupUntrustedCommunication', function () { |
||||
let streamTest |
||||
|
||||
const phishingUrl = 'decentral.market' |
||||
|
||||
afterEach(function () { |
||||
streamTest.end() |
||||
}) |
||||
|
||||
it('sets up phishing stream for untrusted communication ', async function () { |
||||
await metamaskController.blacklistController.updatePhishingList() |
||||
|
||||
streamTest = createThoughStream((chunk, enc, cb) => { |
||||
assert.equal(chunk.name, 'phishing') |
||||
assert.equal(chunk.data.hostname, phishingUrl) |
||||
cb() |
||||
}) |
||||
// console.log(streamTest)
|
||||
metamaskController.setupUntrustedCommunication(streamTest, phishingUrl) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setupTrustedCommunication', function () { |
||||
let streamTest |
||||
|
||||
afterEach(function () { |
||||
streamTest.end() |
||||
}) |
||||
|
||||
it('sets up controller dnode api for trusted communication', function (done) { |
||||
streamTest = createThoughStream((chunk, enc, cb) => {
|
||||
assert.equal(chunk.name, 'controller') |
||||
cb() |
||||
done() |
||||
}) |
||||
|
||||
metamaskController.setupTrustedCommunication(streamTest, 'mycrypto.com') |
||||
}) |
||||
}) |
||||
|
||||
describe('#markAccountsFound', function () { |
||||
it('adds lost accounts to config manager data', function () { |
||||
metamaskController.markAccountsFound(noop) |
||||
const configManagerData = metamaskController.configManager.getData() |
||||
assert.deepEqual(configManagerData.lostAccounts, []) |
||||
}) |
||||
}) |
||||
|
||||
describe('#markPasswordForgotten', function () { |
||||
it('adds and sets forgottenPassword to config data to true', function () { |
||||
metamaskController.markPasswordForgotten(noop) |
||||
const configManagerData = metamaskController.configManager.getData() |
||||
assert.equal(configManagerData.forgottenPassword, true) |
||||
}) |
||||
}) |
||||
|
||||
describe('#unMarkPasswordForgotten', function () { |
||||
it('adds and sets forgottenPassword to config data to false', function () { |
||||
metamaskController.unMarkPasswordForgotten(noop) |
||||
const configManagerData = metamaskController.configManager.getData() |
||||
assert.equal(configManagerData.forgottenPassword, false) |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const configManagerGen = require('../lib/mock-config-manager') |
||||
const NoticeController = require('../../app/scripts/notice-controller') |
||||
const configManagerGen = require('../../../lib/mock-config-manager') |
||||
const NoticeController = require('../../../../app/scripts/notice-controller') |
||||
|
||||
describe('notice-controller', function () { |
||||
var noticeController |
@ -0,0 +1,162 @@ |
||||
const assert = require('assert') |
||||
const PreferencesController = require('../../../../app/scripts/controllers/preferences') |
||||
|
||||
describe('preferences controller', function () { |
||||
let preferencesController |
||||
|
||||
beforeEach(() => { |
||||
preferencesController = new PreferencesController() |
||||
}) |
||||
|
||||
describe('setAddresses', function () { |
||||
it('should keep a map of addresses to names and addresses in the store', function () { |
||||
preferencesController.setAddresses([ |
||||
'0xda22le', |
||||
'0x7e57e2', |
||||
]) |
||||
|
||||
const {identities} = preferencesController.store.getState() |
||||
assert.deepEqual(identities, { |
||||
'0xda22le': { |
||||
name: 'Account 1', |
||||
address: '0xda22le', |
||||
}, |
||||
'0x7e57e2': { |
||||
name: 'Account 2', |
||||
address: '0x7e57e2', |
||||
}, |
||||
}) |
||||
}) |
||||
|
||||
it('should replace its list of addresses', function () { |
||||
preferencesController.setAddresses([ |
||||
'0xda22le', |
||||
'0x7e57e2', |
||||
]) |
||||
preferencesController.setAddresses([ |
||||
'0xda22le77', |
||||
'0x7e57e277', |
||||
]) |
||||
|
||||
const {identities} = preferencesController.store.getState() |
||||
assert.deepEqual(identities, { |
||||
'0xda22le77': { |
||||
name: 'Account 1', |
||||
address: '0xda22le77', |
||||
}, |
||||
'0x7e57e277': { |
||||
name: 'Account 2', |
||||
address: '0x7e57e277', |
||||
}, |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('setAccountLabel', function () { |
||||
it('should update a label for the given account', function () { |
||||
preferencesController.setAddresses([ |
||||
'0xda22le', |
||||
'0x7e57e2', |
||||
]) |
||||
|
||||
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], { |
||||
name: 'Account 1', |
||||
address: '0xda22le', |
||||
}) |
||||
|
||||
|
||||
preferencesController.setAccountLabel('0xda22le', 'Dazzle') |
||||
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], { |
||||
name: 'Dazzle', |
||||
address: '0xda22le', |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('getTokens', function () { |
||||
it('should return an empty list initially', async function () { |
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 0, 'empty list of tokens') |
||||
}) |
||||
}) |
||||
|
||||
describe('addToken', function () { |
||||
it('should add that token to its state', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 1, 'one token added') |
||||
|
||||
const added = tokens[0] |
||||
assert.equal(added.address, address, 'set address correctly') |
||||
assert.equal(added.symbol, symbol, 'set symbol correctly') |
||||
assert.equal(added.decimals, decimals, 'set decimals correctly') |
||||
}) |
||||
|
||||
it('should allow updating a token value', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
const newDecimals = 6 |
||||
await preferencesController.addToken(address, symbol, newDecimals) |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 1, 'one token added') |
||||
|
||||
const added = tokens[0] |
||||
assert.equal(added.address, address, 'set address correctly') |
||||
assert.equal(added.symbol, symbol, 'set symbol correctly') |
||||
assert.equal(added.decimals, newDecimals, 'updated decimals correctly') |
||||
}) |
||||
|
||||
it('should allow adding tokens to two separate addresses', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 1st address') |
||||
|
||||
await preferencesController.setSelectedAddress('0xda22le') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address') |
||||
}) |
||||
}) |
||||
|
||||
describe('removeToken', function () { |
||||
it('should remove the only token from its state', async function () { |
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken('0xa', 'A', 5) |
||||
await preferencesController.removeToken('0xa') |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 0, 'one token removed') |
||||
}) |
||||
|
||||
it('should remove a token from its state', async function () { |
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken('0xa', 'A', 4) |
||||
await preferencesController.addToken('0xb', 'B', 5) |
||||
await preferencesController.removeToken('0xa') |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 1, 'one token removed') |
||||
|
||||
const [token1] = tokens |
||||
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const TokenRatesController = require('../../app/scripts/controllers/token-rates') |
||||
const TokenRatesController = require('../../../../app/scripts/controllers/token-rates') |
||||
const ObservableStore = require('obs-store') |
||||
|
||||
describe('TokenRatesController', () => { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker') |
||||
const MockTxGen = require('../lib/mock-tx-gen') |
||||
const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker') |
||||
const MockTxGen = require('../../../../lib/mock-tx-gen') |
||||
let providerResultStub = {} |
||||
|
||||
describe('Nonce Tracker', function () { |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const txHelper = require('../../ui/lib/tx-helper') |
||||
const txHelper = require('../../../../../ui/lib/tx-helper') |
||||
|
||||
describe('txHelper', function () { |
||||
it('always shows the oldest tx first', function () { |
@ -0,0 +1,129 @@ |
||||
const assert = require('assert') |
||||
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
const testVault = require('../../../../data/v17-long-history.json') |
||||
|
||||
describe ('Transaction state history helper', function () { |
||||
|
||||
describe('#snapshotFromTxMeta', function () { |
||||
it('should clone deep', function () { |
||||
const input = { |
||||
foo: { |
||||
bar: { |
||||
bam: 'baz' |
||||
} |
||||
} |
||||
} |
||||
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
||||
assert('foo' in output, 'has a foo key') |
||||
assert('bar' in output.foo, 'has a bar key') |
||||
assert('bam' in output.foo.bar, 'has a bar key') |
||||
assert.equal(output.foo.bar.bam, 'baz', 'has a baz value') |
||||
}) |
||||
|
||||
it('should remove the history key', function () { |
||||
const input = { foo: 'bar', history: 'remembered' } |
||||
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
||||
assert(typeof output.history, 'undefined', 'should remove history') |
||||
}) |
||||
}) |
||||
|
||||
describe('#migrateFromSnapshotsToDiffs', function () { |
||||
it('migrates history to diffs and can recover original values', function () { |
||||
testVault.data.TransactionController.transactions.forEach((tx, index) => { |
||||
const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history) |
||||
newHistory.forEach((newEntry, index) => { |
||||
if (index === 0) { |
||||
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj') |
||||
} else { |
||||
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj') |
||||
} |
||||
const oldEntry = tx.history[index] |
||||
const historySubset = newHistory.slice(0, index + 1) |
||||
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset) |
||||
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs') |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#replayHistory', function () { |
||||
it('replaying history does not mutate the original obj', function () { |
||||
const initialState = { test: true, message: 'hello', value: 1 } |
||||
const diff1 = [{ |
||||
"op": "replace", |
||||
"path": "/message", |
||||
"value": "haay", |
||||
}] |
||||
const diff2 = [{ |
||||
"op": "replace", |
||||
"path": "/value", |
||||
"value": 2, |
||||
}] |
||||
const history = [initialState, diff1, diff2] |
||||
|
||||
const beforeStateSnapshot = JSON.stringify(initialState) |
||||
const latestState = txStateHistoryHelper.replayHistory(history) |
||||
const afterStateSnapshot = JSON.stringify(initialState) |
||||
|
||||
assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state') |
||||
assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run') |
||||
}) |
||||
}) |
||||
|
||||
describe('#generateHistoryEntry', function () { |
||||
|
||||
function generateHistoryEntryTest(note) { |
||||
|
||||
const prevState = { |
||||
someValue: 'value 1', |
||||
foo: { |
||||
bar: { |
||||
bam: 'baz' |
||||
} |
||||
} |
||||
} |
||||
|
||||
const nextState = { |
||||
newPropRoot: 'new property - root', |
||||
someValue: 'value 2', |
||||
foo: { |
||||
newPropFirstLevel: 'new property - first level', |
||||
bar: { |
||||
bam: 'baz' |
||||
} |
||||
} |
||||
} |
||||
|
||||
const before = new Date().getTime() |
||||
const result = txStateHistoryHelper.generateHistoryEntry(prevState, nextState, note) |
||||
const after = new Date().getTime() |
||||
|
||||
assert.ok(Array.isArray(result)) |
||||
assert.equal(result.length, 3) |
||||
|
||||
const expectedEntry1 = { op: 'add', path: '/foo/newPropFirstLevel', value: 'new property - first level' } |
||||
assert.equal(result[0].op, expectedEntry1.op) |
||||
assert.equal(result[0].path, expectedEntry1.path) |
||||
assert.equal(result[0].value, expectedEntry1.value) |
||||
assert.equal(result[0].value, expectedEntry1.value) |
||||
if (note)
|
||||
assert.equal(result[0].note, note) |
||||
|
||||
assert.ok(result[0].timestamp >= before && result[0].timestamp <= after) |
||||
|
||||
const expectedEntry2 = { op: 'replace', path: '/someValue', value: 'value 2' } |
||||
assert.deepEqual(result[1], expectedEntry2) |
||||
|
||||
const expectedEntry3 = { op: 'add', path: '/newPropRoot', value: 'new property - root' } |
||||
assert.deepEqual(result[2], expectedEntry3) |
||||
} |
||||
|
||||
it('should generate history entries', function () { |
||||
generateHistoryEntryTest() |
||||
}) |
||||
|
||||
it('should add note to first entry', function () { |
||||
generateHistoryEntryTest('custom note') |
||||
})
|
||||
}) |
||||
}) |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const txUtils = require('../../app/scripts/controllers/transactions/lib/util') |
||||
const txUtils = require('../../../../../app/scripts/controllers/transactions/lib/util') |
||||
|
||||
|
||||
describe('txUtils', function () { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
|
||||
const EdgeEncryptor = require('../../app/scripts/edge-encryptor') |
||||
const EdgeEncryptor = require('../../../app/scripts/edge-encryptor') |
||||
|
||||
var password = 'passw0rd1' |
||||
var data = 'some random data' |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const MessageManager = require('../../app/scripts/lib/message-manager') |
||||
const MessageManager = require('../../../app/scripts/lib/message-manager') |
||||
|
||||
describe('Message Manager', function () { |
||||
let messageManager |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const nodeify = require('../../app/scripts/lib/nodeify') |
||||
const nodeify = require('../../../app/scripts/lib/nodeify') |
||||
|
||||
describe('nodeify', function () { |
||||
var obj = { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator') |
||||
const MockTxGen = require('../lib/mock-tx-gen') |
||||
const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator') |
||||
const MockTxGen = require('../../lib/mock-tx-gen') |
||||
const BN = require('ethereumjs-util').BN |
||||
let providerResultStub = {} |
||||
|
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
|
||||
const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager') |
||||
const PersonalMessageManager = require('../../../app/scripts/lib/personal-message-manager') |
||||
|
||||
describe('Personal Message Manager', function () { |
||||
let messageManager |
@ -1,9 +1,9 @@ |
||||
const assert = require('assert') |
||||
const clone = require('clone') |
||||
const KeyringController = require('eth-keyring-controller') |
||||
const firstTimeState = require('../../app/scripts/first-time-state') |
||||
const seedPhraseVerifier = require('../../app/scripts/lib/seed-phrase-verifier') |
||||
const mockEncryptor = require('../lib/mock-encryptor') |
||||
const firstTimeState = require('../../../app/scripts/first-time-state') |
||||
const seedPhraseVerifier = require('../../../app/scripts/lib/seed-phrase-verifier') |
||||
const mockEncryptor = require('../../lib/mock-encryptor') |
||||
|
||||
describe('SeedPhraseVerifier', function () { |
||||
|
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const { sufficientBalance } = require('../../app/scripts/lib/util') |
||||
const { sufficientBalance } = require('../../../app/scripts/lib/util') |
||||
|
||||
|
||||
describe('SufficientBalance', function () { |
@ -1,120 +0,0 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const clone = require('clone') |
||||
const nock = require('nock') |
||||
const MetaMaskController = require('../../app/scripts/metamask-controller') |
||||
const blacklistJSON = require('../stub/blacklist') |
||||
const firstTimeState = require('../../app/scripts/first-time-state') |
||||
|
||||
const DEFAULT_LABEL = 'Account 1' |
||||
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' |
||||
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' |
||||
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
|
||||
describe('MetaMaskController', function () { |
||||
let metamaskController |
||||
const sandbox = sinon.sandbox.create() |
||||
const noop = () => { } |
||||
|
||||
beforeEach(function () { |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get('/v2/blacklist') |
||||
.reply(200, blacklistJSON) |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get(/.*/) |
||||
.reply(200) |
||||
|
||||
metamaskController = new MetaMaskController({ |
||||
showUnapprovedTx: noop, |
||||
encryptor: { |
||||
encrypt: function (password, object) { |
||||
this.object = object |
||||
return Promise.resolve() |
||||
}, |
||||
decrypt: function () { |
||||
return Promise.resolve(this.object) |
||||
}, |
||||
}, |
||||
initState: clone(firstTimeState), |
||||
}) |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
nock.cleanAll() |
||||
sandbox.restore() |
||||
}) |
||||
|
||||
describe('#getGasPrice', function () { |
||||
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { |
||||
const realRecentBlocksController = metamaskController.recentBlocksController |
||||
metamaskController.recentBlocksController = { |
||||
store: { |
||||
getState: () => { |
||||
return { |
||||
recentBlocks: [ |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
], |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
const gasPrice = metamaskController.getGasPrice() |
||||
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price') |
||||
|
||||
metamaskController.recentBlocksController = realRecentBlocksController |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndKeychain', function () { |
||||
it('can only create new vault on keyringController once', async function () { |
||||
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity') |
||||
|
||||
const password = 'a-fake-password' |
||||
|
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce) |
||||
|
||||
selectStub.reset() |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndRestore', function () { |
||||
it('should be able to call newVaultAndRestore despite a mistake.', async function () { |
||||
const password = 'what-what-what' |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null) |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) |
||||
}) |
||||
|
||||
it('should clear previous identities after vault restoration', async () => { |
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, |
||||
}) |
||||
|
||||
await metamaskController.keyringController.saveAccountLabel(TEST_ADDRESS, 'Account Foo') |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, |
||||
}) |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL }, |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,41 @@ |
||||
const assert = require('assert') |
||||
const migration26 = require('../../../app/scripts/migrations/026') |
||||
const oldStorage = { |
||||
'meta': {'version': 25}, |
||||
'data': { |
||||
'PreferencesController': {}, |
||||
'KeyringController': { |
||||
'walletNicknames': { |
||||
'0x1e77e2': 'Test Account 1', |
||||
'0x7e57e2': 'Test Account 2', |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
describe('migration #26', () => { |
||||
it('should move the identities from KeyringController', (done) => { |
||||
migration26.migrate(oldStorage) |
||||
.then((newStorage) => { |
||||
const identities = newStorage.data.PreferencesController.identities |
||||
assert.deepEqual(identities, { |
||||
'0x1e77e2': {name: 'Test Account 1', address: '0x1e77e2'}, |
||||
'0x7e57e2': {name: 'Test Account 2', address: '0x7e57e2'}, |
||||
}) |
||||
assert.strictEqual(newStorage.data.KeyringController.walletNicknames, undefined) |
||||
done() |
||||
}) |
||||
.catch(done) |
||||
}) |
||||
|
||||
it('should successfully migrate first time state', (done) => { |
||||
migration26.migrate({ |
||||
meta: {}, |
||||
data: require('../../../app/scripts/first-time-state'), |
||||
}) |
||||
.then((migratedData) => { |
||||
assert.equal(migratedData.meta.version, migration26.version) |
||||
done() |
||||
}).catch(done) |
||||
}) |
||||
}) |
@ -1,22 +1,22 @@ |
||||
const assert = require('assert') |
||||
const path = require('path') |
||||
|
||||
const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) |
||||
const vault4 = require(path.join('..', 'lib', 'migrations', '004.json')) |
||||
const wallet1 = require(path.join('..', '..', 'lib', 'migrations', '001.json')) |
||||
const vault4 = require(path.join('..', '..', 'lib', 'migrations', '004.json')) |
||||
let vault5, vault6, vault7, vault8, vault9 // vault10, vault11
|
||||
|
||||
const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) |
||||
const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) |
||||
const migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004')) |
||||
const migration5 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '005')) |
||||
const migration6 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '006')) |
||||
const migration7 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '007')) |
||||
const migration8 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '008')) |
||||
const migration9 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '009')) |
||||
const migration10 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '010')) |
||||
const migration11 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '011')) |
||||
const migration12 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '012')) |
||||
const migration13 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '013')) |
||||
const migration2 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '002')) |
||||
const migration3 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '003')) |
||||
const migration4 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '004')) |
||||
const migration5 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '005')) |
||||
const migration6 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '006')) |
||||
const migration7 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '007')) |
||||
const migration8 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '008')) |
||||
const migration9 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '009')) |
||||
const migration10 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '010')) |
||||
const migration11 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '011')) |
||||
const migration12 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '012')) |
||||
const migration13 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '013')) |
||||
|
||||
|
||||
const oldTestRpc = 'https://rawtestrpc.metamask.io/' |
@ -1,48 +0,0 @@ |
||||
const assert = require('assert') |
||||
const PreferencesController = require('../../app/scripts/controllers/preferences') |
||||
|
||||
describe('preferences controller', function () { |
||||
let preferencesController |
||||
|
||||
before(() => { |
||||
preferencesController = new PreferencesController() |
||||
}) |
||||
|
||||
describe('addToken', function () { |
||||
it('should add that token to its state', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 1, 'one token added') |
||||
|
||||
const added = tokens[0] |
||||
assert.equal(added.address, address, 'set address correctly') |
||||
assert.equal(added.symbol, symbol, 'set symbol correctly') |
||||
assert.equal(added.decimals, decimals, 'set decimals correctly') |
||||
}) |
||||
|
||||
it('should allow updating a token value', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
const newDecimals = 6 |
||||
await preferencesController.addToken(address, symbol, newDecimals) |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 1, 'one token added') |
||||
|
||||
const added = tokens[0] |
||||
assert.equal(added.address, address, 'set address correctly') |
||||
assert.equal(added.symbol, symbol, 'set symbol correctly') |
||||
assert.equal(added.decimals, newDecimals, 'updated decimals correctly') |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -1,26 +0,0 @@ |
||||
const assert = require('assert') |
||||
const clone = require('clone') |
||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
|
||||
describe('deepCloneFromTxMeta', function () { |
||||
it('should clone deep', function () { |
||||
const input = { |
||||
foo: { |
||||
bar: { |
||||
bam: 'baz' |
||||
} |
||||
} |
||||
} |
||||
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
||||
assert('foo' in output, 'has a foo key') |
||||
assert('bar' in output.foo, 'has a bar key') |
||||
assert('bam' in output.foo.bar, 'has a bar key') |
||||
assert.equal(output.foo.bar.bam, 'baz', 'has a baz value') |
||||
}) |
||||
|
||||
it('should remove the history key', function () { |
||||
const input = { foo: 'bar', history: 'remembered' } |
||||
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
||||
assert(typeof output.history, 'undefined', 'should remove history') |
||||
}) |
||||
}) |
@ -1,46 +0,0 @@ |
||||
const assert = require('assert') |
||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
const testVault = require('../data/v17-long-history.json') |
||||
|
||||
|
||||
describe('tx-state-history-helper', function () { |
||||
it('migrates history to diffs and can recover original values', function () { |
||||
testVault.data.TransactionController.transactions.forEach((tx, index) => { |
||||
const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history) |
||||
newHistory.forEach((newEntry, index) => { |
||||
if (index === 0) { |
||||
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj') |
||||
} else { |
||||
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj') |
||||
} |
||||
const oldEntry = tx.history[index] |
||||
const historySubset = newHistory.slice(0, index + 1) |
||||
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset) |
||||
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs') |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
it('replaying history does not mutate the original obj', function () { |
||||
const initialState = { test: true, message: 'hello', value: 1 } |
||||
const diff1 = [{ |
||||
"op": "replace", |
||||
"path": "/message", |
||||
"value": "haay", |
||||
}] |
||||
const diff2 = [{ |
||||
"op": "replace", |
||||
"path": "/value", |
||||
"value": 2, |
||||
}] |
||||
const history = [initialState, diff1, diff2] |
||||
|
||||
const beforeStateSnapshot = JSON.stringify(initialState) |
||||
const latestState = txStateHistoryHelper.replayHistory(history) |
||||
const afterStateSnapshot = JSON.stringify(initialState) |
||||
|
||||
assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state') |
||||
assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run') |
||||
}) |
||||
|
||||
}) |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue