Split registrars display (#6157)

* Add subs modal

* Add tests

* Add Translations

* Improve closing modal

* Improve tests

* Improve styles

* Style balances & identity

* Optimize imports

* Improve styles

* Set greater width to balance section

* Rename subs modal toggle

* Change 'amount' to 'count' in test name

* Set make modal header fixed while content is scrollable

* Style multisig

* Update multisig styles

* Capitalize headers

* Move sidebar tests to Sidebar.spec.tsx

* Remove duplications in tests

* Add UserIcon component

* Revert subs modal changes and add scrollable section in Sidebar

* Remove unused styles

* Use accountsPage clearAccount method

* Wrap subs into Expander

* Add useSubidentities hook

* Adjust api mock in tests

* WIP - split judgements

* Display judgements

* Handle no judgements case

* [WIP] Add popup test

* Extract JudgmentTag component

* Add popup test

* Refactor tests and remove pointer from no judgements tag

* Extract JudgementTag test class

* Close popup on scroll

* Remove old section

* Clean registration

* Clean up identity in useAccountInfo and mock in Page

* Remove fragment tag

* Fix imports

* Move SortedJudgements to /types

* Move Registrar interface to types

* Refactor clickRegistrar fcn

* Move hasAccount check to isRegistrar

* Add useJudgements hook

* Rename and move getJudgementColor to utils

* Fix import and casting in tests

* Sort imports

* Add review fixes

* Fix tests

Co-authored-by: Krzysztof Jelski <krzysztof@ethworks.io>
Co-authored-by: Ewa Kowalska <ewcia.kowalska@gmail.com>
Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
MiZiet 2021-09-29 10:45:34 +02:00 committed by GitHub
parent d043b799cd
commit 053e0efca2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 513 additions and 104 deletions

View File

@ -103,6 +103,7 @@
"Name given to this multisig. You can edit it at any later point in time.": "Name given to this multisig. You can edit it at any later point in time.",
"Name given to this proxied account. You can edit it at any later point in time.": "Name given to this proxied account. You can edit it at any later point in time.",
"Next": "Next",
"No judgements": "No judgements",
"No judgments": "No judgments",
"No matches found": "No matches found",
"No sub identities set.": "No sub identities set.",
@ -356,4 +357,4 @@
"website": "website",
"your current password": "your current password",
"your new password": "your new password"
}
}

View File

