Skip to content

Commit

Permalink
πŸ“š Add modals support in stories (#4457)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thesan committed Jun 24, 2023
1 parent 67c6681 commit 195f04a
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 108 deletions.
39 changes: 30 additions & 9 deletions packages/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -43,17 +48,33 @@ const RHFDecorator: Decorator = (Story) => {
)
}

const RouterDecorator: Decorator = (Story, { parameters }) => (
<MemoryRouter initialEntries={[`/story/${parameters.router?.href ?? ''}`]}>
<Switch>
<Route component={Story} path={`/story/${parameters.router?.path ?? ''}`} />
<Route exact path="/404" component={NotFound} />
<Redirect from="*" to="/404" />
</Switch>
</MemoryRouter>
const RouterDecorator: Decorator = (Story, { parameters }) => {
const storyPath = `/story/${parameters.router?.href ?? ''}`
return (
<MemoryRouter initialEntries={[storyPath]}>
<Switch>
<Route component={Story} path={`/story/${parameters.router?.path ?? ''}`} />
{parameters.enable404 && <Route path="*" component={NotFound} />}
<Redirect from="*" to={storyPath} />
</Switch>
</MemoryRouter>
)
}

const ModalDecorator: Decorator = (Story) => (
<TransactionStatusProvider>
<ModalContextProvider>
<Story />
<GlobalModals />
<NotificationsHolder>
<TransactionStatus />
</NotificationsHolder>
</ModalContextProvider>
</TransactionStatusProvider>
)

export const decorators = [
ModalDecorator,
stylesWrapperDecorator,
i18nextDecorator,
RHFDecorator,
Expand Down Expand Up @@ -91,7 +112,7 @@ export const parameters = {
options: {
storySort: {
method: 'alphabetical',
order: ['Common'],
order: ['Pages', 'Common'],
},
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export default {
},
referendum: { stage: {} },
},

tx: {
proposalsEngine: { vote: { event: 'Voted' } },
},
},

queryNode: [
Expand Down
10 changes: 6 additions & 4 deletions packages/ui/src/mocks/helpers/asChainData.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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)

case 'Array':
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
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/mocks/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
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'

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')}`
}
Expand Down
86 changes: 86 additions & 0 deletions packages/ui/src/mocks/helpers/transactions.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
]
57 changes: 31 additions & 26 deletions packages/ui/src/mocks/providers/accounts.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -6,15 +7,15 @@ 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'

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 = {
Expand All @@ -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
Expand Down Expand Up @@ -65,27 +68,28 @@ export const MockAccountsProvider: FC<MockAccountsProps> = ({ 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),
},
]
})
Expand Down Expand Up @@ -135,4 +139,5 @@ export const MockAccountsProvider: FC<MockAccountsProps> = ({ 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
44 changes: 26 additions & 18 deletions packages/ui/src/mocks/providers/api.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<T extends Record<any, any>, R, V = any> = {
[K in keyof T]?: T[K] extends R ? V : RecursiveMock<T[K], R, V>
}

type MockApi = {
consts?: Record<string, any>
derive?: Record<string, any>
query?: Record<string, any>
rpc?: Record<string, any>
tx?: Record<string, { paymentInfo?: any; signAndSend?: any }>
consts?: RecursiveMock<AugmentedConsts<'rxjs'>, Codec>
derive?: RecursiveMock<Api['derive'], CallableFunction>
query?: RecursiveMock<AugmentedQueries<'rxjs'>, CallableFunction>
rpc?: RecursiveMock<RpcInterface, CallableFunction>
tx?: RecursiveMock<AugmentedSubmittables<'rxjs'>, CallableFunction, TxMock>
}

export type MockApiProps = { chain?: MockApi }
Expand All @@ -29,12 +39,11 @@ export const MockApiProvider: FC<MockApiProps> = ({ 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: [] },
}

Expand All @@ -46,7 +55,7 @@ export const MockApiProvider: FC<MockApiProps> = ({ children, chain }) => {
query: {},
rpc: {
chain: {
getBlockHash: asApiMethod(createType('BlockHash', blockHash)),
getBlockHash: asApiMethod(createType('BlockHash', BLOCK_HASH)),
subscribeNewHeads: asApiMethod(createType('Header', blockHead)),
},
},
Expand All @@ -58,16 +67,15 @@ export const MockApiProvider: FC<MockApiProps> = ({ 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<TxMock>('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<T>(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<string, any>).forEach(([key, value]) =>
fn(`${kind}.${moduleName}.${key}`, value, moduleName)
)
)
}
}, [chain])
Expand Down
Loading

2 comments on commit 195f04a

@vercel
Copy link

@vercel vercel bot commented on 195f04a Jun 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pioneer-2-storybook – ./

pioneer-2-storybook-joystream.vercel.app
pioneer-2-storybook-git-dev-joystream.vercel.app
pioneer-2-storybook.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 195f04a Jun 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pioneer-2 – ./

pioneer-2-git-dev-joystream.vercel.app
pioneer-2.vercel.app
pioneer-2-joystream.vercel.app

Please sign in to comment.