From 29122d1f8fa39812b2f730e5d2ca0fb2543cc661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Rutkowski?= Date: Fri, 4 Oct 2019 17:03:36 +0200 Subject: [PATCH] Export account (#166) * setup tests * export account WIP * Fix linter errors. * Frontend test for Export component. * Test for account export by background handler. * Add password to export. * Add simple account export functionality. * Add dedicated selectors. * Improve export view layout * make export textarea bigger * Fix linter error. * Update wrapper in test to make it pass. * test refactoring * add copying exported JSON to clipboard * lint fix * add UI tests * fix dependencies * refactoring * copyright headers; minor refactorings * make input field blink after entering wrong password * download exported JSON * simplify exported JSON download * removed redundant tests * disable export functionality for external accounts * add export restrictions tests * tests refactoring * after successful export go back to main screen --- __mocks__/extensionizer.ts | 31 +++++ babel.config.js | 2 +- jest.config.js | 6 +- package.json | 2 +- packages/extension-ui/package.json | 6 + .../src/Popup/Accounts/Account.test.tsx | 42 ++++++ .../src/Popup/Accounts/Account.tsx | 3 +- .../extension-ui/src/Popup/Export.test.tsx | 55 ++++++++ packages/extension-ui/src/Popup/Export.tsx | 63 +++++++++ packages/extension-ui/src/Popup/index.tsx | 2 + .../extension-ui/src/components/TextArea.tsx | 4 +- packages/extension-ui/src/messaging.test.ts | 24 ++++ packages/extension-ui/src/messaging.ts | 4 + .../src/background/handlers/Extension.test.ts | 28 ++++ .../src/background/handlers/Extension.ts | 30 +++- packages/extension/src/background/types.ts | 10 ++ yarn.lock | 128 +++++++++++++++++- 17 files changed, 431 insertions(+), 9 deletions(-) create mode 100644 __mocks__/extensionizer.ts create mode 100644 packages/extension-ui/src/Popup/Accounts/Account.test.tsx create mode 100644 packages/extension-ui/src/Popup/Export.test.tsx create mode 100644 packages/extension-ui/src/Popup/Export.tsx create mode 100644 packages/extension-ui/src/messaging.test.ts create mode 100644 packages/extension/src/background/handlers/Extension.test.ts diff --git a/__mocks__/extensionizer.ts b/__mocks__/extensionizer.ts new file mode 100644 index 00000000..55316f6b --- /dev/null +++ b/__mocks__/extensionizer.ts @@ -0,0 +1,31 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import chrome from 'sinon-chrome'; + +class MessagingFake { + private listeners: any = []; + + get onMessage (): any { + return { + addListener: (cb: any): any => this.listeners.push(cb) + }; + } + + get onDisconnect (): any { + return { + addListener: (): any => {} + }; + } + + postMessage (data: any): void { + this.listeners.forEach((cb: any) => cb.call(this, data)); + } +} + +const messagingFake = new MessagingFake(); +chrome.runtime.connect.returns(messagingFake); + +export default chrome; diff --git a/babel.config.js b/babel.config.js index 1fbbeda8..1646d4f0 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: '@polkadot/dev/config/babel' + extends: '@polkadot/dev-react/config/babel' }; diff --git a/jest.config.js b/jest.config.js index 2be2f728..76233aa5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,11 +4,15 @@ const config = require('@polkadot/dev/config/jest'); module.exports = Object.assign({}, config, { moduleNameMapper: { '@polkadot/extension-(chains|dapp|inject|ui)(.*)$': '/packages/extension-$1/src/$2', + '@polkadot/extension(.*)$': '/packages/extension/src/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object', '\\.(css|less)$': 'empty/object' }, modulePathIgnorePatterns: [ '/packages/extension/build', - '/packages/extension-ui/build' + '/packages/extension-ui/build', + '/packages/extension-chains/build', + '/packages/extension-inject/build', + '/packages/extension-dapp/build' ] }); diff --git a/package.json b/package.json index a9d94917..e1ed99e7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "clean": "polkadot-dev-clean-build", "postinstall": "polkadot-dev-yarn-only", "start": "yarn watch", - "test": "echo \"no tests... yet\"", + "test": "jest", "watch": "cd packages/extension && webpack --config webpack.config.js --watch" }, "devDependencies": { diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json index 51888447..b6a89a14 100644 --- a/packages/extension-ui/package.json +++ b/packages/extension-ui/package.json @@ -12,5 +12,11 @@ "@types/react-router-dom": "^5.1.0", "react-router": "^5.1.0", "react-router-dom": "^5.1.0" + }, + "devDependencies": { + "@types/enzyme": "^3.10.3", + "@types/enzyme-adapter-react-16": "^1.0.5", + "@types/sinon-chrome": "^2.2.6", + "sinon-chrome": "^3.0.1" } } diff --git a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx new file mode 100644 index 00000000..ffb67c6c --- /dev/null +++ b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx @@ -0,0 +1,42 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import Adapter from 'enzyme-adapter-react-16'; +import { configure, mount, ReactWrapper } from 'enzyme'; +import { Link } from '@polkadot/extension-ui/components'; +import { MemoryRouter } from 'react-router'; +import React from 'react'; + +import Account from './Account'; + +configure({ adapter: new Adapter() }); + +describe('Account component', () => { + let wrapper: ReactWrapper; + const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mountAccountComponent = (additionalAccountProperties: any): ReactWrapper => mount( + + + + ); + + it('shows Export option if account is not external', () => { + wrapper = mountAccountComponent({ isExternal: false }); + + expect(wrapper.find(Link).length).toBe(3); + expect(wrapper.find(Link).at(0).text()).toContain('Edit'); + expect(wrapper.find(Link).at(1).text()).toContain('Export'); + expect(wrapper.find(Link).at(2).text()).toContain('Forget'); + }); + + it('does not show Export option if account is external', () => { + wrapper = mountAccountComponent({ isExternal: true }); + + expect(wrapper.find(Link).length).toBe(2); + expect(wrapper.find(Link).at(0).text()).toContain('Edit'); + expect(wrapper.find(Link).at(1).text()).toContain('Forget'); + }); +}); diff --git a/packages/extension-ui/src/Popup/Accounts/Account.tsx b/packages/extension-ui/src/Popup/Accounts/Account.tsx index b8c30982..51433a60 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.tsx @@ -16,7 +16,7 @@ interface Props extends AccountJson { className?: string; } -function Account ({ address, className }: Props): React.ReactElement { +function Account ({ address, className, isExternal }: Props): React.ReactElement { const onAction = useContext(ActionContext); const [isEditing, setEditing] = useState(false); const [editedName, setName] = useState(null); @@ -50,6 +50,7 @@ function Account ({ address, className }: Props): React.ReactElement { )} Edit + {!isExternal && Export} Forget diff --git a/packages/extension-ui/src/Popup/Export.test.tsx b/packages/extension-ui/src/Popup/Export.test.tsx new file mode 100644 index 00000000..9159dc8e --- /dev/null +++ b/packages/extension-ui/src/Popup/Export.test.tsx @@ -0,0 +1,55 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { act } from 'react-dom/test-utils'; +import Adapter from 'enzyme-adapter-react-16'; +import { configure, mount, ReactWrapper } from 'enzyme'; +import { MemoryRouter, Route } from 'react-router'; +import React from 'react'; + +import { Button } from '../components'; +import Export from './Export'; +import { exportAccount } from '../messaging'; + +configure({ adapter: new Adapter() }); +jest.mock('../messaging'); +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const flushAllPromises = (): Promise => new Promise(resolve => setImmediate(resolve)); + +describe('Export component', () => { + let wrapper: ReactWrapper; + const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5'; + + const enterPassword = (password = 'any password'): void => { + wrapper.find('[data-export-password] input').simulate('change', { target: { value: password } }); + }; + + beforeEach(() => { + (exportAccount as jest.Mock).mockResolvedValue({ exportedJson: '{ "meta": { "name": "account_name" } }' }); + + wrapper = mount( + + ); + }); + + it('creates export message on button press', async () => { + enterPassword('passw0rd'); + wrapper.find('[data-export-button] button').simulate('click'); + await act(flushAllPromises); + + expect(exportAccount).toHaveBeenCalledWith(VALID_ADDRESS, 'passw0rd'); + }); + + it('button is disabled before any password is typed', () => { + expect(wrapper.find(Button).prop('isDisabled')).toBe(true); + }); + + it('button is enabled after password is typed', async () => { + enterPassword(); + await act(flushAllPromises); + + expect(wrapper.find(Button).prop('isDisabled')).toBe(false); + }); +}); diff --git a/packages/extension-ui/src/Popup/Export.tsx b/packages/extension-ui/src/Popup/Export.tsx new file mode 100644 index 00000000..d9d805a2 --- /dev/null +++ b/packages/extension-ui/src/Popup/Export.tsx @@ -0,0 +1,63 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import React, { useContext, useState } from 'react'; +import { RouteComponentProps, withRouter } from 'react-router'; + +import { ActionContext, Address, Button, Header, Input, Tip } from '../components'; +import { exportAccount } from '../messaging'; +import { Back } from '../partials'; + +const MIN_LENGTH = 6; + +type Props = RouteComponentProps<{ address: string }>; + +function Export ({ match: { params: { address } } }: Props): React.ReactElement { + const onAction = useContext(ActionContext); + + const [pass, setPass] = useState(''); + const [wrongPasswordHighlight, setWrongPasswordHighlight] = useState(false); + + const _onExportButtonClick = (): Promise => + exportAccount(address, pass) + .then(({ exportedJson }) => { + const element = document.createElement('a'); + element.href = `data:text/plain;charset=utf-8,${exportedJson}`; + element.download = `${JSON.parse(exportedJson).meta.name}_exported_account_${Date.now()}.json`; + element.click(); + onAction('/'); + }) + .catch((error: Error) => { + console.error(error); + setWrongPasswordHighlight(true); + setTimeout(() => setWrongPasswordHighlight(false), 100); + }); + + return ( +
+
+ +
+ You are exporting your account. Keep it safe and don't share it with anyone. + +
+
+ ); +} + +export default withRouter(Export); diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index 82c829a8..79efb7dd 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -15,6 +15,7 @@ import { subscribeAccounts, subscribeAuthorize, subscribeSigning } from '../mess import Accounts from './Accounts'; import Authorize from './Authorize'; import Create from './Create'; +import Export from './Export'; import Forget from './Forget'; import ImportQr from './ImportQr'; import ImportSeed from './ImportSeed'; @@ -85,6 +86,7 @@ export default function Popup (): React.ReactElement<{}> { + diff --git a/packages/extension-ui/src/components/TextArea.tsx b/packages/extension-ui/src/components/TextArea.tsx index af1d97fb..98db9d8d 100644 --- a/packages/extension-ui/src/components/TextArea.tsx +++ b/packages/extension-ui/src/components/TextArea.tsx @@ -13,12 +13,13 @@ interface Props { isError?: boolean; isFocussed?: boolean; isReadOnly?: boolean; + rowsCount?: number; label: string; onChange?: (value: string) => void; value?: string; } -function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value }: Props): React.ReactElement { +function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value, rowsCount }: Props): React.ReactElement { const _onChange = ({ target: { value } }: React.ChangeEvent): void => { onChange && onChange(value.trim()); }; @@ -33,6 +34,7 @@ function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value } onChange={_onChange} readOnly={isReadOnly} value={value} + rows={rowsCount || 2} /> ); diff --git a/packages/extension-ui/src/messaging.test.ts b/packages/extension-ui/src/messaging.test.ts new file mode 100644 index 00000000..83c699c3 --- /dev/null +++ b/packages/extension-ui/src/messaging.test.ts @@ -0,0 +1,24 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import Adapter from 'enzyme-adapter-react-16'; +import { configure } from 'enzyme'; +import extensionizer from 'extensionizer'; +import { exportAccount } from './messaging'; + +configure({ adapter: new Adapter() }); + +describe('messaging sends message to background via extension port for', () => { + test('exportAccount', () => { + const callback = jest.fn(); + extensionizer.runtime.connect().onMessage.addListener(callback); + + exportAccount('HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', 'passw0rd'); + + expect(callback).toHaveBeenCalledWith(expect.objectContaining({ + message: 'pri(accounts.export)', + request: { address: 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', password: 'passw0rd' } + })); + }); +}); diff --git a/packages/extension-ui/src/messaging.ts b/packages/extension-ui/src/messaging.ts index aad9e20f..2eae265a 100644 --- a/packages/extension-ui/src/messaging.ts +++ b/packages/extension-ui/src/messaging.ts @@ -66,6 +66,10 @@ export async function forgetAccount (address: string): Promise { return sendMessage('pri(accounts.forget)', { address }); } +export async function exportAccount (address: string, password: string): Promise<{ exportedJson: string }> { + return sendMessage('pri(accounts.export)', { address, password }); +} + export async function rejectAuthRequest (id: string): Promise { return sendMessage('pri(authorize.reject)', { id }); } diff --git a/packages/extension/src/background/handlers/Extension.test.ts b/packages/extension/src/background/handlers/Extension.test.ts new file mode 100644 index 00000000..d094ae04 --- /dev/null +++ b/packages/extension/src/background/handlers/Extension.test.ts @@ -0,0 +1,28 @@ +import Extension from '@polkadot/extension/background/handlers/Extension'; +import keyring from '@polkadot/ui-keyring'; +import { cryptoWaitReady } from '@polkadot/util-crypto'; +import State from '@polkadot/extension/background/handlers/State'; +import { ResponseAccountExport } from '@polkadot/extension/background/types'; +import ExtensionStore from '@polkadot/ui-keyring/stores/Extension'; + +describe('Extension', () => { + async function createExtension (): Promise { + await cryptoWaitReady(); + keyring.loadAll({ store: new ExtensionStore() }); + return new Extension(new State()); + } + + test('exports account from keyring', async () => { + const extension = await createExtension(); + const { pair: { address } } = keyring.addUri( + 'seed sock milk update focus rotate barely fade car face mechanic mercy', 'passw0rd' + ); + const result = await extension.handle('id', 'pri(accounts.export)', { + address, + password: 'passw0rd' + }, {} as chrome.runtime.Port) as ResponseAccountExport; + + expect(result.exportedJson).toContain(address); + expect(result.exportedJson).toContain('"encoded"'); + }); +}); diff --git a/packages/extension/src/background/handlers/Extension.ts b/packages/extension/src/background/handlers/Extension.ts index 2256d48b..136b8f14 100644 --- a/packages/extension/src/background/handlers/Extension.ts +++ b/packages/extension/src/background/handlers/Extension.ts @@ -3,7 +3,28 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; -import { AccountJson, AuthorizeRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAuthorizeApprove, RequestAuthorizeReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, ResponseSeedCreate, RequestSeedValidate, ResponseSeedValidate, RequestAccountForget, SigningRequest, RequestTypes, ResponseTypes, MessageTypes } from '../types'; +import { + AccountJson, + AuthorizeRequest, + RequestAccountCreateExternal, + RequestAccountCreateSuri, + RequestAccountEdit, + RequestAuthorizeApprove, + RequestAuthorizeReject, + RequestSigningApprovePassword, + RequestSigningApproveSignature, + RequestSigningCancel, + RequestSeedCreate, + ResponseSeedCreate, + RequestSeedValidate, + ResponseSeedValidate, + RequestAccountForget, + SigningRequest, + RequestTypes, + ResponseTypes, + MessageTypes, + RequestAccountExport, ResponseAccountExport +} from '../types'; import extension from 'extensionizer'; import keyring from '@polkadot/ui-keyring'; @@ -60,6 +81,10 @@ export default class Extension { return true; } + private accountsExport ({ address, password }: RequestAccountExport): ResponseAccountExport { + return { exportedJson: JSON.stringify(keyring.backupAccount(keyring.getPair(address), password), null, 2) }; + } + // FIXME This looks very much like what we have in Tabs private accountsSubscribe (id: string, port: chrome.runtime.Port): boolean { const cb = createSubscription<'pri(accounts.subscribe)'>(id, port); @@ -233,6 +258,9 @@ export default class Extension { case 'pri(accounts.forget)': return this.accountsForget(request as RequestAccountForget); + case 'pri(accounts.export)': + return this.accountsExport(request as RequestAccountExport); + case 'pri(accounts.edit)': return this.accountsEdit(request as RequestAccountEdit); diff --git a/packages/extension/src/background/types.ts b/packages/extension/src/background/types.ts index 0d7b6f66..37b3fc2b 100644 --- a/packages/extension/src/background/types.ts +++ b/packages/extension/src/background/types.ts @@ -47,6 +47,7 @@ export interface RequestSignatures { 'pri(accounts.create.suri)': [RequestAccountCreateSuri, boolean]; 'pri(accounts.edit)': [RequestAccountEdit, boolean]; 'pri(accounts.forget)': [RequestAccountForget, boolean]; + 'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport]; 'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]]; 'pri(authorize.approve)': [RequestAuthorizeApprove, boolean]; 'pri(authorize.reject)': [RequestAuthorizeReject, boolean]; @@ -120,6 +121,11 @@ export interface RequestAccountForget { address: string; } +export interface RequestAccountExport { + address: string; + password: string; +} + export type RequestAccountList = null; export type RequestAccountSubscribe = null; @@ -193,6 +199,10 @@ export interface ResponseSeedValidate { suri: string; } +export interface ResponseAccountExport { + exportedJson: string; +} + // Subscriptions export type SubscriptionMessageTypes = NoUndefinedValues<{ diff --git a/yarn.lock b/yarn.lock index 222f281b..964c95a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2264,6 +2264,35 @@ resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-0.14.1.tgz#f4923bba22d7c68a4be3575ba27790947b212633" integrity sha512-Xng7L2Z8TNZa/5g6pot4O06Jf0ohQRZdvfl8eQL+E/L2mcqJYC1IjkMxJBSBuQEV7hisWzh9mHOy5WCcgPk29Q== +"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" + integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/formatio@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.1.tgz#52310f2f9bcbc67bdac18c94ad4901b95fde267e" + integrity sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" + +"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" + integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== + dependencies: + "@sinonjs/commons" "^1.3.0" + array-from "^2.1.1" + lodash "^4.17.15" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@types/babel__core@^7.1.0": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" @@ -2325,7 +2354,14 @@ dependencies: "@types/base-x" "*" -"@types/chrome@^0.0.89": +"@types/cheerio@*": + version "0.22.13" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6" + integrity sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA== + dependencies: + "@types/node" "*" + +"@types/chrome@*", "@types/chrome@^0.0.89": version "0.0.89" resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.89.tgz#0d5baef543aaa0715b345b57b86cba6ca708b524" integrity sha512-nWgL4U38SnIuMpG2rKMCGDknfZb193KFY15imqHUVFF4IJAVXvdEJOOgtcX9v2XsAaZiz2UUOInvWk5x1DhENQ== @@ -2351,6 +2387,21 @@ dependencies: "@types/color-convert" "*" +"@types/enzyme-adapter-react-16@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5" + integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg== + dependencies: + "@types/enzyme" "*" + +"@types/enzyme@*", "@types/enzyme@^3.10.3": + version "3.10.3" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.3.tgz#02b6c5ac7d0472005944a652e79045e2f6c66804" + integrity sha512-f/Kcb84sZOSZiBPCkr4He9/cpuSLcKRyQaEE20Q30Prx0Dn6wcyMAWI0yofL6yvd9Ht9G7EVkQeRqK0n5w8ILw== + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -2564,6 +2615,19 @@ dependencies: "@types/node" "*" +"@types/sinon-chrome@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/sinon-chrome/-/sinon-chrome-2.2.6.tgz#0ce2b8caea8d6ec0a2f9e20ca72c820b590bef81" + integrity sha512-R4FM7ywHxsqkHQlGdM42etVouSEUxZTUyXAheAyEe19DOGkEwNk7bIWi9aGc6WToIkQcFk/3O9aXCQh1YbgcMQ== + dependencies: + "@types/chrome" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" + integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -3326,6 +3390,11 @@ array-flatten@^2.1.0: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== +array-from@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -5350,6 +5419,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -8615,6 +8689,11 @@ jsx-ast-utils@^2.1.0: array-includes "^3.0.3" object.assign "^4.1.0" +just-extend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" + integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw== + keygrip@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" @@ -9011,7 +9090,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1: +lodash@^4.15.0, lodash@^4.16.3, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -9031,6 +9110,11 @@ loglevelnext@^3.0.0: resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04" integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA== +lolex@^4.1.0, lolex@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" + integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -9730,6 +9814,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652" + integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA== + dependencies: + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + lolex "^4.1.0" + path-to-regexp "^1.7.0" + no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -12427,6 +12522,28 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sinon-chrome@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/sinon-chrome/-/sinon-chrome-3.0.1.tgz#9fb14c230fa0959cb280f9f589e9eda8ccfda1d9" + integrity sha512-NTEFhyuiWEMnRmIqldUiA2DhKn2EqnZxyEk5Ez5rBXj+Nl54aJ0MEmF4wjltrxecxd8zlNLxyE0HyLabev9JsQ== + dependencies: + lodash "^4.16.3" + sinon "^7.2.3" + urijs "^1.18.2" + +sinon@^7.2.3: + version "7.4.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.4.2.tgz#ecd54158fef2fcfbdb231a3fa55140e8cb02ad6c" + integrity sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ== + dependencies: + "@sinonjs/commons" "^1.4.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/samsam" "^3.3.3" + diff "^3.5.0" + lolex "^4.2.0" + nise "^1.5.2" + supports-color "^5.5.0" + sisteransi@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" @@ -13401,7 +13518,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -13640,6 +13757,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urijs@^1.18.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" + integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg== + urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"