@ -5,11 +5,12 @@ import type { AddressIdentity } from '@polkadot/react-hooks/types';
import React, { useMemo } from 'react';
import { AddressMini, AvatarItem, Expander, IconLink, Tag } from '@polkadot/react-components';
import { AddressMini, AvatarItem, Expander, IconLink } from '@polkadot/react-components';
import { useApi, useRegistrars, useSubidentities, useToggle } from '@polkadot/react-hooks';
import { isHex } from '@polkadot/util';
import { useTranslation } from '../translate';
import Judgements from './Judgements';
import RegistrarJudgement from './RegistrarJudgement';
import UserIcon from './UserIcon';
@ -51,33 +52,6 @@ function Identity ({ address, identity }: Props): React.ReactElement<Props> | nu
<div className='ui--AddressMenu-section ui--AddressMenu-identity'>
<div className='ui--AddressMenu-sectionHeader'>
{t<string>('identity')}
<Tag
color={
identity.isBad
? 'red'
: identity.isGood
? 'green'
: 'yellow'
}
isTag={false}
label={
<>
<b>{identity.judgements.length}&nbsp;</b>
{
identity.judgements.length
? identity.isBad
? identity.isErroneous
? t<string>('Erroneous')
: t<string>('Low quality')
: identity.isKnownGood
? t<string>('Known good')
: t<string>('Reasonable')
: t<string>('No judgments')
}
</>
}
size='tiny'
/>
</div>
<div>
<AvatarItem
@ -92,6 +66,7 @@ function Identity ({ address, identity }: Props): React.ReactElement<Props> | nu
subtitle={identity.legal}
title={identity.display}
/>
<Judgements address={address} />
<div className='ui--AddressMenu-identityTable'>
{identity.parent && (
<div className='tr parent'>

View File

@ -0,0 +1,44 @@
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Judgement } from '@polkadot/react-hooks/types';
import React, { useMemo } from 'react';
import { AddressSmall, Menu, Popup, Tag } from '@polkadot/react-components';
import { getJudgementColor } from '../util';
interface Props {
judgement: Judgement
}
function JudgementTag ({ judgement: { judgementName, registrars } }: Props): React.ReactElement<Props> {
const judgementColor = useMemo(() => getJudgementColor(judgementName), [judgementName]);
return (
<Popup
closeOnScroll
position='center'
value={
<Menu>
{registrars.map((registrar) => registrar && (
<AddressSmall
key={registrar.address}
value={registrar.address}
/>
))}
</Menu>
}
>
<Tag
color={judgementColor}
isTag={false}
label={`${registrars.length} ${judgementName}`}
size='tiny'
/>
</Popup>
);
}
export default React.memo(JudgementTag);

View File

@ -0,0 +1,63 @@
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styled from 'styled-components';
import { Tag } from '@polkadot/react-components';
import { useJudgements } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import JudgementTag from './JudgementTag';
interface Props {
address: string;
className?: string;
}
function Judgements ({ address, className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const judgements = useJudgements(address);
if (judgements.length === 0) {
return (
<div
className={`${className} no-judgements`}
data-testid='judgements'
>
<Tag
color='yellow'
isTag={false}
key='NoJudgements'
label={t<string>('No judgements')}
size='tiny'
/>
</div>
);
}
return (
<div
className={className}
data-testid='judgements'
>
{judgements.map((judgement) =>
<JudgementTag
judgement={judgement}
key={`${address}${judgement.judgementName}`}
/>
)}
</div>
);
}
export default React.memo(styled(Judgements)`
margin-top: 0.714rem;
&:not(.no-judgements) {
.ui--Tag:hover {
cursor: pointer;
}
}
`);

View File

@ -3,22 +3,25 @@
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import i18next from '@polkadot/react-components/i18n';
import { AddressFlags } from '@polkadot/react-hooks/types';
import { anAccount, anAccountWithInfo, anAccountWithMeta } from '@polkadot/test-support/creation/account';
import { alice, bob, MemoryStore } from '@polkadot/test-support/keyring';
import { charlieShortAddress, ferdieShortAddress, mockRegistration, registrars } from '@polkadot/test-support/mockData/registrations';
import { Sidebar } from '@polkadot/test-support/pagesElements/Sidebar';
import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks';
import { RegistrationJudgement } from '@polkadot/types/interfaces';
import { keyring } from '@polkadot/ui-keyring';
import { AccountRow } from '../../test/pageElements/AccountRow';
import { AccountsPage } from '../../test/pages/accountsPage';
describe('Sidebar', () => {
let accountsPage: AccountsPage;
let accountRows: AccountRow[];
let sideBar: Sidebar;
beforeAll(() => {
beforeAll(async () => {
await i18next.changeLanguage('en');
if (keyring.getAccounts().length === 0) {
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
}
@ -38,8 +41,7 @@ describe('Sidebar', () => {
describe('changes name', () => {
beforeEach(async () => {
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.changeAccountName(newName);
});
@ -54,14 +56,15 @@ describe('Sidebar', () => {
});
it('within account row', async () => {
const accountRows = await accountsPage.getAccountRows();
await accountRows[0].assertAccountName(newName);
});
});
it('cannot be edited if edit button has not been pressed', async () => {
accountsPage.renderDefaultAccounts(1);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.clickByText('no tags');
expect(sideBar.queryByRole('combobox')).toBeFalsy();
@ -72,8 +75,7 @@ describe('Sidebar', () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithInfo({ flags: { isEditable: false } as AddressFlags })
);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar = await accountsPage.openSidebarForRow(0);
sideBar.edit();
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
@ -81,9 +83,10 @@ describe('Sidebar', () => {
describe('on edit cancel', () => {
beforeEach(async () => {
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })
);
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.assertTags('no tags');
sideBar.edit();
@ -110,9 +113,7 @@ describe('Sidebar', () => {
anAccountWithMeta({ name: 'alice' }),
anAccountWithMeta({ name: 'bob' })
);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar = await accountsPage.openSidebarForRow(0);
sideBar.edit();
});
@ -137,48 +138,111 @@ describe('Sidebar', () => {
it('cancels editing and changes name when opening sidebar for another account', async () => {
await waitFor(() => sideBar.assertAccountInput('alice'));
sideBar = await accountRows[1].openSidebar();
sideBar = await accountsPage.openSidebarForRow(1);
await sideBar.assertAccountName('BOB');
});
});
});
describe('identity section', () => {
it('does not display subs when account has zero subs', async () => {
accountsPage.render([[alice, anAccount()]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
const subs = await sideBar.findSubs();
describe('subs', () => {
describe('when do not exist', () => {
it('does not display subs', async () => {
accountsPage.renderDefaultAccounts(1);
sideBar = await accountsPage.openSidebarForRow(0);
const subs = await sideBar.findSubs();
expect(subs).toHaveLength(0);
expect(subs).toHaveLength(0);
});
});
describe('when exist', () => {
let subs: HTMLElement[];
beforeEach(async () => {
mockApiHooks.setSubs([bob]);
accountsPage.renderAccountsWithDefaultAddresses(
anAccount(),
anAccountWithMeta({ name: 'Bob' })
);
sideBar = await accountsPage.openSidebarForRow(0);
subs = await sideBar.findSubs();
});
it('displays count of subs and account names', () => {
const subsNumber = subs[0].childNodes[0];
const subAccount = subs[0].childNodes[1];
expect(subsNumber).toHaveClass('subs-number');
expect(subsNumber).toHaveTextContent('1');
expect(subAccount).toHaveTextContent('BOB');
});
it('displays picked sub in sidebar', async () => {
const subAccount = subs[0].childNodes[1];
fireEvent.click(await within(subAccount as HTMLElement).findByTestId('account-name'));
await sideBar.assertAccountName('BOB');
});
});
});
it('displays count of subs and account names', async () => {
mockApiHooks.setSubs([bob]);
accountsPage.render([[alice, anAccount()], [bob, anAccountWithMeta({ name: 'Bob' })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
const subs = await sideBar.findSubs();
describe('judgements', () => {
it('displays several judgements', async () => {
mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]);
accountsPage.renderDefaultAccounts(1);
sideBar = await accountsPage.openSidebarForRow(0);
const subsNumber = subs[0].childNodes[0];
const subAccount = subs[0].childNodes[1];
await sideBar.assertJudgement('1 Known good');
await sideBar.assertJudgement('2 Reasonable');
await sideBar.assertJudgement('1 Erroneous');
});
expect(subsNumber).toHaveClass('subs-number');
expect(subsNumber).toHaveTextContent('1');
expect(subAccount).toHaveTextContent('BOB');
});
it('displays no judgements', async () => {
accountsPage.renderDefaultAccounts(1);
sideBar = await accountsPage.openSidebarForRow(0);
it('displays picked sub in sidebar', async () => {
mockApiHooks.setSubs([bob]);
accountsPage.render([[alice, anAccount()], [bob, anAccountWithMeta({ name: 'Bob' })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
const subs = await sideBar.findSubs();
const subAccount = subs[0].childNodes[1];
await sideBar.assertJudgement('No judgements');
});
fireEvent.click(await within(subAccount as HTMLElement).findByTestId('account-name'));
describe('displays registrars', () => {
beforeEach(async () => {
mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]);
mockApiHooks.setRegistrars(registrars);
accountsPage.render([
[alice, anAccountWithMeta({ name: 'Alice' })],
[bob, anAccountWithMeta({ name: 'Bob' })]
]);
sideBar = await accountsPage.openSidebarForRow(0);
});
await sideBar.assertAccountName('BOB');
it('singular registrar', async () => {
const judgementTag = await sideBar.getJudgement('1 Known good');
await judgementTag.assertRegistrars([charlieShortAddress]);
});
it('multiple registrars', async () => {
const judgementTag = await sideBar.getJudgement('2 Reasonable');
await judgementTag.assertRegistrars(['BOB', ferdieShortAddress]);
});
it('opens clicked registrar in sidebar and closes popup', async () => {
const judgementTag = await sideBar.getJudgement('2 Reasonable');
await judgementTag.clickRegistrar('BOB');
expect(screen.queryByTestId('popup-window')).toBeFalsy();
await sideBar.assertAccountName('BOB');
});
});
afterEach(() => {
mockApiHooks.setJudgements([]);
});
});
});
});

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { DisplayedJudgement } from '@polkadot/react-components/types';
import type { AccountId, AccountIndex, Address } from '@polkadot/types/interfaces';
import type { CreateResult } from '@polkadot/ui-keyring/types';
import type { AccountBalance, SortedAccount } from './types';
@ -113,3 +114,7 @@ export function sortAccounts (accountsList: SortedAccount[], accountsMap: Record
? 1
: -1);
}
export const getJudgementColor = (name: DisplayedJudgement): 'green' | 'red' => {
return (name === 'Erroneous' || name === 'Low quality') ? 'red' : 'green';
};

View File

@ -6,6 +6,7 @@ import React from 'react';
import { anAccount } from '@polkadot/test-support/creation/account';
import { Page } from '@polkadot/test-support/pages/Page';
import { Sidebar } from '@polkadot/test-support/pagesElements/Sidebar';
import { AccountOverrides } from '@polkadot/test-support/types';
import { assertText, clickButton } from '@polkadot/test-support/utils/renderedScreenUtils';
@ -74,6 +75,12 @@ export class AccountsPage extends Page {
this.render(accounts);
}
async openSidebarForRow (accountRowIndex: number): Promise<Sidebar> {
const accountRows = await this.getAccountRows();
return accountRows[accountRowIndex].openSidebar();
}
private async getSortCategory (categoryName: string): Promise<HTMLElement> {
const sortByComponent = await this.getSortByComponent();
const availableCategories = await within(sortByComponent).findAllByRole('option');

View File

@ -15,8 +15,9 @@ function PopupWindow ({ children, className = '', position, triggerRef, windowRe
return createPortal(
<div
className={`${className}${verticalPosition === 'top' ? ' pointerTop' : ' pointerBottom'}`}
data-testid='popup-window'
ref={windowRef}
style={renderWindowPosition && { transform: `translate3d(${renderWindowPosition.x}px, ${renderWindowPosition.y}px, 0)`, zIndex: 300 }}
style={renderWindowPosition && { transform: `translate3d(${renderWindowPosition.x}px, ${renderWindowPosition.y}px, 0)`, zIndex: 1000 }}
>
{children}
</div>,

View File

@ -3,7 +3,7 @@
import type { PopupProps } from './types';
import React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import styled from 'styled-components';
import { Button } from '@polkadot/react-components/index';
@ -11,13 +11,21 @@ import { useOutsideClick, useToggle } from '@polkadot/react-hooks';
import PopupWindow from './PopupWindow';
function Popup ({ children, className = '', isDisabled = false, onCloseAction, position = 'left', value }: PopupProps) {
function Popup ({ children, className = '', closeOnScroll = false, isDisabled = false, onCloseAction, position = 'left', value }: PopupProps) {
const [isOpen, toggleIsOpen, setIsOpen] = useToggle(false);
const triggerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const closeWindow = useCallback(() => setIsOpen(false), [setIsOpen]);
useOutsideClick([triggerRef, dropdownRef], () => setIsOpen(false));
useOutsideClick([triggerRef, dropdownRef], closeWindow);
useEffect(() => {
if (closeOnScroll) {
document.addEventListener('scroll', closeWindow, true);
}
return () => document.removeEventListener('scroll', closeWindow, true);
}, [closeOnScroll, closeWindow, setIsOpen]);
useEffect(() => {
if (!isOpen && onCloseAction) {

View File

@ -17,6 +17,7 @@ export interface PopupWindowProps {
export interface PopupProps {
isDisabled?: boolean;
className?: string;
closeOnScroll?: boolean;
value?: React.ReactNode;
children?: React.ReactNode;
position?: HorizontalPosition;

View File

@ -83,3 +83,5 @@ export interface ThemeProps {
export type FlagColor = 'blue' | 'green' | 'grey' | 'orange' | 'pink' | 'red' | 'yellow' | 'theme';
export type AccountIdIsh = AccountId | AccountIndex | Address | string | Uint8Array | null;
export type DisplayedJudgement = 'Erroneous' | 'Low quality' | 'Known good' | 'Reasonable';

View File

@ -1,6 +1,10 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { RegistrarIndex } from '@polkadot/types/interfaces/identity/types';
import { DisplayedJudgement } from '../types';
export interface DropdownOption {
className?: string;
key?: string;
@ -9,3 +13,4 @@ export interface DropdownOption {
}
export type DropdownOptions = DropdownOption[];
export type SortedJudgements = ({ judgementName: DisplayedJudgement, registrarsIndexes: RegistrarIndex[] })[];

View File

@ -27,6 +27,7 @@ export { useIncrement } from './useIncrement';
export { useInflation } from './useInflation';
export { useIpfs } from './useIpfs';
export { useIsMountedRef } from './useIsMountedRef';
export { useJudgements } from './useJudgements';
export { useLedger } from './useLedger';
export { useLoadingDelay } from './useLoadingDelay';
export { useMapEntries } from './useMapEntries';

View File

@ -3,6 +3,7 @@
import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { DeriveAccountFlags, DeriveAccountRegistration } from '@polkadot/api-derive/types';
import type { DisplayedJudgement } from '@polkadot/react-components/types';
import type { AccountId, Balance, BlockNumber, Call, Exposure, Hash, RewardDestination, SessionIndex, StakingLedger, ValidatorPrefs } from '@polkadot/types/interfaces';
import type { IExtrinsic } from '@polkadot/types/types';
import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types';
@ -99,13 +100,8 @@ export interface AddressFlags extends DeriveAccountFlags {
}
export interface AddressIdentity extends DeriveAccountRegistration {
isGood: boolean;
isBad: boolean;
isKnownGood: boolean;
isReasonable: boolean;
isErroneous: boolean;
isLowQuality: boolean;
isExistent: boolean;
isKnownGood: boolean;
waitCount: number;
}
@ -150,3 +146,15 @@ export interface StakerState {
stashId: string;
validatorPrefs?: ValidatorPrefs;
}
export interface Registrar {
address: string;
index: number;
}
export interface Judgement {
judgementName: DisplayedJudgement;
registrars: (Registrar | undefined)[];
}
export type UseJudgements = Judgement[]

View File

@ -100,19 +100,11 @@ export function useAccountInfo (value: string | null, isContract = false): UseAc
if (identity) {
const judgements = identity.judgements.filter(([, judgement]) => !judgement.isFeePaid);
const isKnownGood = judgements.some(([, judgement]) => judgement.isKnownGood);
const isReasonable = judgements.some(([, judgement]) => judgement.isReasonable);
const isErroneous = judgements.some(([, judgement]) => judgement.isErroneous);
const isLowQuality = judgements.some(([, judgement]) => judgement.isLowQuality);
setIdentity({
...identity,
isBad: isErroneous || isLowQuality,
isErroneous,
isExistent: !!identity.display,
isGood: isKnownGood || isReasonable,
isKnownGood,
isLowQuality,
isReasonable,
judgements,
waitCount: identity.judgements.length - judgements.length
});

View File

@ -0,0 +1,21 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useMemo } from 'react';
import { getJudgements } from './utils/getJudgements';
import { matchRegistrarAccountsWithIndexes } from './utils/matchRegistrarAccountsWithIndexes';
import { UseJudgements } from './types';
import { useAccountInfo, useRegistrars } from './';
export function useJudgements (address: string): UseJudgements {
const { identity } = useAccountInfo(address);
const { registrars: allRegistrars } = useRegistrars();
const judgementsWithRegistrarIndexes = useMemo(() => getJudgements(identity), [identity]);
return useMemo(
() => matchRegistrarAccountsWithIndexes(judgementsWithRegistrarIndexes, allRegistrars),
[allRegistrars, judgementsWithRegistrarIndexes]
);
}

View File

@ -3,6 +3,7 @@
import type { Option } from '@polkadot/types';
import type { RegistrarInfo } from '@polkadot/types/interfaces';
import type { Registrar } from './types';
import { useMemo } from 'react';
@ -15,11 +16,6 @@ interface RegistrarNull {
index: number;
}
interface Registrar {
address: string;
index: number;
}
interface State {
isRegistrar: boolean;
registrars: Registrar[];
@ -29,7 +25,7 @@ interface State {
export function useRegistrars (skipQuery?: boolean): State {
const { api } = useApi();
const { allAccounts, hasAccounts } = useAccounts();
const query = useCall<Option<RegistrarInfo>[]>(!skipQuery && hasAccounts && api.query.identity?.registrars);
const query = useCall<Option<RegistrarInfo>[]>(!skipQuery && api.query.identity?.registrars);
// determine if we have a registrar or not - registrars are allowed to approve
return useMemo(
@ -44,10 +40,10 @@ export function useRegistrars (skipQuery?: boolean): State {
.filter((registrar): registrar is Registrar => !!registrar.address);
return {
isRegistrar: registrars.some(({ address }) => allAccounts.includes(address)),
isRegistrar: hasAccounts && registrars.some(({ address }) => allAccounts.includes(address)),
registrars
};
},
[allAccounts, query]
[allAccounts, hasAccounts, query]
);
}

View File

@ -0,0 +1,64 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveAccountRegistration } from '@polkadot/api-derive/types';
import type { DisplayedJudgement } from '@polkadot/react-components/types';
import type { SortedJudgements } from '@polkadot/react-components/util/types';
import type { RegistrationJudgement } from '@polkadot/types/interfaces';
function extractIndexes (registrars: RegistrationJudgement[]) {
return registrars.map((judgement) => judgement[0]);
}
export function getJudgements (identity: DeriveAccountRegistration | undefined): SortedJudgements {
if (!identity) return [];
const judgements = groupJudgements(identity.judgements);
const result = [];
for (const name in judgements) {
const judgementName = name as DisplayedJudgement;
if (judgements[judgementName].length !== 0) {
result.push({ judgementName, registrarsIndexes: extractIndexes(judgements[judgementName]) });
}
}
return result;
}
export function groupJudgements (judgements: RegistrationJudgement[]): Record<DisplayedJudgement, RegistrationJudgement[]> {
const consideredJudgements = filterConsideredJudgements(judgements);
const knownGoodJudgements = filterKnownGood(consideredJudgements);
const reasonableJudgements = filterReasonable(consideredJudgements);
const erroneousJudgements = filterErroneous(consideredJudgements);
const lowQualityJudgements = filterLowQuality(consideredJudgements);
return {
Erroneous: erroneousJudgements,
'Known good': knownGoodJudgements,
'Low quality': lowQualityJudgements,
Reasonable: reasonableJudgements
};
}
const filterConsideredJudgements = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => {
return judgements.filter(([, judgement]) => !judgement.isFeePaid);
};
const filterKnownGood = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => {
return judgements.filter(([, judgement]) => judgement.isKnownGood);
};
const filterReasonable = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => {
return judgements.filter(([, judgement]) => judgement.isReasonable);
};
const filterErroneous = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => {
return judgements.filter(([, judgement]) => judgement.isErroneous);
};
const filterLowQuality = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => {
return judgements.filter(([, judgement]) => judgement.isLowQuality);
};

View File

@ -0,0 +1,19 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SortedJudgements } from '@polkadot/react-components/util/types';
import type { Judgement, Registrar } from '../types';
export function matchRegistrarAccountsWithIndexes (
judgementsWithRegistrarIndexes: SortedJudgements,
allRegistrars: Registrar[]
): Judgement[] {
return judgementsWithRegistrarIndexes.map(({ judgementName, registrarsIndexes }) => {
const findRegistrarByIndex = (index: number) => allRegistrars.find((registrar) => registrar.index === index);
return {
judgementName,
registrars: registrarsIndexes.map((index) => findRegistrarByIndex(index.toNumber()))
};
});
}

View File

@ -0,0 +1,47 @@
// Copyright 2017-2021 @polkadot/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registrar } from '@polkadot/react-hooks/types';
import registry from '@polkadot/react-api/typeRegistry';
import { bob, charlie, ferdie } from '../keyring';
export const mockRegistration = {
judgements: [
[
registry.createType('RegistrarIndex', '0'),
{
isReasonable: true
}
],
[
registry.createType('RegistrarIndex', '1'),
{
isKnownGood: true
}
],
[
registry.createType('RegistrarIndex', '2'),
{
isErroneous: true
}
],
[
registry.createType('RegistrarIndex', '3'),
{
isReasonable: true
}
]
]
};
export const bobRegistrar: Registrar = { address: bob, index: 0 };
export const charlieRegistrar: Registrar = { address: charlie, index: 1 };
export const ferdieRegistrar: Registrar = { address: ferdie, index: 3 };
export const registrars: Registrar[] = [bobRegistrar, charlieRegistrar, ferdieRegistrar];
export const bobShortAddress = '5FHneW…M694ty';
export const charlieShortAddress = '5DAAnr…3PTXFy';
export const ferdieShortAddress = '5CiPPs…SK2DjL';

View File

@ -18,7 +18,7 @@ import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks';
import { TypeRegistry } from '@polkadot/types/create';
import { keyring } from '@polkadot/ui-keyring';
import { alice, bob, charlie } from '../keyring';
import { alice, bob, charlie, ferdie } from '../keyring';
import { Table } from '../pagesElements';
import { AccountOverrides, mockAccountHooks } from '../utils/accountDefaults';
@ -43,7 +43,14 @@ jest.mock('@polkadot/react-hooks/useAccountInfo', () => {
? {
...actual.useAccountInfo(address),
flags: { ...actual.useAccountInfo(address).flags, ...(mockInfo.info.flags) },
identity: { ...actual.useAccountInfo(address).identity, ...(mockInfo.info.identity) },
identity: {
...actual.useAccountInfo(address).identity,
...(mockInfo.info.identity),
judgements: [
...(actual.useAccountInfo(address).identity?.judgements || []),
...(mockApiHooks.judgements || [])
]
},
tags: [...actual.useAccountInfo(address).tags, ...(mockInfo.info.tags)]
}
: actual.useAccountInfo(address);
@ -71,9 +78,20 @@ jest.mock('@polkadot/react-hooks/useSubidentities', () => ({
useSubidentities: () => mockApiHooks.subs
}));
jest.mock('@polkadot/react-hooks/useSubidentities', () => ({
useSubidentities: () => mockApiHooks.subs
}));
jest.mock('@polkadot/react-hooks/useRegistrars', () => ({
useRegistrars: () => ({
isRegistrar: false,
registrars: mockApiHooks.registrars
})
}));
export abstract class Page {
private renderResult?: RenderResult
protected readonly defaultAddresses = [alice, bob, charlie];
protected readonly defaultAddresses = [alice, bob, charlie, ferdie];
protected constructor (private readonly overview: React.ReactElement, private readonly rowClassName: string) {
this.overview = overview;
@ -97,6 +115,9 @@ export abstract class Page {
balances: {
all: noop
},
chain: {
bestNumber: noop
},
democracy: {
locks: noop
},

View File

@ -0,0 +1,36 @@
// Copyright 2017-2021 @polkadot/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { fireEvent, screen, within } from '@testing-library/react';
export class JudgementTag {
public judgementTag: HTMLElement
constructor (judgementTag: HTMLElement) {
this.judgementTag = judgementTag;
}
async assertRegistrars (expectedRegistrars: string[]): Promise<void> {
const popup = await this.openPopup();
for (let index = 0; index < expectedRegistrars.length; index++) {
await within(popup).findByText(expectedRegistrars[index]);
}
}
async clickRegistrar (registrarName: string): Promise<void> {
const popup = await this.openPopup();
const registrars = await within(popup).findAllByTestId('account-name');
const registrar = registrars.find((reg) => reg.textContent === registrarName) ?? fail('Registrar not found');
fireEvent.click(registrar);
}
private async openPopup (): Promise<HTMLElement> {
fireEvent.click(this.judgementTag);
return screen.findByTestId('popup-window');
}
}

View File

@ -1,8 +1,10 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// Copyright 2017-2021 @polkadot/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { fireEvent, screen, within } from '@testing-library/react';
import { JudgementTag } from './JudgementTag';
export class Sidebar {
public sidebar: HTMLElement
@ -43,6 +45,12 @@ export class Sidebar {
expect(sideBarName).toHaveTextContent(expectedAccountName);
}
async assertJudgement (judgement: string): Promise<void> {
const judgementsSection = await this.findByTestId('judgements');
expect(judgementsSection).toHaveTextContent(judgement);
}
async assertTags (tagsContent: string): Promise<void> {
const sideBarTags = await this.findByTestId('sidebar-tags');
@ -124,6 +132,12 @@ export class Sidebar {
return screen.findByTestId('modal');
}
async getJudgement (judgementName: string): Promise<JudgementTag> {
const judgements = await this.findByTestId('judgements');
return new JudgementTag(await within(judgements).findByText(judgementName));
}
private clickButton (buttonName: string) {
const button = this.getByRole('button', { name: buttonName });

View File

@ -1,11 +1,25 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registrar } from '@polkadot/react-hooks/types';
import type { RegistrationJudgement } from '@polkadot/types/interfaces';
class MockApiHooks {
public subs: string[] | undefined = []
public subs: string[] | undefined = [];
public judgements: RegistrationJudgement[] | undefined = [];
public registrars: Registrar[] = [];
public setSubs (subs: string[] | undefined) {
this.subs = subs;
}
public setJudgements (judgements: RegistrationJudgement[] | undefined) {
this.judgements = judgements;
}
public setRegistrars (registrars: Registrar[]) {
this.registrars = registrars;
}
}
export const mockApiHooks = new MockApiHooks();