diff --git a/packages/apps/public/locales/en/app-accounts.json b/packages/apps/public/locales/en/app-accounts.json index 0812ec1b1c..a0381285b6 100644 --- a/packages/apps/public/locales/en/app-accounts.json +++ b/packages/apps/public/locales/en/app-accounts.json @@ -88,6 +88,7 @@ "Low quality": "Low quality", "Make Transfer": "Make Transfer", "Make recoverable": "Make recoverable", + "Manage delegation": "Manage delegation", "Manage proxies": "Manage proxies", "Mnemonic": "Mnemonic", "Multisig": "Multisig", @@ -239,6 +240,7 @@ "Validator": "Validator", "Vanity generator": "Vanity generator", "Verify the password entered above.": "Verify the password entered above.", + "View pending approvals": "View pending approvals", "We will provide you with a generated backup file after your account is created. As long as you have access to your account you can always download this file later by clicking on \"Backup\" button from the Accounts section.": "We will provide you with a generated backup file after your account is created. As long as you have access to your account you can always download this file later by clicking on \"Backup\" button from the Accounts section.", "With the keep-alive option set, the account is protected against removal due to low balances.": "With the keep-alive option set, the account is protected against removal due to low balances.", "You can set a custom derivation index for this account": "You can set a custom derivation index for this account", diff --git a/packages/page-accounts/src/Accounts/Account.tsx b/packages/page-accounts/src/Accounts/Account.tsx index 60be75a269..f9ddd3088d 100644 --- a/packages/page-accounts/src/Accounts/Account.tsx +++ b/packages/page-accounts/src/Accounts/Account.tsx @@ -16,7 +16,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're import styled, { ThemeContext } from 'styled-components'; import { ApiPromise } from '@polkadot/api'; -import { AddressInfo, AddressSmall, Badge, Button, ChainLock, CryptoType, ExpandButton, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components'; +import { AddressInfo, AddressSmall, Badge, Button, ChainLock, CryptoType, ExpandButton, Forget, Icon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components'; import { useAccountInfo, useApi, useBalancesAll, useBestNumber, useCall, useLedger, useStakingInfo, useToggle } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { BN_ZERO, formatBalance, formatNumber, isFunction } from '@polkadot/util'; @@ -410,99 +410,111 @@ function Account ({ account: { address, meta }, className = '', delegation, filt /> - {meta.genesisHash - ? - : isDevelopment - ? ( +
+
+ {meta.genesisHash + ? + : isDevelopment + ? ( + ('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.')} + icon='wrench' + /> + ) + : ( + +

{t('This account is available on all networks. It is recommended to link to a specific network via the account options ("only this network" option) to limit availability. For accounts from an extension, set the network on the extension.')}

+

{t('This does not send any transaction, rather is only sets the genesis in the account JSON.')}

+
+ } + icon='exclamation-triangle' + /> + ) + } + {recoveryInfo && ( ('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.')} - icon='wrench' - /> - ) - : ( - -

{t('This account is available on all networks. It is recommended to link to a specific network via the account options ("only this network" option) to limit availability. For accounts from an extension, set the network on the extension.')}

-

{t('This does not send any transaction, rather is only sets the genesis in the account JSON.')}

+

{t('This account is recoverable, with the following friends:')}

+
+ {recoveryInfo.friends.map((friend, index): React.ReactNode => ( + + ))} +
+ + + + + + + + + + + + + + + +
{t('threshold')}{formatNumber(recoveryInfo.threshold)}
{t('delay')}{formatNumber(recoveryInfo.delayPeriod)}
{t('deposit')}{formatBalance(recoveryInfo.deposit)}
} - icon='exclamation-triangle' + icon='redo' /> - ) - } - {recoveryInfo && ( - -

{t('This account is recoverable, with the following friends:')}

-
- {recoveryInfo.friends.map((friend, index): React.ReactNode => ( - - ))} -
- - - - - - - - - - - - - - - -
{t('threshold')}{formatNumber(recoveryInfo.threshold)}
{t('delay')}{formatNumber(recoveryInfo.delayPeriod)}
{t('deposit')}{formatBalance(recoveryInfo.deposit)}
- - } - icon='shield' - /> - )} - {multiInfos && multiInfos.length !== 0 && ( - ('Multisig approvals pending')} - info={multiInfos.length} - /> - )} - {isProxied && !proxyInfo.hasOwned && ( - ('Proxied account has no owned proxies')} - info='0' - /> - )} - {delegation?.accountDelegated && ( - ('This account has a governance delegation')} - icon='calendar-check' - onClick={toggleDelegate} - /> - )} - {!!proxy?.[0].length && api.api.tx.utility && ( - ('This account has {{proxyNumber}} proxy set.', { - replace: { - proxyNumber: proxy[0].length - } - })} - icon='arrow-right' - onClick={toggleProxyOverview} - /> - )} + )} + {isProxied && !proxyInfo.hasOwned && ( + ('Proxied account has no owned proxies')} + icon='sitemap' + info='0' + /> + )} + +
+ {multiInfos && multiInfos.length !== 0 && ( + ('Multisig approvals pending')} + hoverAction={t('View pending approvals')} + icon='file-signature' + info={multiInfos.length} + onClick={toggleMultisig} + /> + )} + {delegation?.accountDelegated && ( + ('This account has a governance delegation')} + hoverAction={t('Manage delegation')} + icon='calendar-check' + onClick={toggleDelegate} + /> + )} + {!!proxy?.[0].length && api.api.tx.utility && ( + ('This account has {{proxyNumber}} proxy set.', { + replace: { + proxyNumber: proxy[0].length + } + })} + hoverAction={t('Proxy overview')} + icon='sitemap' + onClick={toggleProxyOverview} + /> + )} +
+ .badges { + display: flex; + flex-direction: column; + justify-content: space-between; + + & > .info + .action { + margin-top: 0.5rem; + + &:empty { + margin-top: 0; + } + } + + & > .info:empty + .action { + margin-top: 0; + } + } `); diff --git a/packages/page-accounts/src/Accounts/index.spec.ts b/packages/page-accounts/src/Accounts/index.spec.ts index c8d7410d64..2ad14c7070 100644 --- a/packages/page-accounts/src/Accounts/index.spec.ts +++ b/packages/page-accounts/src/Accounts/index.spec.ts @@ -1,18 +1,25 @@ // Copyright 2017-2021 @polkadot/page-accounts authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { screen } from '@testing-library/react'; +import { fireEvent, screen, within } from '@testing-library/react'; import BN from 'bn.js'; +import { POLKADOT_GENESIS } from '@polkadot/apps-config'; import i18next from '@polkadot/react-components/i18n'; import { toShortAddress } from '@polkadot/react-components/util/toShortAddress'; -import { anAccountWithBalance, anAccountWithBalanceAndMeta, anAccountWithInfo, anAccountWithMeta, anAccountWithStaking } from '@polkadot/test-support/creation/account'; +import { AddressFlags } from '@polkadot/react-hooks/types'; +import { anAccountWithBalance, anAccountWithBalanceAndMeta, anAccountWithInfo, anAccountWithInfoAndMeta, anAccountWithMeta, anAccountWithStaking } from '@polkadot/test-support/creation/account'; import { makeStakingLedger as ledger } from '@polkadot/test-support/creation/stakingInfo/stakingLedger'; -import { alice, MemoryStore } from '@polkadot/test-support/keyring'; +import { alice, bob, MemoryStore } from '@polkadot/test-support/keyring'; import { Table } from '@polkadot/test-support/pagesElements'; import { balance, showBalance } from '@polkadot/test-support/utils/balance'; +import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks'; +import { u32 } from '@polkadot/types'; +import { TypeRegistry } from '@polkadot/types/create'; +import { AccountId, Multisig, ProxyDefinition, Timepoint, Voting, VotingDelegating } from '@polkadot/types/interfaces'; import { keyring } from '@polkadot/ui-keyring'; +import { AccountRow } from '../../test/pageElements/AccountRow'; import { AccountsPage } from '../../test/pages/accountsPage'; describe('Accounts page', () => { @@ -357,4 +364,160 @@ describe('Accounts page', () => { }); }); }); + + describe('badges', () => { + let accountRows: AccountRow[]; + + beforeEach(() => { + mockApiHooks.setMultisigApprovals([ + [new TypeRegistry().createType('Hash', POLKADOT_GENESIS), { + approvals: [bob as unknown as AccountId], + deposit: balance(927000000000000), + depositor: bob as unknown as AccountId, + when: { height: new BN(1190) as u32, index: new BN(1) as u32 } as Timepoint + } as Multisig + ] + ]); + mockApiHooks.setDelegations([{ asDelegating: { target: bob as unknown as AccountId } as unknown as VotingDelegating, isDelegating: true } as Voting]); + mockApiHooks.setProxies([[[{ delegate: alice as unknown as AccountId, proxyType: { isAny: true, isGovernance: true, isNonTransfer: true, isStaking: true, toNumber: () => 1 } } as unknown as ProxyDefinition], new BN(1)]]); + }); + describe('when genesis hash is not set', () => { + beforeEach(async () => { + accountsPage.renderAccountsWithDefaultAddresses( + anAccountWithInfoAndMeta({ flags: { isDevelopment: true } as AddressFlags }, { name: 'alice' }), + anAccountWithMeta({ name: 'bob' }) + ); + accountRows = await accountsPage.getAccountRows(); + }); + + describe('when isDevelopment flag', () => { + let aliceRow: AccountRow; + + beforeEach(async () => { + aliceRow = accountRows[0]; + await aliceRow.assertAccountName('ALICE'); + }); + + it('the development badge is displayed', async () => { + await aliceRow.assertBadge('wrench-badge'); + }); + + it('the all networks badge is not displayed', () => { + aliceRow.assertNoBadge('exclamation-triangle-badge'); + }); + + it('the regular badge is not displayed', () => { + aliceRow.assertNoBadge('transparent-badge'); + }); + }); + + describe('when no isDevelopment flag', () => { + let bobRow: AccountRow; + + beforeEach(async () => { + bobRow = accountRows[1]; + await bobRow.assertAccountName('BOB'); + }); + + it('the development badge is not displayed', () => { + bobRow.assertNoBadge('wrench-badge'); + }); + + it('the all networks badge is displayed', async () => { + await bobRow.assertBadge('exclamation-triangle-badge'); + }); + + it('the regular badge is not displayed', () => { + bobRow.assertNoBadge('transparent-badge'); + }); + }); + }); + + describe('when genesis hash set', () => { + beforeEach(async () => { + accountsPage.renderAccountsWithDefaultAddresses( + anAccountWithInfoAndMeta({ flags: { isDevelopment: true } as AddressFlags }, { genesisHash: 'someHash', name: 'charlie' }) + ); + accountRows = await accountsPage.getAccountRows(); + }); + + it('the development badge is not displayed', () => { + accountRows[0].assertNoBadge('wrench-badge'); + }); + + it('the all networks badge is not displayed', () => { + accountRows[0].assertNoBadge('exclamation-triangle-badge'); + }); + + it('the regular badge is displayed', async () => { + await accountRows[0].assertBadge('badge'); + }); + }); + + describe('show popups', () => { + beforeEach(async () => { + accountsPage.renderAccountsWithDefaultAddresses( + anAccountWithInfoAndMeta({ flags: { isDevelopment: true } as AddressFlags }, { name: 'alice', who: [] }) + ); + accountRows = await accountsPage.getAccountRows(); + }); + + it('development', async () => { + await accountRows[0].assertBadge('wrench-badge'); + const badgePopup = getPopupById(/wrench-badge-hover.*/); + + await within(badgePopup).findByText('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.'); + }); + + it('multisig approvals', async () => { + await accountRows[0].assertBadge('file-signature-badge'); + const badgePopup = getPopupById(/file-signature-badge-hover.*/); + const approvalsModalToggle = await within(badgePopup).findByText('View pending approvals'); + + fireEvent.click(approvalsModalToggle); + const modal = await screen.findByTestId('modal'); + + within(modal).getByText('Pending call hashes'); + expect(approvalsModalToggle).toHaveClass('purpleColor'); + }); + + it('delegate democracy vote', async () => { + await accountRows[0].assertBadge('calendar-check-badge'); + const badgePopup = getPopupById(/calendar-check-badge-hover.*/); + const delegateModalToggle = await within(badgePopup).findByText('Manage delegation'); + + fireEvent.click(delegateModalToggle); + const modal = await screen.findByTestId('modal'); + + within(modal).getByText('democracy vote delegation'); + expect(delegateModalToggle).toHaveClass('normalColor'); + }); + + it('proxy overview', async () => { + await accountRows[0].assertBadge('sitemap-badge'); + const badgePopup = getPopupById(/sitemap-badge-hover.*/); + const proxyOverviewToggle = await within(badgePopup).findByText('Proxy overview'); + + fireEvent.click(proxyOverviewToggle); + const modal = await screen.findByTestId('modal'); + + within(modal).getByText('Proxy overview'); + expect(proxyOverviewToggle).toHaveClass('normalColor'); + }); + + afterEach(() => { + mockApiHooks.setMultisigApprovals([]); + }); + }); + + function getPopupById (popupId: RegExp): HTMLElement { + const badgePopup = accountsPage.getById(popupId); + + if (!badgePopup) { + fail('badge popup should be found'); + } + + return badgePopup; + } + }); }); diff --git a/packages/page-accounts/src/Accounts/index.tsx b/packages/page-accounts/src/Accounts/index.tsx index 50149c97ae..0d610a27f4 100644 --- a/packages/page-accounts/src/Accounts/index.tsx +++ b/packages/page-accounts/src/Accounts/index.tsx @@ -3,14 +3,13 @@ import type BN from 'bn.js'; import type { ActionStatus } from '@polkadot/react-components/Status/types'; -import type { AccountId, ProxyDefinition, ProxyType, Voting } from '@polkadot/types/interfaces'; import type { AccountBalance, Delegation, SortedAccount } from '../types'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { Button, FilterInput, SortDropdown, SummaryBox, Table } from '@polkadot/react-components'; -import { useAccounts, useApi, useCall, useFavorites, useIpfs, useLedger, useLoadingDelay, useToggle } from '@polkadot/react-hooks'; +import { useAccounts, useApi, useDelegations, useFavorites, useIpfs, useLedger, useLoadingDelay, useProxies, useToggle } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { BN_ZERO } from '@polkadot/util'; @@ -63,15 +62,8 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen const [filterOn, setFilter] = useState(''); const [sortedAccounts, setSorted] = useState([]); const [{ sortBy, sortFromMax }, setSortBy] = useState(DEFAULT_SORT_CONTROLS); - const delegations = useCall(api.query.democracy?.votingOf?.multi, [allAccounts]); - const proxies = useCall<[ProxyDefinition[], BN][]>(api.query.proxy?.proxies.multi, [allAccounts], { - transform: (result: [([AccountId, ProxyType] | ProxyDefinition)[], BN][]): [ProxyDefinition[], BN][] => - api.tx.proxy.addProxy.meta.args.length === 3 - ? result as [ProxyDefinition[], BN][] - : (result as [[AccountId, ProxyType][], BN][]).map(([arr, bn]): [ProxyDefinition[], BN] => - [arr.map(([delegate, proxyType]): ProxyDefinition => api.createType('ProxyDefinition', { delegate, proxyType })), bn] - ) - }); + const delegations = useDelegations(); + const proxies = useProxies(); const isLoading = useLoadingDelay(); // We use favorites only to check if it includes some element, diff --git a/packages/react-components/src/Badge.tsx b/packages/react-components/src/Badge.tsx index 0ffe975f5f..33a856ed11 100644 --- a/packages/react-components/src/Badge.tsx +++ b/packages/react-components/src/Badge.tsx @@ -3,16 +3,18 @@ import type { IconName } from '@fortawesome/fontawesome-svg-core'; -import React, { useState } from 'react'; -import styled from 'styled-components'; +import React, { useContext, useMemo, useState } from 'react'; +import styled, { ThemeContext } from 'styled-components'; import Icon from './Icon'; import Tooltip from './Tooltip'; +import { ThemeDef } from './types'; interface Props { className?: string; - color: 'blue' | 'gray' | 'green' | 'highlight' | 'normal' | 'orange' | 'purple' | 'red' | 'transparent' | 'white'; + color?: 'blue' | 'gray' | 'green' | 'highlight' | 'normal' | 'orange' | 'purple' | 'red' | 'transparent' | 'white'; hover?: React.ReactNode; + hoverAction?: React.ReactNode; icon?: IconName; info?: React.ReactNode; isSmall?: boolean; @@ -21,23 +23,50 @@ interface Props { let badgeId = 0; -function Badge ({ className = '', color = 'normal', hover, icon, info, isSmall, onClick }: Props): React.ReactElement | null { - const [trigger] = useState(() => `badge-hover-${Date.now()}-${badgeId++}`); +function Badge ({ className = '', color = 'normal', hover, hoverAction, icon, info, isSmall, onClick }: Props): React.ReactElement | null { + const badgeTestId = `${icon ? `${icon}-` : ''}badge`; + const { theme } = useContext(ThemeContext); + + const [trigger] = useState(() => `${badgeTestId}-hover-${Date.now()}-${badgeId++}`); const extraProps = hover ? { 'data-for': trigger, 'data-tip': true } : {}; const isHighlight = color === 'highlight'; + const hoverContent = useMemo(() => ( +
+
{hover}
+ {hoverAction && ( + {hoverAction} + )} +
+ ), [color, hover, hoverAction, onClick]); + return (
-
{info || (icon && )}
+
+ {(icon && )} + {info} + {hoverAction && ( + + )} +
{hover && ( )} @@ -53,7 +82,7 @@ export default React.memo(styled(Badge)` font-size: 12px; height: 22px; line-height: 22px; - margin-right: 0.25rem; + margin-right: 0.43rem; min-width: 22px; padding: 0 4px; overflow: hidden; @@ -72,7 +101,7 @@ export default React.memo(styled(Badge)` width: 1em; } - &.isClickable { + &.isClickable:not(.withAction) { cursor: pointer; } @@ -127,4 +156,64 @@ export default React.memo(styled(Badge)` &.whiteColor { background: rgba(255, 255, 255, 0.3); } + + &.recovery, &.warning, &.information, &.important { + background-color: #FFFFFF; + + &.darkTheme { + background-color: #212227; + } + } + + &.recovery { + background-image: linear-gradient(0deg, rgba(17, 185, 74, 0.08), rgba(17, 185, 74, 0.08)); + color: #11B94A; + } + + &.warning { + background-image: linear-gradient(0deg, rgba(232, 111, 0, 0.08), rgba(232, 111, 0, 0.08)); + color: #FF7D01; + } + + &.information { + background-image: linear-gradient(0deg, rgba(226, 246, 255, 0.08), rgba(226, 246, 255, 0.08)); + color: #3BBEFF; + + &.lightTheme { + background-color: rgba(226, 246, 255, 1); + } + } + + &.important { + background: linear-gradient(0deg, rgba(230, 0, 122, 0.08), rgba(230, 0, 122, 0.08)), rgba(230, 0, 122, 0.01); + color: #E6007A; + } + + &.withAction.withIcon:not(.withInfo) { + width: 34px; + border-radius: 4px; + } + + &.withInfo.withIcon:not(.withAction) { + width: 34px; + border-radius: 18px; + } + + &.withAction.withIcon.withInfo { + width: 44px; + border-radius: 4px; + } + + &.withInfo .ui--Icon:not(.action-icon) { + margin-right: 4px; + } + + .hoverContent { + display: flex; + flex-direction: column; + } + + .action-icon { + margin-left: 4px; + } `); diff --git a/packages/react-components/src/Tooltip.tsx b/packages/react-components/src/Tooltip.tsx index f4603d5352..673d9f85b7 100644 --- a/packages/react-components/src/Tooltip.tsx +++ b/packages/react-components/src/Tooltip.tsx @@ -14,6 +14,7 @@ function rootElement () { interface Props { className?: string; + clickable?: boolean; dataFor?: string; effect?: 'solid' | 'float'; offset?: { @@ -27,7 +28,7 @@ interface Props { trigger: string; } -function Tooltip ({ className = '', effect = 'solid', offset, place = 'top', text, trigger }: Props): React.ReactElement | null { +function Tooltip ({ className = '', clickable = false, effect = 'solid', offset, place = 'top', text, trigger }: Props): React.ReactElement | null { const [tooltipContainer] = useState( typeof document === 'undefined' ? {} as HTMLElement // This hack is required for server side rendering @@ -47,6 +48,7 @@ function Tooltip ({ className = '', effect = 'solid', offset, place = 'top', tex return ReactDOM.createPortal( ` text-align: center; z-index: 1002; max-width: 300px; + + + &.accounts-badge { + background-color: var(--bg-menu) !important; + color: var(--color-text) !important; + box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%); + z-index: 999; + + &.place-top::after { + border-top-color: var(--bg-menu) !important; + } + + &.place-right::after { + border-right-color: var(--bg-menu) !important; + } + + a { + color: #3BBEFF; + + &.purpleColor { + color: #E6007A; + } + } } `; diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 732faa415e..5db4255f5c 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -20,6 +20,7 @@ export { useCollectiveInstance } from './useCollectiveInstance'; export { useCollectiveMembers } from './useCollectiveMembers'; export { useFormField } from './useFormField'; export { useDebounce } from './useDebounce'; +export { useDelegations } from './useDelegations'; export { useEventTrigger } from './useEventTrigger'; export { useExtrinsicTrigger } from './useExtrinsicTrigger'; export { useFavorites } from './useFavorites'; @@ -39,6 +40,7 @@ export { useOwnEraRewards } from './useOwnEraRewards'; export { useOwnStashes, useOwnStashIds } from './useOwnStashes'; export { useOwnStashInfos } from './useOwnStashInfos'; export { useParaApi } from './useParaApi'; +export { useProxies } from './useProxies'; export { useIsParasLinked, useParaEndpoints } from './useParaEndpoints'; export { usePassword } from './usePassword'; export { useRegistrars } from './useRegistrars'; diff --git a/packages/react-hooks/src/useDelegations.ts b/packages/react-hooks/src/useDelegations.ts new file mode 100644 index 0000000000..ef916844af --- /dev/null +++ b/packages/react-hooks/src/useDelegations.ts @@ -0,0 +1,12 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useAccounts, useApi, useCall } from '@polkadot/react-hooks'; +import { Voting } from '@polkadot/types/interfaces'; + +export function useDelegations (): Voting[] | undefined { + const { api } = useApi(); + const { allAccounts } = useAccounts(); + + return useCall(api.query.democracy?.votingOf?.multi, [allAccounts]); +} diff --git a/packages/react-hooks/src/useProxies.ts b/packages/react-hooks/src/useProxies.ts new file mode 100644 index 0000000000..c4799f74ea --- /dev/null +++ b/packages/react-hooks/src/useProxies.ts @@ -0,0 +1,26 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import BN from 'bn.js'; + +import { useAccounts } from '@polkadot/react-hooks/useAccounts'; +import { useApi } from '@polkadot/react-hooks/useApi'; +import { useCall } from '@polkadot/react-hooks/useCall'; +import { AccountId, ProxyDefinition, ProxyType } from '@polkadot/types/interfaces'; + +export function useProxies (): [ProxyDefinition[], BN][] | undefined { + const { api } = useApi(); + const { allAccounts } = useAccounts(); + + return useCall<[ProxyDefinition[], BN][]>(api.query.proxy?.proxies.multi, [allAccounts], { + transform: (result: [([AccountId, ProxyType] | ProxyDefinition)[], BN][]): [ProxyDefinition[], BN][] => + api.tx.proxy.addProxy.meta.args.length === 3 + ? result as [ProxyDefinition[], BN][] + : (result as [[AccountId, ProxyType][], BN][]).map(([arr, bn]): [ProxyDefinition[], BN] => + [arr.map(([delegate, proxyType]): ProxyDefinition => api.createType('ProxyDefinition', { + delegate, + proxyType + })), bn] + ) + }); +} diff --git a/packages/test-support/src/creation/account.ts b/packages/test-support/src/creation/account.ts index 470bfa4d74..3e3f84e1bd 100644 --- a/packages/test-support/src/creation/account.ts +++ b/packages/test-support/src/creation/account.ts @@ -13,11 +13,6 @@ export const anAccountWithBalance = (balance: Override): Acco balance }); -export const anAccountWithBalanceAndMeta = (balance: Override, meta: Override): AccountOverrides => ({ - balance, - meta -}); - export const anAccountWithInfo = (info: Override): AccountOverrides => ({ info }); @@ -29,3 +24,13 @@ export const anAccountWithMeta = (meta: Override): AccountOver export const anAccountWithStaking = (staking: Override): AccountOverrides => ({ staking }); + +export const anAccountWithBalanceAndMeta = (balance: Override, meta: Override): AccountOverrides => ({ + balance, + meta +}); + +export const anAccountWithInfoAndMeta = (info: Override, meta: Override): AccountOverrides => ({ + info, + meta +}); diff --git a/packages/test-support/src/pages/Page.tsx b/packages/test-support/src/pages/Page.tsx index b6f3d0928f..3aefd43716 100644 --- a/packages/test-support/src/pages/Page.tsx +++ b/packages/test-support/src/pages/Page.tsx @@ -1,7 +1,8 @@ // Copyright 2017-2021 @polkadot/page-accounts authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { render, RenderResult, screen } from '@testing-library/react'; +import { queryByAttribute, render, RenderResult, screen } from '@testing-library/react'; +import BN from 'bn.js'; import React, { Suspense } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; @@ -78,6 +79,19 @@ jest.mock('@polkadot/react-hooks/useSubidentities', () => ({ useSubidentities: () => mockApiHooks.subs })); +jest.mock('@polkadot/app-accounts/Accounts/useMultisigApprovals', () => ({ + __esModule: true, + default: () => mockApiHooks.multisigApprovals +})); + +jest.mock('@polkadot/react-hooks/useDelegations', () => ({ + useDelegations: () => mockApiHooks.delegations +})); + +jest.mock('@polkadot/react-hooks/useProxies', () => ({ + useProxies: () => mockApiHooks.proxies +})); + jest.mock('@polkadot/react-hooks/useSubidentities', () => ({ useSubidentities: () => mockApiHooks.subs })); @@ -108,6 +122,21 @@ export abstract class Page { const noop = () => Promise.resolve(() => { /**/ }); const mockApi: ApiProps = { api: { + consts: { + babe: { + expectedBlockTime: new BN(1) + }, + democracy: { + enactmentPeriod: new BN(1) + }, + proxy: { + proxyDepositBase: new BN(1), + proxyDepositFactor: new BN(1) + } + }, + createType: () => ({ + defKeys: [] + }), derive: { accounts: { info: noop @@ -136,8 +165,17 @@ export abstract class Page { }, registry: { chainDecimals: [12], chainTokens: ['Unit'] }, tx: { - council: { - } + council: {}, + democracy: { + delegate: noop + }, + multisig: { + approveAsMulti: Object.assign(noop, { meta: { args: [] } }) + }, + proxy: { + removeProxies: noop + }, + utility: noop } }, systemName: 'substrate' @@ -178,6 +216,13 @@ export abstract class Page { this.defaultAddresses.forEach((address) => keyring.forgetAccount(address)); } + getById (id: string | RegExp): HTMLElement | null { + this.assertRendered(); + const getById = queryByAttribute.bind(null, 'id'); + + return getById(this.renderResult?.container ?? fail('Page render failed'), id); + } + protected assertRendered (): void { if (this.renderResult === undefined) { throw new NotYetRendered(); diff --git a/packages/test-support/src/pagesElements/Row.ts b/packages/test-support/src/pagesElements/Row.ts index 3013c038ed..90e0812915 100644 --- a/packages/test-support/src/pagesElements/Row.ts +++ b/packages/test-support/src/pagesElements/Row.ts @@ -41,6 +41,14 @@ export class Row { } } + async assertBadge (expectedBadgeName: string): Promise { + await within(this.primaryRow).findByTestId(expectedBadgeName); + } + + assertNoBadge (badgeName: string): void { + expect(within(this.primaryRow).queryByTestId(badgeName)).toBeFalsy(); + } + async assertTags (expectedTagsContent: string): Promise { const actualTags = await within(this.detailsRow).findByTestId('tags'); @@ -59,6 +67,10 @@ export class Row { fireEvent.click(toggle); } + async getBadge (expectedBadgeName: string): Promise { + return within(this.primaryRow).findByTestId(`${expectedBadgeName}-badge`); + } + async openSidebar (): Promise { const accountName = await this.getAccountName(); diff --git a/packages/test-support/src/pagesElements/index.ts b/packages/test-support/src/pagesElements/index.ts index 12d5fe1119..2378f9c0eb 100644 --- a/packages/test-support/src/pagesElements/index.ts +++ b/packages/test-support/src/pagesElements/index.ts @@ -2,4 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 export * from './Row'; +export * from './Sidebar'; export * from './Table'; diff --git a/packages/test-support/src/utils/mockApiHooks.ts b/packages/test-support/src/utils/mockApiHooks.ts index 7098dd50e9..df6fb1570d 100644 --- a/packages/test-support/src/utils/mockApiHooks.ts +++ b/packages/test-support/src/utils/mockApiHooks.ts @@ -2,13 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 import type { Registrar } from '@polkadot/react-hooks/types'; -import type { RegistrationJudgement } from '@polkadot/types/interfaces'; +import type { ProxyDefinition, RegistrationJudgement, Voting } from '@polkadot/types/interfaces'; + +import BN from 'bn.js'; + +import { H256, Multisig } from '@polkadot/types/interfaces'; class MockApiHooks { + public multisigApprovals: [H256, Multisig][] | undefined = []; + public delegations: Voting[] | undefined; + public proxies: [ProxyDefinition[], BN][] | undefined = []; public subs: string[] | undefined = []; public judgements: RegistrationJudgement[] | undefined = []; public registrars: Registrar[] = []; + public setDelegations (delegations: Voting[]) { + this.delegations = delegations; + } + + public setMultisigApprovals (multisigApprovals: [H256, Multisig][]) { + this.multisigApprovals = multisigApprovals; + } + + public setProxies (proxies: [ProxyDefinition[], BN][]) { + this.proxies = proxies; + } + public setSubs (subs: string[] | undefined) { this.subs = subs; }