Gaëtan Renaudeau
6 years ago
88 changed files with 1307 additions and 389 deletions
@ -0,0 +1,85 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
/* eslint-disable no-console */ |
||||
|
|
||||
|
const util = require('util') |
||||
|
const exec = util.promisify(require('child_process').exec) |
||||
|
const octokit = require('@octokit/rest')() |
||||
|
|
||||
|
const repo = { |
||||
|
owner: 'LedgerHQ', |
||||
|
repo: 'ledger-live-desktop', |
||||
|
} |
||||
|
|
||||
|
async function getTag() { |
||||
|
const { stdout } = await exec('git tag --points-at HEAD') |
||||
|
const tag = stdout.replace('\n', '') |
||||
|
|
||||
|
if (!tag) { |
||||
|
throw new Error(`Unable to get current tag. Is your HEAD on a tagged commit?`) |
||||
|
} |
||||
|
|
||||
|
return tag |
||||
|
} |
||||
|
|
||||
|
async function checkDraft(tag) { |
||||
|
const { status, data } = await octokit.repos.getReleases(repo) |
||||
|
|
||||
|
if (status !== 200) { |
||||
|
throw new Error(`Got HTTP status ${status} when trying to fetch releases list.`) |
||||
|
} |
||||
|
|
||||
|
for (const release of data) { |
||||
|
if (release.tag_name === tag) { |
||||
|
if (release.draft) { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
throw new Error(`A release tagged ${tag} exists but is not a draft.`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
async function createDraft(tag) { |
||||
|
const params = { |
||||
|
...repo, |
||||
|
tag_name: tag, |
||||
|
name: tag, |
||||
|
draft: true, |
||||
|
prerelease: true, |
||||
|
} |
||||
|
|
||||
|
const { status } = await octokit.repos.createRelease(params) |
||||
|
|
||||
|
if (status !== 201) { |
||||
|
throw new Error(`Got HTTP status ${status} when trying to create the release draft.`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function main() { |
||||
|
try { |
||||
|
const token = process.env.GH_TOKEN |
||||
|
const tag = await getTag() |
||||
|
|
||||
|
octokit.authenticate({ |
||||
|
type: 'token', |
||||
|
token, |
||||
|
}) |
||||
|
|
||||
|
const existingDraft = await checkDraft(tag) |
||||
|
|
||||
|
if (!existingDraft) { |
||||
|
console.log(`No draft exists for ${tag}, creating...`) |
||||
|
createDraft(tag) |
||||
|
} else { |
||||
|
console.log(`A draft already exists for ${tag}, nothing to do.`) |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
process.exit(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
main() |
@ -1,19 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import { createCommand } from 'helpers/ipc' |
|
||||
import { fromPromise } from 'rxjs/observable/fromPromise' |
|
||||
import withLibcore from 'helpers/withLibcore' |
|
||||
import { HardResetFail } from 'config/errors' |
|
||||
|
|
||||
const cmd = createCommand('libcoreHardReset', () => |
|
||||
fromPromise( |
|
||||
withLibcore(async core => { |
|
||||
const result = await core.getPoolInstance().eraseDataSince(new Date(0)) |
|
||||
if (result !== core.ERROR_CODE.FUTURE_WAS_SUCCESSFULL) { |
|
||||
throw new HardResetFail(`Hard reset fail with ${result} (check core.ERROR_CODE)`) |
|
||||
} |
|
||||
}), |
|
||||
), |
|
||||
) |
|
||||
|
|
||||
export default cmd |
|
@ -0,0 +1,29 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { fromPromise } from 'rxjs/observable/fromPromise' |
||||
|
import type { AccountRaw } from '@ledgerhq/live-common/lib/types' |
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import withLibcore from 'helpers/withLibcore' |
||||
|
import { scanAccountsFromXPUB } from 'helpers/libcore' |
||||
|
|
||||
|
type Input = { |
||||
|
currencyId: string, |
||||
|
xpub: string, |
||||
|
isSegwit: boolean, |
||||
|
isUnsplit: boolean, |
||||
|
} |
||||
|
|
||||
|
type Result = AccountRaw |
||||
|
|
||||
|
const cmd: Command<Input, Result> = createCommand( |
||||
|
'libcoreScanFromXPUB', |
||||
|
({ currencyId, xpub, isSegwit, isUnsplit }) => |
||||
|
fromPromise( |
||||
|
withLibcore(async core => |
||||
|
scanAccountsFromXPUB({ core, currencyId, xpub, isSegwit, isUnsplit }), |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
export default cmd |
@ -0,0 +1,207 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent, Fragment } from 'react' |
||||
|
import invariant from 'invariant' |
||||
|
import { connect } from 'react-redux' |
||||
|
|
||||
|
import type { Currency, Account } from '@ledgerhq/live-common/lib/types' |
||||
|
|
||||
|
import { decodeAccount } from 'reducers/accounts' |
||||
|
import { addAccount } from 'actions/accounts' |
||||
|
|
||||
|
import FormattedVal from 'components/base/FormattedVal' |
||||
|
import Switch from 'components/base/Switch' |
||||
|
import Spinner from 'components/base/Spinner' |
||||
|
import Box, { Card } from 'components/base/Box' |
||||
|
import TranslatedError from 'components/TranslatedError' |
||||
|
import Button from 'components/base/Button' |
||||
|
import Input from 'components/base/Input' |
||||
|
import Label from 'components/base/Label' |
||||
|
import SelectCurrency from 'components/SelectCurrency' |
||||
|
import { CurrencyCircleIcon } from 'components/base/CurrencyBadge' |
||||
|
|
||||
|
import { idleCallback } from 'helpers/promise' |
||||
|
import { splittedCurrencies } from 'config/cryptocurrencies' |
||||
|
|
||||
|
import scanFromXPUB from 'commands/libcoreScanFromXPUB' |
||||
|
|
||||
|
const mapDispatchToProps = { |
||||
|
addAccount, |
||||
|
} |
||||
|
|
||||
|
type Props = { |
||||
|
addAccount: Account => void, |
||||
|
} |
||||
|
|
||||
|
const INITIAL_STATE = { |
||||
|
status: 'idle', |
||||
|
currency: null, |
||||
|
xpub: '', |
||||
|
account: null, |
||||
|
isSegwit: true, |
||||
|
isUnsplit: false, |
||||
|
error: null, |
||||
|
} |
||||
|
|
||||
|
type State = { |
||||
|
status: string, |
||||
|
currency: ?Currency, |
||||
|
xpub: string, |
||||
|
account: ?Account, |
||||
|
isSegwit: boolean, |
||||
|
isUnsplit: boolean, |
||||
|
error: ?Error, |
||||
|
} |
||||
|
|
||||
|
class AccountImporter extends PureComponent<Props, State> { |
||||
|
state = INITIAL_STATE |
||||
|
|
||||
|
onChangeCurrency = currency => { |
||||
|
if (currency.family !== 'bitcoin') return |
||||
|
this.setState({ |
||||
|
currency, |
||||
|
isSegwit: !!currency.supportsSegwit, |
||||
|
isUnsplit: false, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
onChangeXPUB = xpub => this.setState({ xpub }) |
||||
|
onChangeSegwit = isSegwit => this.setState({ isSegwit }) |
||||
|
onChangeUnsplit = isUnsplit => this.setState({ isUnsplit }) |
||||
|
|
||||
|
isValid = () => { |
||||
|
const { currency, xpub } = this.state |
||||
|
return !!currency && !!xpub |
||||
|
} |
||||
|
|
||||
|
scan = async () => { |
||||
|
if (!this.isValid()) return |
||||
|
this.setState({ status: 'scanning' }) |
||||
|
try { |
||||
|
const { currency, xpub, isSegwit, isUnsplit } = this.state |
||||
|
invariant(currency, 'no currency') |
||||
|
const rawAccount = await scanFromXPUB |
||||
|
.send({ |
||||
|
currencyId: currency.id, |
||||
|
xpub, |
||||
|
isSegwit, |
||||
|
isUnsplit, |
||||
|
}) |
||||
|
.toPromise() |
||||
|
const account = decodeAccount(rawAccount) |
||||
|
this.setState({ status: 'finish', account }) |
||||
|
} catch (error) { |
||||
|
this.setState({ status: 'error', error }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
import = async () => { |
||||
|
const { account } = this.state |
||||
|
invariant(account, 'no account') |
||||
|
await idleCallback() |
||||
|
this.props.addAccount(account) |
||||
|
this.reset() |
||||
|
} |
||||
|
|
||||
|
reset = () => this.setState(INITIAL_STATE) |
||||
|
|
||||
|
render() { |
||||
|
const { currency, xpub, isSegwit, isUnsplit, status, account, error } = this.state |
||||
|
const supportsSplit = !!currency && !!splittedCurrencies[currency.id] |
||||
|
return ( |
||||
|
<Card title="Import from xpub" flow={3}> |
||||
|
{status === 'idle' ? ( |
||||
|
<Fragment> |
||||
|
<Box flow={1}> |
||||
|
<Label>{'currency'}</Label> |
||||
|
<SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} /> |
||||
|
</Box> |
||||
|
{currency && (currency.supportsSegwit || supportsSplit) ? ( |
||||
|
<Box horizontal justify="flex-end" align="center" flow={3}> |
||||
|
{supportsSplit && ( |
||||
|
<Box horizontal align="center" flow={1}> |
||||
|
<Box ff="Museo Sans|Bold" fontSize={4}> |
||||
|
{'unsplit'} |
||||
|
</Box> |
||||
|
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} /> |
||||
|
</Box> |
||||
|
)} |
||||
|
{currency.supportsSegwit && ( |
||||
|
<Box horizontal align="center" flow={1}> |
||||
|
<Box ff="Museo Sans|Bold" fontSize={4}> |
||||
|
{'segwit'} |
||||
|
</Box> |
||||
|
<Switch isChecked={isSegwit} onChange={this.onChangeSegwit} /> |
||||
|
</Box> |
||||
|
)} |
||||
|
</Box> |
||||
|
) : null} |
||||
|
<Box flow={1}> |
||||
|
<Label>{'xpub'}</Label> |
||||
|
<Input |
||||
|
placeholder="xpub" |
||||
|
value={xpub} |
||||
|
onChange={this.onChangeXPUB} |
||||
|
onEnter={this.scan} |
||||
|
/> |
||||
|
</Box> |
||||
|
<Box align="flex-end"> |
||||
|
<Button primary small disabled={!this.isValid()} onClick={this.scan}> |
||||
|
{'scan'} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
</Fragment> |
||||
|
) : status === 'scanning' ? ( |
||||
|
<Box align="center" justify="center" p={5}> |
||||
|
<Spinner size={16} /> |
||||
|
</Box> |
||||
|
) : status === 'finish' ? ( |
||||
|
account ? ( |
||||
|
<Box p={8} align="center" justify="center" flow={5} horizontal> |
||||
|
<Box horizontal flow={4} color="graphite" align="center"> |
||||
|
{currency && <CurrencyCircleIcon size={64} currency={currency} />} |
||||
|
<Box> |
||||
|
<Box ff="Museo Sans|Bold">{account.name}</Box> |
||||
|
<FormattedVal |
||||
|
fontSize={2} |
||||
|
alwaysShowSign={false} |
||||
|
color="graphite" |
||||
|
unit={account.unit} |
||||
|
showCode |
||||
|
val={account.balance || 0} |
||||
|
/> |
||||
|
<Box fontSize={2}>{`${account.operations.length} operation(s)`}</Box> |
||||
|
</Box> |
||||
|
</Box> |
||||
|
|
||||
|
<Button outline small disabled={!account} onClick={this.import}> |
||||
|
{'import'} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
) : ( |
||||
|
<Box align="center" justify="center" p={5} flow={4}> |
||||
|
<Box>{'No accounts found or wrong xpub'}</Box> |
||||
|
<Button primary onClick={this.reset} small autoFocus> |
||||
|
{'Reset'} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
) |
||||
|
) : status === 'error' ? ( |
||||
|
<Box align="center" justify="center" p={5} flow={4}> |
||||
|
<Box> |
||||
|
<TranslatedError error={error} /> |
||||
|
</Box> |
||||
|
<Button primary onClick={this.reset} small autoFocus> |
||||
|
{'Reset'} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
) : null} |
||||
|
</Card> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect( |
||||
|
null, |
||||
|
mapDispatchToProps, |
||||
|
)(AccountImporter) |
@ -0,0 +1,11 @@ |
|||||
|
import React from 'react' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
|
||||
|
import AccountImporter from './AccountImporter' |
||||
|
|
||||
|
export default () => ( |
||||
|
<Box flow={2}> |
||||
|
<AccountImporter /> |
||||
|
</Box> |
||||
|
) |
@ -1,13 +0,0 @@ |
|||||
import libcoreHardReset from 'commands/libcoreHardReset' |
|
||||
import { disable as disableDBMiddleware } from 'middlewares/db' |
|
||||
|
|
||||
import db from 'helpers/db' |
|
||||
import { delay } from 'helpers/promise' |
|
||||
|
|
||||
export default async function hardReset() { |
|
||||
await libcoreHardReset.send() |
|
||||
disableDBMiddleware() |
|
||||
db.resetAll() |
|
||||
await delay(500) |
|
||||
window.location.href = '' |
|
||||
} |
|
@ -0,0 +1,35 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import path from 'path' |
||||
|
import rimraf from 'rimraf' |
||||
|
import resolveUserDataDirectory from 'helpers/resolveUserDataDirectory' |
||||
|
import { disable as disableDBMiddleware } from 'middlewares/db' |
||||
|
import db from 'helpers/db' |
||||
|
import { delay } from 'helpers/promise' |
||||
|
|
||||
|
function resetLibcoreDatabase() { |
||||
|
const dbpath = path.resolve(resolveUserDataDirectory(), 'sqlite/') |
||||
|
rimraf.sync(dbpath, { glob: false }) |
||||
|
} |
||||
|
|
||||
|
function reload() { |
||||
|
require('electron') |
||||
|
.remote.getCurrentWindow() |
||||
|
.webContents.reload() |
||||
|
} |
||||
|
|
||||
|
export async function hardReset() { |
||||
|
disableDBMiddleware() |
||||
|
db.resetAll() |
||||
|
await delay(500) |
||||
|
resetLibcoreDatabase() |
||||
|
reload() |
||||
|
} |
||||
|
|
||||
|
export async function softReset({ cleanAccountsCache }: *) { |
||||
|
cleanAccountsCache() |
||||
|
await delay(500) |
||||
|
await db.cleanCache() |
||||
|
resetLibcoreDatabase() |
||||
|
reload() |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
# ledgerLive-QA |
||||
|
Automated tests for Ledger Live Desktop application. |
||||
|
Start Ledger Live Desktop application with accounts for the supported coin. Operations history removed from db. Then sync to retrieve account balance and transactions history. |
||||
|
|
||||
|
|
||||
|
## Accounts setup and sync |
||||
|
#### Launch test |
||||
|
yarn test-sync |
||||
|
|
||||
|
#### Test description |
||||
|
Clean Ledger Live Application settings directory. |
||||
|
Copy app.json init file for testing in a new Ledger Live Application settings directory. |
||||
|
Start Ledger Live Desktop app. |
||||
|
Wait for sync OK. |
||||
|
Compare new app.json with expected app.json file. |
@ -0,0 +1,119 @@ |
|||||
|
import { Application } from 'spectron' |
||||
|
|
||||
|
import { waitForDisappear, waitForExpectedText } from './helpers' |
||||
|
|
||||
|
const os = require('os') |
||||
|
const appVersion = require('../package.json') |
||||
|
|
||||
|
let app |
||||
|
|
||||
|
const TIMEOUT = 50 * 1000 |
||||
|
|
||||
|
let app_path |
||||
|
const platform = os.platform() |
||||
|
if (platform === 'darwin') { |
||||
|
app_path = `./dist/mac/Ledger Live.app/Contents/MacOS/Ledger Live` |
||||
|
} else if (platform === 'win32') { |
||||
|
app_path = `.\\dist\\win-unpacked\\Ledger Live.exe` |
||||
|
} else { |
||||
|
app_path = `./dist/ledger-live-desktop-${appVersion.version}-linux-x86_64.AppImage` |
||||
|
} |
||||
|
|
||||
|
describe('Application launch', () => { |
||||
|
beforeEach(async () => { |
||||
|
app = new Application({ |
||||
|
path: app_path, |
||||
|
env: { |
||||
|
SKIP_ONBOARDING: '1', |
||||
|
}, |
||||
|
}) |
||||
|
await app.start() |
||||
|
}, TIMEOUT) |
||||
|
|
||||
|
afterEach(async () => { |
||||
|
if (app && app.isRunning()) { |
||||
|
await app.stop() |
||||
|
} |
||||
|
}, TIMEOUT) |
||||
|
|
||||
|
test( |
||||
|
'Start app, skip onboarding, check Empty State, check General Settings and verify Developer mode', |
||||
|
async () => { |
||||
|
const title = await app.client.getTitle() |
||||
|
expect(title).toEqual('Ledger Live') |
||||
|
await app.client.waitUntilWindowLoaded() |
||||
|
await waitForDisappear(app, '#preload') |
||||
|
|
||||
|
// Post Onboarding (Analytics)
|
||||
|
const analytics_title = await waitForExpectedText( |
||||
|
app, |
||||
|
'[data-e2e=onboarding_title]', |
||||
|
'Analytics and bug reports', |
||||
|
) |
||||
|
// Verify "Technical Data" + Link "Learn more"
|
||||
|
const analytics_techData_title = await app.client.getText('[data-e2e=analytics_techData]') |
||||
|
expect(analytics_techData_title).toEqual('Technical data *') |
||||
|
await app.client.click('[data-e2e=analytics_techData_Link]') |
||||
|
await waitForExpectedText(app, '[data-e2e=modal_title]', 'Technical data') |
||||
|
await app.client.click('[data-e2e=modal_buttonClose_techData]') |
||||
|
analytics_title |
||||
|
|
||||
|
// Verify "Share analytics" + Link "Learn more"
|
||||
|
const analytics_shareAnalytics_title = await app.client.getText( |
||||
|
'[data-e2e=analytics_shareAnalytics]', |
||||
|
) |
||||
|
expect(analytics_shareAnalytics_title).toEqual('Share analytics') |
||||
|
await app.client.click('[data-e2e=analytics_shareAnalytics_Link]') |
||||
|
await waitForExpectedText(app, '[data-e2e=modal_title]', 'Share analytics') |
||||
|
await app.client.click('[data-e2e=modal_buttonClose_shareAnalytics]') |
||||
|
analytics_title |
||||
|
|
||||
|
// Verify "Report bugs"
|
||||
|
const analytics_reportBugs_title = await app.client.getText('[data-e2e=analytics_reportBugs]') |
||||
|
expect(analytics_reportBugs_title).toEqual('Report bugs') |
||||
|
|
||||
|
await app.client.click('[data-e2e=continue_button]') |
||||
|
|
||||
|
// Finish Onboarding
|
||||
|
await waitForExpectedText(app, '[data-e2e=finish_title]', 'Your device is ready!') |
||||
|
await app.client.click('[data-e2e=continue_button]') |
||||
|
|
||||
|
await waitForExpectedText(app, '[data-e2e=modal_title]', 'Trade safely') |
||||
|
await app.client.click('[data-e2e=continue_button]') |
||||
|
|
||||
|
// Dashboard EmptyState
|
||||
|
await waitForExpectedText( |
||||
|
app, |
||||
|
'[data-e2e=dashboard_empty_title]', |
||||
|
'Add accounts to your portfolio', |
||||
|
) |
||||
|
const openManager_button = await app.client.getText('[data-e2e=dashboard_empty_OpenManager]') |
||||
|
expect(openManager_button).toEqual('Open Manager') |
||||
|
const addAccount_button = await app.client.getText('[data-e2e=dashboard_empty_AddAccounts]') |
||||
|
expect(addAccount_button).toEqual('Add accounts') |
||||
|
|
||||
|
// Open Settings
|
||||
|
await app.client.click('[data-e2e=setting_button]') |
||||
|
await waitForExpectedText(app, '[data-e2e=settings_title]', 'Settings') |
||||
|
// Verify settings General section
|
||||
|
const settingsGeneral_title = await app.client.getText('[data-e2e=settingsGeneral_title]') |
||||
|
expect(settingsGeneral_title).toEqual('General') |
||||
|
|
||||
|
// TO ADD : VERIFY PASSWORD LOCK VALUE = DISABLE ???
|
||||
|
// Report bugs = OFF
|
||||
|
await app.client.click('[data-e2e=reportBugs_button]') |
||||
|
|
||||
|
// Analytics = ON
|
||||
|
await app.client.click('[data-e2e=shareAnalytics_button]') |
||||
|
|
||||
|
// DevMode = ON
|
||||
|
await app.client.click('[data-e2e=devMode_button]') |
||||
|
|
||||
|
// Verify Dev mode
|
||||
|
// Add New Account
|
||||
|
await app.client.click('[data-e2e=menuAddAccount_button]') |
||||
|
await waitForExpectedText(app, '[data-e2e=modal_title]', 'Add accounts') |
||||
|
}, |
||||
|
TIMEOUT, |
||||
|
) |
||||
|
}) |
@ -0,0 +1,40 @@ |
|||||
|
import { delay } from 'helpers/promise' |
||||
|
|
||||
|
// Wait for an element to be present then continue
|
||||
|
export function waitForExpectedText(app, selector, expected, maxRetry = 5) { |
||||
|
async function check() { |
||||
|
if (!maxRetry) { |
||||
|
throw new Error(`Cant find the element ${selector} in the page`) |
||||
|
} |
||||
|
try { |
||||
|
const str = await app.client.getText(selector) |
||||
|
if (str === expected) { |
||||
|
return true |
||||
|
} |
||||
|
} catch (err) {} // eslint-disable-line
|
||||
|
await delay(500) |
||||
|
--maxRetry |
||||
|
return check() |
||||
|
} |
||||
|
return check() |
||||
|
} |
||||
|
|
||||
|
// Wait for an element to disappear then continue
|
||||
|
export function waitForDisappear(app, selector, maxRetry = 5) { |
||||
|
async function check() { |
||||
|
if (!maxRetry) { |
||||
|
throw new Error('Too many retries for waiting element to disappear') |
||||
|
} |
||||
|
try { |
||||
|
await app.client.getText(selector) |
||||
|
} catch (err) { |
||||
|
if (err.message.startsWith('An element could not be located')) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
await delay(500) |
||||
|
--maxRetry |
||||
|
return check() |
||||
|
} |
||||
|
return check() |
||||
|
} |
@ -1,64 +0,0 @@ |
|||||
const Application = require('spectron').Application |
|
||||
|
|
||||
let app |
|
||||
|
|
||||
const TIMEOUT = 50 * 1000 |
|
||||
|
|
||||
describe('Application launch', () => { |
|
||||
beforeEach(async () => { |
|
||||
app = new Application({ |
|
||||
path: './dist/ledger-live-desktop-1.1.0-linux-x86_64.AppImage', |
|
||||
env: { |
|
||||
SKIP_ONBOARDING: '1', |
|
||||
}, |
|
||||
}) |
|
||||
await app.start() |
|
||||
}, TIMEOUT) |
|
||||
|
|
||||
afterEach(async () => { |
|
||||
if (app && app.isRunning()) { |
|
||||
await app.stop() |
|
||||
} |
|
||||
}, TIMEOUT) |
|
||||
|
|
||||
test( |
|
||||
'Start app and set developper mode ', |
|
||||
async () => { |
|
||||
const title = await app.client.getTitle() |
|
||||
expect(title).toEqual('Ledger Live') |
|
||||
await app.client.waitUntilWindowLoaded() |
|
||||
await app.client.pause(2000) |
|
||||
|
|
||||
// Post Onboarding
|
|
||||
const title_onboarding = await app.client.getText('[data-e2e=onboarding_title]') |
|
||||
expect(title_onboarding).toEqual('Analytics and bug reports') |
|
||||
await app.client.click('[data-e2e=continue_button]') |
|
||||
await app.client.pause(1000) |
|
||||
|
|
||||
const title_finish = await app.client.getText('[data-e2e=finish_title]') |
|
||||
expect(title_finish).toEqual('Your device is ready!') |
|
||||
await app.client.click('[data-e2e=continue_button]') |
|
||||
await app.client.pause(1000) |
|
||||
|
|
||||
const title_disclaimer = await app.client.getText('[data-e2e=disclaimer_title]') |
|
||||
expect(title_disclaimer).toEqual('Trade safely') |
|
||||
await app.client.click('[data-e2e=continue_button]') |
|
||||
await app.client.pause(1000) |
|
||||
|
|
||||
// Dashboard EmptyState
|
|
||||
const title_dashboard_empty = await app.client.getText('[data-e2e=dashboard_empty_title]') |
|
||||
expect(title_dashboard_empty).toEqual('Add accounts to your portfolio') |
|
||||
|
|
||||
// Open Settings
|
|
||||
await app.client.click('[data-e2e=setting_button]') |
|
||||
await app.client.pause(1000) |
|
||||
const title_settings = await app.client.getText('[data-e2e=settings_title]') |
|
||||
expect(title_settings).toEqual('Settings') |
|
||||
|
|
||||
// DevMode ON
|
|
||||
await app.client.click('[data-e2e=devMode_button]') |
|
||||
await app.client.pause(500) |
|
||||
}, |
|
||||
TIMEOUT, |
|
||||
) |
|
||||
}) |
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,47 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# get app version |
||||
|
ledgerLiveVersion=$(grep version package.json | cut -d : -f 2 | sed -E 's/.*"([^"]*)".*/\1/g') |
||||
|
|
||||
|
# OS settings |
||||
|
if [[ $(uname) == 'Darwin' ]]; then \ |
||||
|
settingsPath=~/Library/Application\ Support/Ledger\ Live/ |
||||
|
appPath="/Applications/Ledger Live.app/Contents/MacOS/Ledger Live" |
||||
|
elif [[ $(uname) == 'Linux' ]]; then \ |
||||
|
settingsPath="$HOME/.config/Ledger Live" |
||||
|
appPath="$HOME/apps/ledger-live-desktop-$ledgerLiveVersion-linux-x86_64.AppImage" |
||||
|
else \ |
||||
|
settingsPath="%AppData\\Roaming\\Ledger Live" |
||||
|
appPath="C:\\Program Files\\Ledger Live\\Ledger Live.exe" |
||||
|
fi |
||||
|
|
||||
|
# clean Ledger Live Application settings directory |
||||
|
rm -rf "$settingsPath" |
||||
|
mkdir "$settingsPath" |
||||
|
|
||||
|
# Copy app.json init file for testing |
||||
|
cp test-e2e/sync/data/empty-app.json "$settingsPath/app.json" |
||||
|
|
||||
|
# Start Ledger Live Desktop app |
||||
|
"$appPath" & |
||||
|
lastPid=$! |
||||
|
|
||||
|
# wait for sync |
||||
|
electron ./test-e2e/sync/wait-sync.js |
||||
|
returnCode=$? |
||||
|
|
||||
|
# kill Ledger Live Desktop process |
||||
|
kill -9 $lastPid |
||||
|
|
||||
|
if [[ $returnCode = 0 ]]; then |
||||
|
echo "[OK] Sync finished" |
||||
|
else |
||||
|
echo "[x] Sync failed" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Copy app.json file to test folder |
||||
|
cp "$settingsPath"/app.json test-e2e/sync/data/actual-app.json |
||||
|
|
||||
|
# compare new app.json with expected_app.json |
||||
|
./node_modules/.bin/jest test-e2e/sync/sync-accounts.spec.js |
@ -0,0 +1,66 @@ |
|||||
|
const pick = require('lodash/pick') |
||||
|
|
||||
|
const ACCOUNTS_FIELDS = [ |
||||
|
'archived', |
||||
|
'freshAddress', |
||||
|
'freshAddressPath', |
||||
|
'id', |
||||
|
'index', |
||||
|
'isSegwit', |
||||
|
'name', |
||||
|
'path', |
||||
|
'xpub', |
||||
|
'operations', |
||||
|
'currencyId', |
||||
|
'unitMagnitude', |
||||
|
'balance', |
||||
|
] |
||||
|
|
||||
|
const OPS_FIELDS = ['id', 'hash', 'accountId', 'type', 'senders', 'recipients', 'value', 'fee'] |
||||
|
|
||||
|
const OP_SORT = (a, b) => { |
||||
|
const aHash = getOpHash(a) |
||||
|
const bHash = getOpHash(b) |
||||
|
if (aHash < bHash) return -1 |
||||
|
if (aHash > bHash) return 1 |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
const ACCOUNT_SORT = (a, b) => { |
||||
|
const aHash = getAccountHash(a) |
||||
|
const bHash = getAccountHash(b) |
||||
|
if (aHash < bHash) return -1 |
||||
|
if (aHash > bHash) return 1 |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
describe('sync accounts', () => { |
||||
|
test('should give the same app.json', () => { |
||||
|
const expected = getSanitized('./data/expected-app.json') |
||||
|
const actual = getSanitized('./data/actual-app.json') |
||||
|
expect(actual).toEqual(expected) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
function getSanitized(filePath) { |
||||
|
const data = require(`${filePath}`) // eslint-disable-line import/no-dynamic-require
|
||||
|
const accounts = data.data.accounts.map(a => a.data) |
||||
|
accounts.sort(ACCOUNT_SORT) |
||||
|
return accounts |
||||
|
.map(a => pick(a, ACCOUNTS_FIELDS)) |
||||
|
.map(a => { |
||||
|
a.operations.sort(OP_SORT) |
||||
|
return { |
||||
|
...a, |
||||
|
operations: a.operations.map(o => pick(o, OPS_FIELDS)), |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function getOpHash(op) { |
||||
|
return `${op.accountId}--${op.hash}--${op.type}` |
||||
|
} |
||||
|
|
||||
|
function getAccountHash(account) { |
||||
|
return `${account.name}` |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
/* eslint-disable no-console */ |
||||
|
|
||||
|
const electron = require('electron') |
||||
|
const fs = require('fs') |
||||
|
const path = require('path') |
||||
|
const moment = require('moment') |
||||
|
|
||||
|
const delay = ms => new Promise(f => setTimeout(f, ms)) |
||||
|
|
||||
|
const MIN_TIME_DIFF = 1 * 1000 * 90 // 1.5 minute
|
||||
|
const PING_INTERVAL = 1 * 1000 // 1 seconds
|
||||
|
|
||||
|
async function waitForSync() { |
||||
|
let MAX_RETRIES = 100 |
||||
|
const userDataDirectory = electron.app.getPath('userData') |
||||
|
const tmpAppJSONPath = path.resolve(userDataDirectory, 'app.json') |
||||
|
const appJSONPath = tmpAppJSONPath.replace('/Electron/', '/Ledger Live/') |
||||
|
|
||||
|
function check() { |
||||
|
const appJSONContent = fs.readFileSync(appJSONPath, 'utf-8') |
||||
|
const appJSONParsed = JSON.parse(appJSONContent) |
||||
|
const mapped = appJSONParsed.data.accounts.map(a => ({ |
||||
|
name: a.data.name, |
||||
|
lastSyncDate: a.data.lastSyncDate, |
||||
|
})) |
||||
|
const now = Date.now() |
||||
|
const areAllSync = mapped.every(account => { |
||||
|
const diff = now - new Date(account.lastSyncDate).getTime() |
||||
|
if (diff <= MIN_TIME_DIFF) return true |
||||
|
console.log(`[${account.name}] synced ${moment(account.lastSyncDate).fromNow()} (${moment(account.lastSyncDate).format('YYYY-MM-DD HH:mm:ss')})`) |
||||
|
return false |
||||
|
}) |
||||
|
return areAllSync |
||||
|
} |
||||
|
|
||||
|
while (!check()) { |
||||
|
MAX_RETRIES-- |
||||
|
if (!MAX_RETRIES) { |
||||
|
console.log(`x Too much retries. Exitting.`) |
||||
|
process.exit(1) |
||||
|
} |
||||
|
await delay(PING_INTERVAL) |
||||
|
} |
||||
|
|
||||
|
process.exit(0) |
||||
|
} |
||||
|
|
||||
|
waitForSync() |
Loading…
Reference in new issue