packages/extension-ui/src/components/Address.spec.tsx
// Copyright 2019-2024 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals" />
import '@polkadot/extension-mocks/chrome';
import type { ReactWrapper } from 'enzyme';
import type { AccountJson } from '@polkadot/extension-base/background/types';
import type { IconTheme } from '@polkadot/react-identicon/types';
import type { HexString } from '@polkadot/util/types';
import type { Props as AddressComponentProps } from './Address.js';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import enzyme from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import * as messaging from '../messaging.js';
import * as MetadataCache from '../MetadataCache.js';
import { westendMetadata } from '../Popup/Signing/metadataMock.js';
import { flushAllPromises } from '../testHelpers.js';
import { buildHierarchy } from '../util/buildHierarchy.js';
import { DEFAULT_TYPE } from '../util/defaultType.js';
import getParentNameSuri from '../util/getParentNameSuri.js';
import { AccountContext, Address } from './index.js';
const { configure, mount } = enzyme;
// NOTE Required for spyOn when using @swc/jest
// https://github.com/swc-project/swc/issues/3843
// jest.mock('../messaging', (): Record<string, unknown> => ({
// __esModule: true,
// ...jest.requireActual('../messaging')
// }));
// jest.mock('../MetadataCache', (): Record<string, unknown> => ({
// __esModule: true,
// ...jest.requireActual('../MetadataCache')
// }));
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
configure({ adapter: new Adapter() });
interface AccountTestJson extends AccountJson {
expectedIconTheme: IconTheme
}
interface AccountTestGenesisJson extends AccountTestJson {
expectedEncodedAddress: string;
expectedNetworkLabel: string;
genesisHash: HexString;
}
const externalAccount = { address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', expectedIconTheme: 'polkadot', isExternal: true, name: 'External Account', type: 'sr25519' } as AccountJson;
const hardwareAccount = {
address: 'HDE6uFdw53SwUyfKSsjwZNmS2sziWMPuY6uJhGHcFzLYRaJ',
expectedIconTheme: 'polkadot',
// Kusama genesis hash
genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe',
isExternal: true,
isHardware: true,
name: 'Hardware Account',
type: 'sr25519'
} as AccountJson;
const accounts = [
{ address: '5HSDXAC3qEMkSzZK377sTD1zJhjaPiX5tNWppHx2RQMYkjaJ', expectedIconTheme: 'polkadot', name: 'ECDSA Account', type: 'ecdsa' },
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', expectedIconTheme: 'polkadot', name: 'Ed Account', type: 'ed25519' },
{ address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q', expectedIconTheme: 'polkadot', name: 'Parent Sr Account', type: 'sr25519' },
{ address: '0xd5D81CD4236a43F48A983fc5B895975c511f634D', expectedIconTheme: 'ethereum', name: 'Ethereum', type: 'ethereum' },
{ ...externalAccount },
{ ...hardwareAccount }
] as AccountTestJson[];
// With Westend genesis Hash
// This account isn't part of the generic test because Westend isn't a built in network
// The network would only be displayed if the corresponding metadata are known
const westEndAccount = {
address: 'Cs2LLqQ6DSRx8UPdVp6jny4DvwNqziBSowSu5Nb1u3R6Z7X',
expectedEncodedAddress: '5CMQg2VXTrRWCUewro13qqc45Lf93KtzzS6hWR6dY6pvMZNF',
expectedIconTheme: 'polkadot',
expectedNetworkLabel: 'Westend',
genesisHash: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e',
name: 'acc',
type: 'ed25519'
} as AccountTestGenesisJson;
const accountsWithGenesisHash = [
// with Polkadot genesis Hash
{
address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q',
expectedEncodedAddress: '15csxS8s2AqrX1etYMMspzF6V7hM56KEjUqfjJvWHP7YWkoF',
expectedIconTheme: 'polkadot',
expectedNetworkLabel: 'Polkadot',
genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
type: 'sr25519'
},
// with Kusama genesis Hash
{
address: '5DoYawpxt6aBy1pKAt1beLMrakqtbWMtG3NF6jwRR8uKJGqD',
expectedEncodedAddress: 'EKAFGAqWTb7ifdkwapeYHirjM88QBB4iRCzVQDNtw7p3bgF',
expectedIconTheme: 'polkadot',
expectedNetworkLabel: 'Kusama',
genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe',
type: 'sr25519'
},
// with Edgeware genesis Hash
{
address: '5GYQRJj3NUznYDzCduENRcocMsyxmb6tjb5xW87ZMErBe9R7',
expectedEncodedAddress: 'mzKNamvvJPM5ApxwGSYD5VjjtyfrB4g8fhMyCc29K37nuop',
expectedIconTheme: 'substrate',
expectedNetworkLabel: 'Edgeware',
genesisHash: '0x742a2ca70c2fda6cee4f8df98d64c4c670a052d9568058982dad9d5a7a135c5b',
type: 'sr25519'
}
] as AccountTestGenesisJson[];
const mountComponent = async (addressComponentProps: AddressComponentProps, contextAccounts: AccountJson[]): Promise<{
wrapper: ReactWrapper;
}> => {
const actionStub = jest.fn();
const { actions = actionStub } = addressComponentProps;
const wrapper = mount(
<AccountContext.Provider
value={{
accounts: contextAccounts,
hierarchy: buildHierarchy(contextAccounts)
}}
>
<Address
actions={actions}
{...addressComponentProps}
/>
</AccountContext.Provider>
);
await act(flushAllPromises);
wrapper.update();
return { wrapper };
};
const getWrapper = async (account: AccountJson, contextAccounts: AccountJson[], withAccountsInContext: boolean) => {
// the address component can query info about the account from the account context
// in this case, the account's address (any encoding) should suffice
// In case the account is not in the context, then more info are needed as props
// to display accurately
const mountedComponent = withAccountsInContext
// only the address is passed as props, the full acount info are loaded in the context
? await mountComponent({ address: account.address }, contextAccounts)
// the context is empty, all account's info are passed as props to the Address component
: await mountComponent(account, []);
return mountedComponent.wrapper;
};
const genericTestSuite = (account: AccountTestJson, withAccountsInContext = true) => {
let wrapper: ReactWrapper;
const { address, expectedIconTheme, name = '', type = DEFAULT_TYPE } = account;
describe(`Account ${withAccountsInContext ? 'in context from address' : 'from props'} (${name}) - ${type}`, () => {
beforeAll(async () => {
wrapper = await getWrapper(account, accounts, withAccountsInContext);
});
it('shows the account address and name', () => {
expect(wrapper.find('[data-field="address"]').text()).toEqual(address);
expect(wrapper.find('Name span').text()).toEqual(name);
});
it(`shows a ${expectedIconTheme} identicon`, () => {
expect(wrapper.find('Identicon').first().prop('iconTheme')).toEqual(expectedIconTheme);
});
it('can copy its address', () => {
// the first CopyToClipboard is from the identicon, the second from the copy button
expect(wrapper.find('CopyToClipboard').at(0).prop('text')).toEqual(address);
expect(wrapper.find('CopyToClipboard').at(1).prop('text')).toEqual(address);
});
it('has the account visiblity icon', () => {
expect(wrapper.find('FontAwesomeIcon.visibleIcon')).toHaveLength(1);
});
it('can hide the account', () => {
jest.spyOn(messaging, 'showAccount').mockImplementation(() => Promise.resolve(false));
const visibleIcon = wrapper.find('FontAwesomeIcon.visibleIcon');
const hiddenIcon = wrapper.find('FontAwesomeIcon.hiddenIcon');
expect(visibleIcon.exists()).toBe(true);
expect(hiddenIcon.exists()).toBe(false);
visibleIcon.simulate('click');
expect(messaging.showAccount).toHaveBeenCalledWith(address, false);
});
it('can show the account if hidden', async () => {
const additionalProps = { isHidden: true };
const mountedHiddenComponent = withAccountsInContext
? await mountComponent({ address, ...additionalProps }, accounts)
: await mountComponent({ ...account, ...additionalProps }, []);
const wrapperHidden = mountedHiddenComponent.wrapper;
jest.spyOn(messaging, 'showAccount').mockImplementation(() => Promise.resolve(true));
const visibleIcon = wrapperHidden.find('FontAwesomeIcon.visibleIcon');
const hiddenIcon = wrapperHidden.find('FontAwesomeIcon.hiddenIcon');
expect(visibleIcon.exists()).toBe(false);
expect(hiddenIcon.exists()).toBe(true);
hiddenIcon.simulate('click');
expect(messaging.showAccount).toHaveBeenCalledWith(address, true);
});
it('has settings button', () => {
expect(wrapper.find('.settings')).toHaveLength(1);
});
it('has no account hidding and settings button if no action is provided', async () => {
const additionalProps = { actions: null };
const mountedComponentWithoutAction = withAccountsInContext
? await mountComponent({ address, ...additionalProps }, accounts)
: await mountComponent({ ...account, ...additionalProps }, []);
wrapper = mountedComponentWithoutAction.wrapper;
expect(wrapper.find('.settings')).toHaveLength(0);
});
});
};
const genesisHashTestSuite = (account: AccountTestGenesisJson, withAccountsInContext = true) => {
const { expectedEncodedAddress, expectedIconTheme, expectedNetworkLabel } = account;
describe(`Account ${withAccountsInContext ? 'in context from address' : 'from props'} with ${expectedNetworkLabel} genesiHash`, () => {
let wrapper: ReactWrapper;
beforeAll(async () => {
wrapper = await getWrapper(account, accountsWithGenesisHash, withAccountsInContext);
});
it('shows the account address correctly encoded', () => {
expect(wrapper.find('[data-field="address"]').text()).toEqual(expectedEncodedAddress);
});
it(`shows a ${expectedIconTheme} identicon`, () => {
expect(wrapper.find('Identicon').first().prop('iconTheme')).toEqual(expectedIconTheme);
});
it('Copy buttons contain the encoded address', () => {
// the first CopyToClipboard is from the identicon, the second from the copy button
expect(wrapper.find('CopyToClipboard').at(0).prop('text')).toEqual(expectedEncodedAddress);
expect(wrapper.find('CopyToClipboard').at(1).prop('text')).toEqual(expectedEncodedAddress);
});
it('Network label shows the correct network', () => {
expect(wrapper.find('[data-field="chain"]').text()).toEqual(expectedNetworkLabel);
});
});
};
describe('Address', () => {
accounts.forEach((account) => {
genericTestSuite(account);
genericTestSuite(account, false);
});
accountsWithGenesisHash.forEach((account) => {
genesisHashTestSuite(account);
genesisHashTestSuite(account, false);
});
describe('External account', () => {
let wrapper: ReactWrapper;
beforeAll(async () => {
wrapper = await getWrapper(externalAccount, [], false);
});
it('has an icon in front of its name', () => {
expect(wrapper.find('Name').find('FontAwesomeIcon [data-icon="qrcode"]').exists()).toBe(true);
});
});
describe('Hardware wallet account', () => {
let wrapper: ReactWrapper;
beforeAll(async () => {
wrapper = await getWrapper(hardwareAccount, [], false);
});
it('has a usb icon in front of its name', () => {
expect(wrapper.find('Name').find('FontAwesomeIcon [data-icon="usb"]').exists()).toBe(true);
});
});
describe('Encoding and label based on Metadata', () => {
let wrapper: ReactWrapper;
beforeAll(async () => {
jest.spyOn(MetadataCache, 'getSavedMeta').mockImplementation(() => Promise.resolve(westendMetadata));
wrapper = await getWrapper(westEndAccount, [], false);
});
it('shows westend label with the correct color', () => {
const bannerChain = wrapper.find('[data-field="chain"]');
expect(bannerChain.text()).toEqual(westendMetadata.chain);
expect(bannerChain.prop('style')?.backgroundColor).toEqual(westendMetadata.color);
});
it('shows the account correctly reencoded', () => {
expect(wrapper.find('[data-field="address"]').text()).toEqual(westEndAccount.expectedEncodedAddress);
});
});
describe('Derived accounts', () => {
let wrapper: ReactWrapper;
const childAccount = {
address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q',
name: 'Luke',
parentName: 'Dark Vador',
suri: '//42',
type: 'sr25519'
} as AccountJson;
beforeAll(async () => {
wrapper = await getWrapper(childAccount, [], false);
});
it('shows the child\'s account address and name', () => {
expect(wrapper.find('[data-field="address"]').text()).toEqual(childAccount.address);
expect(wrapper.find('Name span').text()).toEqual(childAccount.name);
});
it('shows the parent account and suri', () => {
const expectedParentNameSuri = getParentNameSuri(childAccount.parentName, childAccount.suri);
expect(wrapper.find('.parentName').text()).toEqual(expectedParentNameSuri);
});
});
});