commit
770379c3da
@ -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 |
@ -1,26 +1,129 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const clone = require('clone') |
|
||||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||||
|
const testVault = require('../data/v17-long-history.json') |
||||||
|
|
||||||
describe('deepCloneFromTxMeta', function () { |
describe ('Transaction state history helper', function () { |
||||||
it('should clone deep', function () { |
|
||||||
const input = { |
describe('#snapshotFromTxMeta', function () { |
||||||
foo: { |
it('should clone deep', function () { |
||||||
bar: { |
const input = { |
||||||
bam: 'baz' |
foo: { |
||||||
|
bar: { |
||||||
|
bam: 'baz' |
||||||
|
} |
||||||
} |
} |
||||||
} |
} |
||||||
} |
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
||||||
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
assert('foo' in output, 'has a foo key') |
||||||
assert('foo' in output, 'has a foo key') |
assert('bar' in output.foo, 'has a bar key') |
||||||
assert('bar' in output.foo, 'has a bar key') |
assert('bam' in output.foo.bar, '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') |
||||||
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') |
||||||
|
}) |
||||||
}) |
}) |
||||||
|
|
||||||
it('should remove the history key', function () { |
describe('#migrateFromSnapshotsToDiffs', function () { |
||||||
const input = { foo: 'bar', history: 'remembered' } |
it('migrates history to diffs and can recover original values', function () { |
||||||
const output = txStateHistoryHelper.snapshotFromTxMeta(input) |
testVault.data.TransactionController.transactions.forEach((tx, index) => { |
||||||
assert(typeof output.history, 'undefined', 'should remove history') |
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,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') |
|
||||||
}) |
|
||||||
|
|
||||||
}) |
|
Loading…
Reference in new issue