From 67c6681e1856bd422856f85e38ee82c19062159e Mon Sep 17 00:00:00 2001 From: Ilya Smiyukha <39830587+IlyaSmiyukha@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:01:06 +0300 Subject: [PATCH 01/10] Hide total balance when wallet was disconected (#4442) Co-authored-by: ilya Smiyukha --- packages/ui/src/accounts/providers/balances/provider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/accounts/providers/balances/provider.tsx b/packages/ui/src/accounts/providers/balances/provider.tsx index 763e6dbf14..54c2af7551 100644 --- a/packages/ui/src/accounts/providers/balances/provider.tsx +++ b/packages/ui/src/accounts/providers/balances/provider.tsx @@ -25,6 +25,8 @@ export const BalancesContextProvider = (props: Props) => { ) const balances = useMemo(() => { + if (!addresses.length) return {} + if (!isLoading && result) return result.reduce((acc, balance, index) => { return { @@ -32,7 +34,7 @@ export const BalancesContextProvider = (props: Props) => { ...acc, } }, {} as AddressToBalanceMap) - }, [result]) + }, [result, addresses]) return {props.children} } From 195f04ad5c876450ea637b740aa766b394b61584 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Sat, 24 Jun 2023 19:28:46 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=93=9A=20Add=20modals=20support=20i?= =?UTF-8?q?n=20stories=20(#4457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add modal and transaction support in page stories * Mock chain numbers as Codec instead of BNs * Simplify mocking balances * Improve the `MockApi` type * Mock transaction results with `TxMock` * Move the Pages section to the top menu * Mock the `api.tx.proposalsEngine.vote` transaction * Always redirects to the current story * Improve TxMock * Use a single spy for `onCall` and `onSend` Because Storybook actions are converted to jest.fn --- packages/ui/.storybook/preview.tsx | 39 +++++++-- .../Proposals/ProposalPreview.stories.tsx | 4 + packages/ui/src/mocks/helpers/asChainData.ts | 10 ++- packages/ui/src/mocks/helpers/index.ts | 8 +- packages/ui/src/mocks/helpers/transactions.ts | 86 +++++++++++++++++++ packages/ui/src/mocks/providers/accounts.tsx | 57 ++++++------ packages/ui/src/mocks/providers/api.tsx | 44 ++++++---- .../ui/src/mocks/providers/query-node.tsx | 4 +- packages/ui/test/_mocks/transactions.ts | 52 +---------- 9 files changed, 196 insertions(+), 108 deletions(-) create mode 100644 packages/ui/src/mocks/helpers/transactions.ts diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 26bb759588..7b0ad1280b 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -5,9 +5,14 @@ import { useForm, FormProvider } from 'react-hook-form' import { MemoryRouter, Redirect, Route, Switch } from 'react-router' import { createGlobalStyle } from 'styled-components' +import { GlobalModals } from '../src/app/GlobalModals' import { NotFound } from '../src/app/pages/NotFound' import { GlobalStyle } from '../src/app/providers/GlobalStyle' +import { NotificationsHolder } from '../src/common/components/page/SideNotification' +import { TransactionStatus } from '../src/common/components/TransactionStatus/TransactionStatus' import { Colors } from '../src/common/constants' +import { ModalContextProvider } from '../src/common/providers/modal/provider' +import { TransactionStatusProvider } from '../src/common/providers/transactionStatus/provider' import { MockProvidersDecorator } from '../src/mocks/providers' import { i18next } from '../src/services/i18n' @@ -43,17 +48,33 @@ const RHFDecorator: Decorator = (Story) => { ) } -const RouterDecorator: Decorator = (Story, { parameters }) => ( - - - - - - - +const RouterDecorator: Decorator = (Story, { parameters }) => { + const storyPath = `/story/${parameters.router?.href ?? ''}` + return ( + + + + {parameters.enable404 && } + + + + ) +} + +const ModalDecorator: Decorator = (Story) => ( + + + + + + + + + ) export const decorators = [ + ModalDecorator, stylesWrapperDecorator, i18nextDecorator, RHFDecorator, @@ -91,7 +112,7 @@ export const parameters = { options: { storySort: { method: 'alphabetical', - order: ['Common'], + order: ['Pages', 'Common'], }, }, } diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx index 107b297098..546dcf7d9c 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx @@ -127,6 +127,10 @@ export default { }, referendum: { stage: {} }, }, + + tx: { + proposalsEngine: { vote: { event: 'Voted' } }, + }, }, queryNode: [ diff --git a/packages/ui/src/mocks/helpers/asChainData.ts b/packages/ui/src/mocks/helpers/asChainData.ts index da92610f0e..590ac4c9e9 100644 --- a/packages/ui/src/mocks/helpers/asChainData.ts +++ b/packages/ui/src/mocks/helpers/asChainData.ts @@ -1,9 +1,11 @@ +import { createType } from '@joystream/types' import { mapValues } from 'lodash' -import { asBN } from '@/common/utils' +import { isDefined } from '@/common/utils' export const asChainData = (data: any): any => { - switch (Object.getPrototypeOf(data).constructor.name) { + const type = isDefined(data) ? Object.getPrototypeOf(data).constructor.name : typeof data + switch (type) { case 'Object': return mapValues(data, asChainData) @@ -11,10 +13,10 @@ export const asChainData = (data: any): any => { return data.map(asChainData) case 'Number': - return asBN(data) + return createType('u128', data) case 'String': - return isNaN(data) ? data : asBN(data) + return isNaN(data) ? data : createType('u128', data) default: return data diff --git a/packages/ui/src/mocks/helpers/index.ts b/packages/ui/src/mocks/helpers/index.ts index 0c2ab25e19..7cda8debec 100644 --- a/packages/ui/src/mocks/helpers/index.ts +++ b/packages/ui/src/mocks/helpers/index.ts @@ -1,5 +1,9 @@ +import { isObject } from 'lodash' + import { JOY_DECIMAL_PLACES } from '@/common/constants' +import { Balance } from '../providers/accounts' + export * from './storybook' export { getMember } from '@/../test/_mocks/members' @@ -7,7 +11,9 @@ export function camelCaseToDash(myStr: string) { return myStr.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() } -export const joy = (value: string | number): string => { +export const joy = (value: Balance): string => { + if (isObject(value)) return value.toString() + const [integer = '0', decimal = ''] = value.toString().replace(/[,_ ]/g, '').split('.') return `${integer}${decimal.padEnd(JOY_DECIMAL_PLACES, '0')}` } diff --git a/packages/ui/src/mocks/helpers/transactions.ts b/packages/ui/src/mocks/helpers/transactions.ts new file mode 100644 index 0000000000..572083b99c --- /dev/null +++ b/packages/ui/src/mocks/helpers/transactions.ts @@ -0,0 +1,86 @@ +import { createType } from '@joystream/types' +import BN from 'bn.js' +import { asyncScheduler, from, of, scheduled } from 'rxjs' + +import { Balance } from '../providers/accounts' +import { BLOCK_HASH } from '../providers/api' + +import { joy } from '.' + +export type TxMock = { + data?: any[] | any + failure?: string + event?: string + fee?: Balance + onCall?: CallableFunction + onSend?: CallableFunction +} + +export const fromTxMock = ( + { data, failure, event: eventName = 'Default Event', fee = 5, onCall, onSend }: TxMock, + moduleName: string +) => { + const event = failure ? createErrorEvents(failure) : createSuccessEvents([data ?? []].flat(), moduleName, eventName) + const txResult = stubTransactionResult(event) + + const paymentInfo = () => of({ partialFee: createType('BalanceOf', joy(fee)) }) + + return (...args: any[]) => { + onCall?.(...args) + return { + paymentInfo, + signAndSend: () => { + onSend?.(...args) + return txResult + }, + } + } +} + +export const stubTransactionResult = (events: any[]) => + scheduled( + from([ + { + status: { isReady: true, type: 'Ready' }, + }, + { + status: { type: 'InBlock', isInBlock: true, asInBlock: BLOCK_HASH }, + events: [...events], + }, + { + status: { type: 'Finalized', isFinalized: true, asFinalized: BLOCK_HASH }, + events: [...events], + }, + ]), + asyncScheduler + ) + +export const createSuccessEvents = (data: any[], section: string, method: string) => [ + { + phase: { ApplyExtrinsic: 2 }, + event: { index: '0x0502', data, method, section }, + }, + { + phase: { ApplyExtrinsic: 2 }, + event: { index: '0x0000', data: [{ weight: 190949000, class: 'Normal', paysFee: 'Yes' }] }, + }, +] + +export const createErrorEvents = (errorMessage: string) => [ + { + phase: { ApplyExtrinsic: 2 }, + event: { + index: '0x0001', + data: [ + { + Module: { index: new BN(5), error: new BN(3) }, + isModule: true, + registry: { findMetaError: () => ({ docs: [errorMessage] }) }, + }, + { weight: 190949000, class: 'Normal', paysFee: 'Yes' }, + ], + section: 'system', + method: 'ExtrinsicFailed', + }, + }, +] diff --git a/packages/ui/src/mocks/providers/accounts.tsx b/packages/ui/src/mocks/providers/accounts.tsx index 845262d79b..d534d9d610 100644 --- a/packages/ui/src/mocks/providers/accounts.tsx +++ b/packages/ui/src/mocks/providers/accounts.tsx @@ -1,3 +1,4 @@ +import { createType } from '@joystream/types' import BN from 'bn.js' import { isObject, isString, mapValues } from 'lodash' import React, { FC, useCallback, useEffect, useState } from 'react' @@ -6,7 +7,7 @@ import { AccountsContext } from '@/accounts/providers/accounts/context' import { UseAccounts } from '@/accounts/providers/accounts/provider' import { BalancesContext } from '@/accounts/providers/balances/context' import { Account, AddressToBalanceMap, LockType } from '@/accounts/types' -import { asBN, whenDefined } from '@/common/utils' +import { whenDefined } from '@/common/utils' import { MembershipContext } from '@/memberships/providers/membership/context' import { MyMemberships } from '@/memberships/providers/membership/provider' import { Member, asMember } from '@/memberships/types' @@ -14,7 +15,7 @@ import { Member, asMember } from '@/memberships/types' import { Membership } from '../data/members' import { joy } from '../helpers' -type Balance = number | string | BN +export type Balance = number | string | BN type BalanceLock = LockType | { amount: Balance; type: LockType } type DeriveBalancesVesting = { @@ -24,18 +25,20 @@ type DeriveBalancesVesting = { locked: Balance vested: Balance } -type Balances = { - total?: Balance - locked?: Balance - recoverable?: Balance - transferable?: Balance - locks?: BalanceLock[] - vesting?: DeriveBalancesVesting[] - vestingTotal?: Balance - vestedClaimable?: Balance - vestedBalance?: Balance - vestingLocked?: Balance -} +type Balances = + | Balance + | { + total?: Balance + locked?: Balance + recoverable?: Balance + transferable?: Balance + locks?: BalanceLock[] + vesting?: DeriveBalancesVesting[] + vestingTotal?: Balance + vestedClaimable?: Balance + vestedBalance?: Balance + vestingLocked?: Balance + } type AccountMock = { balances?: Balances @@ -65,27 +68,28 @@ export const MockAccountsProvider: FC = ({ children, accounts const allAccounts: Account[] = accountData.map(({ address, member }) => ({ address, name: member?.handle })) const balances: AddressToBalanceMap = Object.fromEntries( - accountData.map(({ address, balances }) => { + accountData.map(({ address, balances = 100 }) => { + const _balances = isObject(balances) && !(balances instanceof BN) ? balances : { total: balances } const locks = - balances?.locks?.map((lock) => + _balances?.locks?.map((lock) => isString(lock) ? { amount: asBalance(1), type: lock } : { amount: asBalance(lock.amount), type: lock.type } ) ?? [] - const vesting = balances?.vesting?.map((schedule) => mapValues(schedule, asBalance)) ?? [] + const vesting = _balances?.vesting?.map((schedule) => mapValues(schedule, asBalance)) ?? [] return [ address, { - total: asBalance(balances?.total), - locked: asBalance(balances?.locked), - recoverable: asBalance(balances?.recoverable), - transferable: asBalance(balances?.transferable), + total: asBalance(_balances.total ?? _balances.transferable), + locked: asBalance(_balances.locked), + recoverable: asBalance(_balances.recoverable), + transferable: asBalance(_balances.transferable ?? _balances.total), locks, vesting, - vestingTotal: asBalance(balances?.vestingTotal), - vestedClaimable: asBalance(balances?.vestedClaimable), - vestedBalance: asBalance(balances?.vestedBalance), - vestingLocked: asBalance(balances?.vestingLocked), + vestingTotal: asBalance(_balances.vestingTotal), + vestedClaimable: asBalance(_balances.vestedClaimable), + vestedBalance: asBalance(_balances.vestedBalance), + vestingLocked: asBalance(_balances.vestingLocked), }, ] }) @@ -135,4 +139,5 @@ export const MockAccountsProvider: FC = ({ children, accounts ) } -const asBalance = (balance: Balance = 0): BN => (balance instanceof BN ? balance : asBN(joy(balance))) +const asBalance = (balance: Balance = 0): BN => + (balance instanceof BN ? balance : createType('BalanceOf', joy(balance))) as BN diff --git a/packages/ui/src/mocks/providers/api.tsx b/packages/ui/src/mocks/providers/api.tsx index 1daa553492..14bf92a893 100644 --- a/packages/ui/src/mocks/providers/api.tsx +++ b/packages/ui/src/mocks/providers/api.tsx @@ -1,3 +1,6 @@ +import { AugmentedConsts, AugmentedQueries, AugmentedSubmittables } from '@polkadot/api/types' +import { RpcInterface } from '@polkadot/rpc-core/types' +import { Codec } from '@polkadot/types/types' import { isFunction, set } from 'lodash' import React, { FC, useEffect, useMemo, useState } from 'react' import { Observable, of } from 'rxjs' @@ -7,15 +10,22 @@ import { ApiContext } from '@/api/providers/context' import { UseApi } from '@/api/providers/provider' import { createType } from '@/common/model/createType' -import { joy } from '../helpers' import { asChainData } from '../helpers/asChainData' +import { TxMock, fromTxMock } from '../helpers/transactions' + +export const BLOCK_HEAD = 1337 +export const BLOCK_HASH = '0x1234567890' + +type RecursiveMock, R, V = any> = { + [K in keyof T]?: T[K] extends R ? V : RecursiveMock +} type MockApi = { - consts?: Record - derive?: Record - query?: Record - rpc?: Record - tx?: Record + consts?: RecursiveMock, Codec> + derive?: RecursiveMock + query?: RecursiveMock, CallableFunction> + rpc?: RecursiveMock + tx?: RecursiveMock, CallableFunction, TxMock> } export type MockApiProps = { chain?: MockApi } @@ -29,12 +39,11 @@ export const MockApiProvider: FC = ({ children, chain }) => { if (!chain) return // Add default mocks - const blockHash = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' const blockHead = { - parentHash: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - number: 1337, - stateRoot: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - extrinsicsRoot: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + parentHash: BLOCK_HASH, + number: BLOCK_HEAD, + stateRoot: BLOCK_HASH, + extrinsicsRoot: BLOCK_HASH, digest: { logs: [] }, } @@ -46,7 +55,7 @@ export const MockApiProvider: FC = ({ children, chain }) => { query: {}, rpc: { chain: { - getBlockHash: asApiMethod(createType('BlockHash', blockHash)), + getBlockHash: asApiMethod(createType('BlockHash', BLOCK_HASH)), subscribeNewHeads: asApiMethod(createType('Header', blockHead)), }, }, @@ -58,16 +67,15 @@ export const MockApiProvider: FC = ({ children, chain }) => { traverseParams('derive', (path, value) => set(api, path, asApiMethod(value))) traverseParams('query', (path, value) => set(api, path, asApiMethod(value))) traverseParams('rpc', (path, value) => set(api, path, asApiMethod(value))) - traverseParams('tx', (path, { paymentInfo, signAndSend }) => { - set(api.tx, `${path}.paymentInfo`, asApiMethod(paymentInfo ?? joy(5))) - set(api.tx, `${path}.signAndSend`, asApiMethod(signAndSend ?? undefined)) - }) + traverseParams('tx', (path, txMock, moduleName) => set(api, path, fromTxMock(txMock, moduleName))) return api - function traverseParams(kind: keyof MockApi, fn: (path: string, value: any) => any) { + function traverseParams(kind: keyof MockApi, fn: (path: string, value: T, moduleName: string) => any) { Object.entries(chain?.[kind] ?? {}).forEach(([moduleName, moduleParam]) => - Object.entries(moduleParam).forEach(([key, value]) => fn(`${kind}.${moduleName}.${key}`, value)) + Object.entries(moduleParam as Record).forEach(([key, value]) => + fn(`${kind}.${moduleName}.${key}`, value, moduleName) + ) ) } }, [chain]) diff --git a/packages/ui/src/mocks/providers/query-node.tsx b/packages/ui/src/mocks/providers/query-node.tsx index 8f817b964b..285abbca8f 100644 --- a/packages/ui/src/mocks/providers/query-node.tsx +++ b/packages/ui/src/mocks/providers/query-node.tsx @@ -1,6 +1,8 @@ import { DocumentNode } from '@apollo/client/core' import React, { FC, createContext, useCallback, useContext, useMemo, useState } from 'react' +import { BLOCK_HEAD } from './api' + export { ApolloClient, gql, HttpLink, InMemoryCache } from '@apollo/client/core' export { ApolloProvider } from '@apollo/client/react' @@ -24,7 +26,7 @@ export const useLazyQuery = (query: DocumentNode, options?: Options): [() => voi return [get, lazyResult] } -export const useSubscription = useQuery +export const useSubscription = () => ({ data: { stateSubscription: { indexerHead: BLOCK_HEAD } } }) export const useApolloClient = () => ({ refetchQueries: () => undefined, diff --git a/packages/ui/test/_mocks/transactions.ts b/packages/ui/test/_mocks/transactions.ts index e2f58d02b6..3588abe900 100644 --- a/packages/ui/test/_mocks/transactions.ts +++ b/packages/ui/test/_mocks/transactions.ts @@ -3,7 +3,7 @@ import { AugmentedEvents } from '@polkadot/api/types' import { AnyTuple } from '@polkadot/types/types' import BN from 'bn.js' import { set } from 'lodash' -import { asyncScheduler, from, Observable, of, scheduled } from 'rxjs' +import { from, Observable, of } from 'rxjs' import { toBalances } from '@/accounts/model/toBalances' import { UseAccounts } from '@/accounts/providers/accounts/provider' @@ -13,60 +13,14 @@ import { UseApi } from '@/api/providers/provider' import { BN_ZERO } from '@/common/constants' import { createType } from '@/common/model/createType' import { ExtractTuple } from '@/common/model/JoystreamNode' +import { createErrorEvents, createSuccessEvents, stubTransactionResult } from '@/mocks/helpers/transactions' import { proposalDetails } from '@/proposals/model/proposalDetails' import { mockedBalances, mockedMyBalances, mockedUseMyAccounts } from '../setup' import { createBalanceLock, createRuntimeDispatchInfo } from './chainTypes' -const createSuccessEvents = (data: any[], section: string, method: string) => [ - { - phase: { ApplyExtrinsic: 2 }, - event: { index: '0x0502', data, method, section }, - }, - { - phase: { ApplyExtrinsic: 2 }, - event: { index: '0x0000', data: [{ weight: 190949000, class: 'Normal', paysFee: 'Yes' }] }, - }, -] - export const currentStubErrorMessage = 'Balance too low to send value.' -const findMetaError = () => ({ - docs: [currentStubErrorMessage], -}) - -const createErrorEvents = () => [ - { - phase: { ApplyExtrinsic: 2 }, - event: { - index: '0x0001', - data: [ - { Module: { index: new BN(5), error: new BN(3) }, isModule: true, registry: { findMetaError } }, - { weight: 190949000, class: 'Normal', paysFee: 'Yes' }, - ], - section: 'system', - method: 'ExtrinsicFailed', - }, - }, -] - -export const stubTransactionResult = (events: any[]) => - scheduled( - from([ - { - status: { isReady: true, type: 'Ready' }, - }, - { - status: { type: 'InBlock', isInBlock: true, asInBlock: '0x93XXX' }, - events: [...events], - }, - { - status: { type: 'Finalized', isFinalized: true, asFinalized: '0x93XXX' }, - events: [...events], - }, - ]), - asyncScheduler - ) const createBatchSuccessEvents = () => [ { @@ -96,7 +50,7 @@ const createBatchErrorEvents = () => [ ] export const stubTransactionFailure = (transaction: any) => { - set(transaction, 'signAndSend', () => stubTransactionResult(createErrorEvents())) + set(transaction, 'signAndSend', () => stubTransactionResult(createErrorEvents(currentStubErrorMessage))) } type PartialTuple = Partial From b50855a514b366a3a1628b8ba9401f1beaa3b8d3 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Tue, 27 Jun 2023 18:18:42 +0200 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=9A=9A=20Move=20the=20vote=20for=20?= =?UTF-8?q?proposal=20modal=20tests=20to=20Storybook=20=20(#4458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a story for `VoteForProposalModal` * Make CKEditor usable in tests * Rewrite test args rather than merging objects * Render modals in a container within the canvas * Add some of VoteforProposalModal tests to the stories * Fix failing tests * Simplify mocking balances * Render the modal container outside of the router * Add an id to the "Slash Proposal" toggle * Finalize the VoteForProposalModal Happy cases * Add failure cases * Remove the previous VoteforProposalModal tests * Remove unnecessary `toBeDefined` * Fix the failing tests --- packages/ui/.storybook/preview.tsx | 17 +- packages/ui/src/app/GlobalModals.tsx | 10 +- .../Proposals/ProposalPreview.stories.tsx | 343 +++++++++++++++--- .../common/components/CKEditor/CKEditor.tsx | 17 +- .../components/forms/ToggleCheckbox.tsx | 3 + packages/ui/src/mocks/helpers/storybook.ts | 33 +- packages/ui/src/mocks/providers/accounts.tsx | 8 +- .../VoteForProposalModalForm.tsx | 3 +- .../modals/VoteForProposalModal.test.tsx | 213 ----------- 9 files changed, 355 insertions(+), 292 deletions(-) delete mode 100644 packages/ui/test/proposals/modals/VoteForProposalModal.test.tsx diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 7b0ad1280b..4ba0cbdf26 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -51,13 +51,16 @@ const RHFDecorator: Decorator = (Story) => { const RouterDecorator: Decorator = (Story, { parameters }) => { const storyPath = `/story/${parameters.router?.href ?? ''}` return ( - - - - {parameters.enable404 && } - - - + <> +