From df079343d95929c0278a13545f9853c3893cc8ef Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Tue, 4 Jul 2023 12:52:03 +0200 Subject: [PATCH 01/32] Reduce fee re-calculation in `AddNewProposalModal` --- .../AddNewProposal/AddNewProposalModal.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx index e083d9fbc1..7dae196c1a 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx @@ -64,7 +64,7 @@ export type BaseProposalParams = Exclude< const minimalSteps = [{ title: 'Bind account for staking' }, { title: 'Create proposal' }] export const AddNewProposalModal = () => { - const { api, connectionState } = useApi() + const { api } = useApi() const { active: activeMember } = useMyMemberships() const minimumValidatorCount = useMinimumValidatorCount() const maximumReferralCut = api?.consts.members.referralCutMaximumPercent @@ -149,12 +149,14 @@ export const AddNewProposalModal = () => { [state.context.discussionMode] ) + const formValues = form.getValues() as AddNewProposalForm + const formStr = JSON.stringify(formValues) + const { transaction, isLoading, feeInfo } = useTransactionFee( activeMember?.controllerAccount, async () => { if (activeMember && api) { - const { proposalDetails, triggerAndDiscussion, stakingAccount, ...specifics } = - form.getValues() as AddNewProposalForm + const { proposalDetails, triggerAndDiscussion, stakingAccount, ...specifics } = formValues const txBaseParams: BaseProposalParams = { memberId: activeMember?.id, @@ -176,13 +178,7 @@ export const AddNewProposalModal = () => { ]) } }, - [ - state.value, - connectionState, - stakingStatus, - form.formState.isValidating, - JSON.stringify(form.getValues()?.[path as keyof AddNewProposalForm]), - ] + [api?.isConnected, activeMember, stakingStatus, formStr] ) useEffect((): any => { @@ -244,7 +240,7 @@ export const AddNewProposalModal = () => { form.formState.isDirty, isExecutionError, warningAccepted, - JSON.stringify(form.getValues()), + formStr, JSON.stringify(form.formState.errors), isLoading, ]) @@ -309,7 +305,7 @@ export const AddNewProposalModal = () => { } if (state.matches('discussionTransaction')) { - const { triggerAndDiscussion } = form.getValues() as AddNewProposalForm + const { triggerAndDiscussion } = formValues const threadMode = createType('PalletProposalsDiscussionThreadModeBTreeSet', { closed: triggerAndDiscussion.discussionWhitelist?.map((member) => createType('MemberId', Number.parseInt(member.id)) @@ -336,7 +332,7 @@ export const AddNewProposalModal = () => { } if (state.matches('success')) { - const { proposalDetails, proposalType } = form.getValues() as AddNewProposalForm + const { proposalDetails, proposalType } = formValues return ( Date: Wed, 5 Jul 2023 10:28:46 +0200 Subject: [PATCH 02/32] Fix the "warningAccepted" logic --- .../proposals/modals/AddNewProposal/AddNewProposalModal.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx index 7dae196c1a..3de2b54494 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx @@ -77,7 +77,7 @@ export const AddNewProposalModal = () => { const [formMap, setFormMap] = useState>([]) const workingGroupConsts = api?.consts[formMap[2] as GroupIdName] - const [warningAccepted, setWarningAccepted] = useState(true) + const [warningAccepted, setWarningAccepted] = useState(false) const [isExecutionError, setIsExecutionError] = useState(false) const constants = useProposalConstants(formMap[1]) @@ -133,6 +133,7 @@ export const AddNewProposalModal = () => { useEffect(() => { form.trigger([]) + setWarningAccepted(false) }, [path]) useEffect(() => { @@ -207,8 +208,6 @@ export const AddNewProposalModal = () => { } }, [state, stakingStatus, feeInfo]) - useEffect(() => setWarningAccepted(!isExecutionError), [isExecutionError]) - const goToPrevious = useCallback(() => { send('BACK') setIsExecutionError(false) From d1980598f286e7d12418c78221f15417b21b455c Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Tue, 4 Jul 2023 19:43:56 +0200 Subject: [PATCH 03/32] Fix the proposal `WarningModal` --- .../modals/AddNewProposal/components/WarningModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/WarningModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/WarningModal.tsx index d1bb41ef4e..a07b8e6029 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/WarningModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/WarningModal.tsx @@ -40,9 +40,9 @@ export const WarningModal = ({ onNext }: AddNewProposalWarningModalProps) => { - Iā€™m aware of the possible risks associated with creating a proposal. + I'm aware of the possible risks associated with creating a proposal. - + Do not show this message again. From ef03660940cb64ed40b10ec4ddb917a11b4c882b Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Tue, 4 Jul 2023 19:53:23 +0200 Subject: [PATCH 04/32] Test the signal proposal flow --- .../Proposals/CurrentProposals.stories.tsx | 192 ++++++++++++++++-- .../pages/Proposals/PastProposals.stories.tsx | 2 +- .../Proposals/ProposalPreview.stories.tsx | 6 +- packages/ui/src/mocks/data/proposals.ts | 38 +++- packages/ui/src/mocks/providers/api.tsx | 12 +- 5 files changed, 218 insertions(+), 32 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index e082e09d08..6191a1f3d8 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -1,28 +1,30 @@ import { linkTo } from '@storybook/addon-links' -import { Meta, StoryContext, StoryObj } from '@storybook/react' -import { userEvent, within } from '@storybook/testing-library' -import { random } from 'faker' +import { expect, jest } from '@storybook/jest' +import { Meta, ReactRenderer, StoryContext, StoryObj } from '@storybook/react' +import { userEvent, waitFor, within } from '@storybook/testing-library' +import { PlayFunction, StepFunction } from '@storybook/types' import { FC } from 'react' import { member } from '@/mocks/data/members' import { generateProposals, MAX_ACTIVE_PROPOSAL, proposalsPagesChain } from '@/mocks/data/proposals' -import { getButtonByText } from '@/mocks/helpers' +import { Container, getButtonByText, getEditorByLabel, selectFromDropdown, withinModal } from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' import { GetProposalVotesDocument, GetProposalsCountDocument, GetProposalsDocument } from '@/proposals/queries' import { Proposals } from './Proposals' -import { randomMarkdown } from '@/../dev/query-node-mocks/generators/utils' - const PROPOSAL_DATA = { - title: random.words(4), - description: randomMarkdown(), + title: 'Foo bar', + description: '## est minus rerum sed\n\nAssumenda et laboriosam minus accusantium. Sed in quo illum.', } type Args = { isCouncilMember: boolean proposalCount: number - onVote: CallableFunction + onCreateProposal: jest.Mock + onConfirmStakingAccount: jest.Mock + onStakingAccountAdded: jest.Mock + onVote: jest.Mock } type Story = StoryObj> @@ -32,7 +34,10 @@ export default { argTypes: { proposalCount: { control: { type: 'range', max: MAX_ACTIVE_PROPOSAL } }, - onVote: { action: 'Voted' }, + onCreateProposal: { action: 'ProposalsCodex.ProposalCreated' }, + onConfirmStakingAccount: { action: 'Members.StakingAccountConfirmed' }, + onStakingAccountAdded: { action: 'Members.StakingAccountAdded' }, + onVote: { action: 'ProposalsEngine.Voted' }, }, args: { @@ -48,19 +53,38 @@ export default { }, }, - mocks: ({ args }: StoryContext): MocksParameters => { + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: false, + size: 0, + }, + + mocks: ({ args, parameters }: StoryContext): MocksParameters => { const alice = member('alice', { isCouncilMember: args.isCouncilMember }) return { accounts: { active: { member: alice } }, - chain: proposalsPagesChain(args.proposalCount, { - tx: { - proposalsEngine: { - vote: { event: 'Voted', onSend: args.onVote }, - }, + chain: proposalsPagesChain( + { + activeProposalCount: args.proposalCount, + onCreateProposal: args.onCreateProposal, + onConfirmStakingAccount: args.onConfirmStakingAccount, + onStakingAccountAdded: args.onStakingAccountAdded, }, - }), + { + query: { + members: { + stakingAccountIdMemberStatus: parameters.stakingAccountIdMemberStatus, + }, + }, + tx: { + proposalsEngine: { + vote: { event: 'Voted', onSend: args.onVote }, + }, + }, + } + ), queryNode: [ { @@ -102,9 +126,137 @@ export default { export const Default: Story = {} -export const AddNewProposal: Story = { - play: async ({ canvasElement }) => { +// ---------------------------------------------------------------------------- +// Create proposal tests +// ---------------------------------------------------------------------------- + +const alice = member('alice') +const fillGeneralParameters = async ( + modal: Container, + step: StepFunction, + proposalType: string +) => { + await step('General Parameters: Proposal type', async () => { + expect(await modal.findByRole('heading', { name: 'Creating new proposal' })) + const nextButton = getButtonByText(modal, 'Next step') + await userEvent.click(modal.getByText(proposalType)) + await waitFor(() => expect(nextButton).not.toBeDisabled()) + await userEvent.click(nextButton) + }) + + await step('General Parameters: Stake', async () => { + const nextButton = getButtonByText(modal, 'Next step') + await selectFromDropdown(modal, 'Select account for Staking', 'alice') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + await step('General Parameters: Details', async () => { + const nextButton = getButtonByText(modal, 'Next step') + const rationaleEditor = await getEditorByLabel(modal, 'Rationale') + await userEvent.type(modal.getByLabelText('Proposal title'), PROPOSAL_DATA.title) + rationaleEditor.setData(PROPOSAL_DATA.description) + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + await step('General Parameters: Discussion', async () => { + const nextButton = getButtonByText(modal, 'Next step') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) +} + +export const AddNewProposalHappy: Story = { + parameters: { + stakingAccountIdMemberStatus: { + memberId: alice.id, + confirmed: true, + size: 1, + }, + }, + + play: async ({ args, canvasElement, step }) => { const screen = within(canvasElement) - await userEvent.click(getButtonByText(screen, 'Add new proposal')) + const modal = withinModal(canvasElement) + const createProposalButton = getButtonByText(screen, 'Add new proposal') + + await step('Warning Modal', async () => { + const closeModal = async (name: string) => { + const closeButton = (await modal.findByRole('heading', { name })).nextElementSibling + await userEvent.click(closeButton as HTMLElement) + await userEvent.click(getButtonByText(modal, 'Close')) + } + + await step('Temporarily close ', async () => { + await userEvent.click(createProposalButton) + expect(await modal.findByRole('heading', { name: 'Caution' })) + const nextButton = getButtonByText(modal, 'Create A Proposal') + expect(nextButton).toBeDisabled() + await userEvent.click( + modal.getByLabelText("I'm aware of the possible risks associated with creating a proposal.") + ) + await userEvent.click(nextButton) + + await closeModal('Creating new proposal') + + expect(localStorage.getItem('proposalCaution')).toBe(null) + }) + + await step('Permanently close ', async () => { + await userEvent.click(createProposalButton) + expect(await modal.findByRole('heading', { name: 'Caution' })) + const nextButton = getButtonByText(modal, 'Create A Proposal') + await userEvent.click(modal.getByLabelText('Do not show this message again.')) + expect(nextButton).toBeDisabled() + await userEvent.click( + modal.getByLabelText("I'm aware of the possible risks associated with creating a proposal.") + ) + await userEvent.click(nextButton) + + await closeModal('Creating new proposal') + + await userEvent.click(createProposalButton) + await closeModal('Creating new proposal') + + expect(localStorage.getItem('proposalCaution')).toBe('true') + }) + }) + + const createProposal = async (proposalType: string, specificStep: PlayFunction) => { + await userEvent.click(createProposalButton) + + await fillGeneralParameters(modal, step, 'Signal') + + await step(`Specific parameters: ${proposalType}`, specificStep) + + await step('Sign transaction and Create', async () => { + const signButton = getButtonByText(modal, 'Sign transaction and Create') + await userEvent.click(signButton) + const closeButton = (await modal.findByRole('heading', { name: 'Success' })).nextElementSibling + await userEvent.click(closeButton as HTMLElement) + }) + } + + await step('Signal', async () => { + await createProposal('Signal', async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + const editor = await getEditorByLabel(modal, 'Signal') + editor.setData('Lorem ipsum...') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + const [generalParameters, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toHuman()).toEqual({ Signal: 'Lorem ipsum...' }) + expect(generalParameters).toEqual({ + memberId: alice.id, + title: PROPOSAL_DATA.title, + description: PROPOSAL_DATA.description, + stakingAccountId: alice.controllerAccount, + }) + + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) + }) }, } diff --git a/packages/ui/src/app/pages/Proposals/PastProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/PastProposals.stories.tsx index 66236dd6bc..b9a06ab4fd 100644 --- a/packages/ui/src/app/pages/Proposals/PastProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/PastProposals.stories.tsx @@ -46,7 +46,7 @@ export default { mocks: ({ args }: StoryContext): MocksParameters => { return { - chain: proposalsPagesChain(5), + chain: proposalsPagesChain({ activeProposalCount: 5 }), queryNode: [ { diff --git a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx index 271bf84e4a..1e9bf67846 100644 --- a/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/ProposalPreview.stories.tsx @@ -1,4 +1,4 @@ -import { expect } from '@storybook/jest' +import { expect, jest } from '@storybook/jest' import { Meta, StoryContext, StoryObj } from '@storybook/react' import { userEvent, within } from '@storybook/testing-library' import { random } from 'faker' @@ -49,7 +49,7 @@ type Args = { vote1: VoteArg vote2: VoteArg vote3: VoteArg - onVote: CallableFunction + onVote: jest.Mock } type Story = StoryObj> @@ -63,7 +63,7 @@ export default { vote1: { control: { type: 'inline-radio' }, options: voteArgs }, vote2: { control: { type: 'inline-radio' }, options: voteArgs }, vote3: { control: { type: 'inline-radio' }, options: voteArgs }, - onVote: { action: 'Voted' }, + onVote: { action: 'ProposalsEngine.Voted' }, }, args: { diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 196e3739ef..64d183cfc6 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -1,3 +1,4 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types' import { random } from 'faker' import { mapValues, merge } from 'lodash' @@ -131,8 +132,17 @@ export const generateProposals = ( }) }, Math.min(limit, max - offset)) +type ProposalChainProps = { + activeProposalCount: number + onCreateProposal?: CallableFunction + onConfirmStakingAccount?: CallableFunction + onStakingAccountAdded?: CallableFunction +} type Chain = MocksParameters['chain'] -export const proposalsPagesChain = (activeProposalCount: number, extra?: Chain): Chain => +export const proposalsPagesChain = ( + { activeProposalCount, onCreateProposal, onConfirmStakingAccount, onStakingAccountAdded }: ProposalChainProps, + extra?: Chain +): Chain => merge( { consts: { @@ -174,17 +184,35 @@ export const proposalsPagesChain = (activeProposalCount: number, extra?: Chain): }, query: { - members: { membershipPrice: joy(20) }, + members: { + membershipPrice: joy(20), + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: false, + size: 0, + }, + }, + proposalsEngine: { activeProposalCount }, staking: { minimumValidatorCount: 4 }, }, tx: { proposalsCodex: { - createProposal: { event: 'Create' }, + createProposal: { event: 'ProposalCreated', onSend: onCreateProposal }, + }, + + members: { + addStakingAccountCandidate: { event: 'StakingAccountAdded', onSend: onStakingAccountAdded }, + confirmStakingAccount: { event: 'StakingAccountConfirmed', onSend: onConfirmStakingAccount }, + }, + + utility: { + batch: { + onSend: (transactions: SubmittableExtrinsic<'rxjs'>[]) => + transactions.forEach((transaction) => transaction.signAndSend('')), + }, }, - members: { confirmStakingAccount: {} }, - utility: { batch: {} }, }, } satisfies Chain, extra diff --git a/packages/ui/src/mocks/providers/api.tsx b/packages/ui/src/mocks/providers/api.tsx index 056982d6a5..39cc1b3f43 100644 --- a/packages/ui/src/mocks/providers/api.tsx +++ b/packages/ui/src/mocks/providers/api.tsx @@ -1,7 +1,7 @@ import { AugmentedConsts, AugmentedQueries, AugmentedSubmittables } from '@polkadot/api/types' import { RpcInterface } from '@polkadot/rpc-core/types' import { Codec } from '@polkadot/types/types' -import { isFunction, mapValues, merge } from 'lodash' +import { isFunction, isObject, mapValues, merge } from 'lodash' import React, { FC, useEffect, useMemo, useState } from 'react' import { Observable, of } from 'rxjs' @@ -118,7 +118,13 @@ const asApiMethod = (value: any) => { return value } else if (value instanceof Observable) { return () => value - } else { - return () => of(asChainData(value)) } + + const method = () => of(asChainData(value)) + + if (isObject(value) && 'size' in value) { + method.size = () => of(asChainData(value.size)) + } + + return method } From f857f2b7f0d7bad3d420b53b2aef4f6d81c99277 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 5 Jul 2023 16:34:02 +0200 Subject: [PATCH 05/32] Add "General parameters" tests --- packages/ui/.storybook/preview.tsx | 3 + .../Proposals/CurrentProposals.stories.tsx | 248 ++++++++++++++---- packages/ui/src/mocks/helpers/storybook.ts | 8 +- 3 files changed, 197 insertions(+), 62 deletions(-) diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 7c5447c4f2..fc6b5ec906 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -1,4 +1,5 @@ import { Decorator } from '@storybook/react' +import { configure } from '@storybook/testing-library' import React from 'react' import { I18nextProvider } from 'react-i18next' import { useForm, FormProvider } from 'react-hook-form' @@ -15,6 +16,8 @@ import { TransactionStatusProvider } from '../src/common/providers/transactionSt import { MockProvidersDecorator, MockRouterDecorator } from '../src/mocks/providers' import { i18next } from '../src/services/i18n' +configure({ testIdAttribute: 'id' }) + const stylesWrapperDecorator: Decorator = (Story) => ( <> diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 6191a1f3d8..e6ec35e058 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -1,10 +1,11 @@ import { linkTo } from '@storybook/addon-links' import { expect, jest } from '@storybook/jest' import { Meta, ReactRenderer, StoryContext, StoryObj } from '@storybook/react' -import { userEvent, waitFor, within } from '@storybook/testing-library' +import { userEvent, waitFor, waitForElementToBeRemoved, within } from '@storybook/testing-library' import { PlayFunction, StepFunction } from '@storybook/types' import { FC } from 'react' +import { SearchMembersDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { generateProposals, MAX_ACTIVE_PROPOSAL, proposalsPagesChain } from '@/mocks/data/proposals' import { Container, getButtonByText, getEditorByLabel, selectFromDropdown, withinModal } from '@/mocks/helpers' @@ -118,6 +119,13 @@ export default { proposalVotedEvents: [], }, }, + + { + query: SearchMembersDocument, + data: { + memberships: [alice], + }, + }, ], } }, @@ -131,39 +139,42 @@ export const Default: Story = {} // ---------------------------------------------------------------------------- const alice = member('alice') +const waitForModal = (modal: Container, name: string) => modal.findByRole('heading', { name }) const fillGeneralParameters = async ( modal: Container, step: StepFunction, proposalType: string ) => { - await step('General Parameters: Proposal type', async () => { - expect(await modal.findByRole('heading', { name: 'Creating new proposal' })) - const nextButton = getButtonByText(modal, 'Next step') - await userEvent.click(modal.getByText(proposalType)) - await waitFor(() => expect(nextButton).not.toBeDisabled()) - await userEvent.click(nextButton) - }) + let nextButton: HTMLElement - await step('General Parameters: Stake', async () => { - const nextButton = getButtonByText(modal, 'Next step') - await selectFromDropdown(modal, 'Select account for Staking', 'alice') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) - }) + await step('Fill General Parameters', async () => { + await step('Proposal type', async () => { + await waitForModal(modal, 'Creating new proposal') + nextButton = getButtonByText(modal, 'Next step') - await step('General Parameters: Details', async () => { - const nextButton = getButtonByText(modal, 'Next step') - const rationaleEditor = await getEditorByLabel(modal, 'Rationale') - await userEvent.type(modal.getByLabelText('Proposal title'), PROPOSAL_DATA.title) - rationaleEditor.setData(PROPOSAL_DATA.description) - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) - }) + await userEvent.click(modal.getByText(proposalType)) + await waitFor(() => expect(nextButton).not.toBeDisabled()) + await userEvent.click(nextButton) + }) + + await step('Staking account', async () => { + await selectFromDropdown(modal, 'Select account for Staking', 'alice') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + await step('Proposal details', async () => { + const rationaleEditor = await getEditorByLabel(modal, 'Rationale') + await userEvent.type(modal.getByLabelText('Proposal title'), PROPOSAL_DATA.title) + rationaleEditor.setData(PROPOSAL_DATA.description) + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) - await step('General Parameters: Discussion', async () => { - const nextButton = getButtonByText(modal, 'Next step') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) + await step('Trigger & Discussion', async () => { + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) }) } @@ -181,16 +192,36 @@ export const AddNewProposalHappy: Story = { const modal = withinModal(canvasElement) const createProposalButton = getButtonByText(screen, 'Add new proposal') - await step('Warning Modal', async () => { - const closeModal = async (name: string) => { - const closeButton = (await modal.findByRole('heading', { name })).nextElementSibling - await userEvent.click(closeButton as HTMLElement) - await userEvent.click(getButtonByText(modal, 'Close')) - } + // Helpers: + + const closeModal = async (heading: string | HTMLElement) => { + const headingElement = heading instanceof HTMLElement ? heading : modal.getByRole('heading', { name: heading }) + await userEvent.click(headingElement.nextElementSibling as HTMLElement) + await userEvent.click(getButtonByText(modal, 'Close')) + } + const createProposal = async (proposalType: string, specificStep: PlayFunction) => { + await userEvent.click(createProposalButton) + + await fillGeneralParameters(modal, step, proposalType) + + await step(`Specific parameters: ${proposalType}`, specificStep) + + await step('Sign transaction and Create', async () => { + const signButton = getButtonByText(modal, 'Sign transaction and Create') + await userEvent.click(signButton) + const heading = await waitForModal(modal, 'Success') + await userEvent.click(heading?.nextElementSibling as HTMLElement) + }) + } + + // Tests: + + await step('Warning Modal', async () => { await step('Temporarily close ', async () => { await userEvent.click(createProposalButton) - expect(await modal.findByRole('heading', { name: 'Caution' })) + await waitForModal(modal, 'Caution') + const nextButton = getButtonByText(modal, 'Create A Proposal') expect(nextButton).toBeDisabled() await userEvent.click( @@ -205,7 +236,8 @@ export const AddNewProposalHappy: Story = { await step('Permanently close ', async () => { await userEvent.click(createProposalButton) - expect(await modal.findByRole('heading', { name: 'Caution' })) + await waitForModal(modal, 'Caution') + const nextButton = getButtonByText(modal, 'Create A Proposal') await userEvent.click(modal.getByLabelText('Do not show this message again.')) expect(nextButton).toBeDisabled() @@ -217,46 +249,146 @@ export const AddNewProposalHappy: Story = { await closeModal('Creating new proposal') await userEvent.click(createProposalButton) - await closeModal('Creating new proposal') + await closeModal(await waitForModal(modal, 'Creating new proposal')) expect(localStorage.getItem('proposalCaution')).toBe('true') }) }) - const createProposal = async (proposalType: string, specificStep: PlayFunction) => { - await userEvent.click(createProposalButton) + await step('General parameters', async () => { + let nextButton: HTMLElement - await fillGeneralParameters(modal, step, 'Signal') + await step('Proposal type', async () => { + await userEvent.click(createProposalButton) + await waitForModal(modal, 'Creating new proposal') + nextButton = getButtonByText(modal, 'Next step') - await step(`Specific parameters: ${proposalType}`, specificStep) + // TODO test steps - await step('Sign transaction and Create', async () => { - const signButton = getButtonByText(modal, 'Sign transaction and Create') - await userEvent.click(signButton) - const closeButton = (await modal.findByRole('heading', { name: 'Success' })).nextElementSibling - await userEvent.click(closeButton as HTMLElement) + expect(nextButton).toBeDisabled() + await userEvent.click(modal.getByText('Funding Request')) + await waitFor(() => expect(nextButton).not.toBeDisabled()) + await userEvent.click(nextButton) }) - } - await step('Signal', async () => { - await createProposal('Signal', async () => { - const nextButton = getButtonByText(modal, 'Create proposal') - const editor = await getEditorByLabel(modal, 'Signal') - editor.setData('Lorem ipsum...') + await step('Staking account', async () => { + expect(nextButton).toBeDisabled() + await selectFromDropdown(modal, 'Select account for Staking', 'alice') await waitFor(() => expect(nextButton).toBeEnabled()) await userEvent.click(nextButton) }) - const [generalParameters, specificParameters] = args.onCreateProposal.mock.calls.at(-1) - expect(specificParameters.toHuman()).toEqual({ Signal: 'Lorem ipsum...' }) - expect(generalParameters).toEqual({ - memberId: alice.id, - title: PROPOSAL_DATA.title, - description: PROPOSAL_DATA.description, - stakingAccountId: alice.controllerAccount, + await step('Proposal details', async () => { + const titleField = modal.getByLabelText('Proposal title') + const rationaleEditor = await getEditorByLabel(modal, 'Rationale') + + expect(nextButton).toBeDisabled() + + // Somehow reason the validation for the title field stop working once the editor setData + // However it only happens with tests `userEvent` and not when interacting manually with the story + await userEvent.type(titleField, PROPOSAL_DATA.title.padEnd(41, ' baz')) + rationaleEditor.setData(PROPOSAL_DATA.description) + const titleValidation = await modal.findByText('Title exceeds maximum length') + + expect(titleValidation) + expect(nextButton).toBeDisabled() + + await userEvent.clear(titleField) + await userEvent.type(titleField, PROPOSAL_DATA.title) + + await waitForElementToBeRemoved(titleValidation) + expect(nextButton).toBeEnabled() + + rationaleEditor.setData(PROPOSAL_DATA.description.padEnd(3002, ' baz')) + const rationaleValidation = await modal.findByText('Rationale exceeds maximum length') + + expect(rationaleValidation) + expect(nextButton).toBeDisabled() + + rationaleEditor.setData(PROPOSAL_DATA.description) + + await waitForElementToBeRemoved(rationaleValidation) + expect(nextButton).toBeEnabled() + + await userEvent.click(nextButton) }) - expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) + await step('Trigger & Discussion', async () => { + await step('Trigger', async () => { + expect(nextButton).toBeEnabled() + + await userEvent.click(modal.getByText('Yes')) + expect(nextButton).toBeDisabled() + const blockInput = modal.getByRole('textbox') + await userEvent.type(blockInput, '5000') + + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.clear(blockInput) + await userEvent.type(blockInput, '10') + + expect(await modal.findByText(/The minimum block number is \d+/)) + expect(nextButton).toBeDisabled() + + // This "too high" test case seems to fail due to a RHF validation bug + // await userEvent.type(blockInput, '999999999') + + await userEvent.clear(blockInput) + await userEvent.type(blockInput, '9999') + + expect(await modal.findByText(/^ā‰ˆ.*/)) + expect(modal.queryByText(/The minimum block number is \d+/)).toBeNull() + expect(nextButton).toBeEnabled() + }) + + await step('Discussion Mode', async () => { + await userEvent.click(modal.getByText('Closed')) + + await waitFor(() => expect(nextButton).toBeDisabled()) + await selectFromDropdown(modal, 'Add member to whitelist', 'alice') + + expect(await modal.findByText('alice')) + expect(nextButton).toBeEnabled() + + userEvent.click(screen.getByTestId('removeMember')) + expect(modal.queryByText('alice')).toBeNull() + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.click(nextButton) + + expect(modal.getByText('Specific parameters', { selector: 'h4' })) + }) + }) + }) + + closeModal('Creating new proposal: Funding Request') + await waitFor(() => expect(createProposalButton).toBeEnabled()) + + await step('Specific parameters', async () => { + await step('Signal', async () => { + await createProposal('Signal', async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + const editor = await getEditorByLabel(modal, 'Signal') + editor.setData('Lorem ipsum...') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + const [generalParameters, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toHuman()).toEqual({ Signal: 'Lorem ipsum...' }) + expect(generalParameters).toEqual({ + memberId: alice.id, + title: PROPOSAL_DATA.title, + description: PROPOSAL_DATA.description, + stakingAccountId: alice.controllerAccount, + }) + + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) + }) }) }, } + +// TODO: +// - No active member +// - Not enough funds diff --git a/packages/ui/src/mocks/helpers/storybook.ts b/packages/ui/src/mocks/helpers/storybook.ts index 34ea9fa1d8..087094b92c 100644 --- a/packages/ui/src/mocks/helpers/storybook.ts +++ b/packages/ui/src/mocks/helpers/storybook.ts @@ -42,10 +42,10 @@ export const getEditorByLabel = async ( return editor as HTMLElement & { setData: (data: string) => void } } -export const selectFromDropdown = async (container: Container, labelText: string | RegExp, name: string) => { - const label = container.getByText(labelText) - const toggle = label.parentElement?.querySelector('.ui-toggle') - if (!toggle) throw `Found a label with the text of: ${labelText}, however no dropdown is associated with this label.` +export const selectFromDropdown = async (container: Container, label: string | RegExp | HTMLElement, name: string) => { + const labelElement = label instanceof HTMLElement ? label : container.getByText(label) + const toggle = labelElement.parentElement?.querySelector('.ui-toggle') + if (!toggle) throw `Found a label: ${label.toString()}, however no dropdown is associated with this label.` await userEvent.click(toggle) From 19a3fc3eb6d07c0115f952a6cbf53bb2aad1a4a9 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 7 Jul 2023 13:57:43 +0200 Subject: [PATCH 06/32] Add "Funding Request" and "Set Referral Cut" tests --- .../Proposals/CurrentProposals.stories.tsx | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index e6ec35e058..dd7a69c683 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -138,6 +138,7 @@ export const Default: Story = {} // Create proposal tests // ---------------------------------------------------------------------------- +const EXECUTION_WARNING_BOX = 'I understand the implications of overriding the execution constraints validation.' const alice = member('alice') const waitForModal = (modal: Container, name: string) => modal.findByRole('heading', { name }) const fillGeneralParameters = async ( @@ -190,7 +191,6 @@ export const AddNewProposalHappy: Story = { play: async ({ args, canvasElement, step }) => { const screen = within(canvasElement) const modal = withinModal(canvasElement) - const createProposalButton = getButtonByText(screen, 'Add new proposal') // Helpers: @@ -201,6 +201,11 @@ export const AddNewProposalHappy: Story = { } const createProposal = async (proposalType: string, specificStep: PlayFunction) => { + const createProposalButton = getButtonByText(screen, 'Add new proposal') + + await waitFor(() => expect(createProposalButton).toBeEnabled()) + await new Promise((res) => setTimeout(res, 2000)) // TODO try without a timeout + await userEvent.click(createProposalButton) await fillGeneralParameters(modal, step, proposalType) @@ -218,6 +223,8 @@ export const AddNewProposalHappy: Story = { // Tests: await step('Warning Modal', async () => { + const createProposalButton = getButtonByText(screen, 'Add new proposal') + await step('Temporarily close ', async () => { await userEvent.click(createProposalButton) await waitForModal(modal, 'Caution') @@ -259,6 +266,7 @@ export const AddNewProposalHappy: Story = { let nextButton: HTMLElement await step('Proposal type', async () => { + const createProposalButton = getButtonByText(screen, 'Add new proposal') await userEvent.click(createProposalButton) await waitForModal(modal, 'Creating new proposal') nextButton = getButtonByText(modal, 'Next step') @@ -362,15 +370,27 @@ export const AddNewProposalHappy: Story = { }) closeModal('Creating new proposal: Funding Request') - await waitFor(() => expect(createProposalButton).toBeEnabled()) await step('Specific parameters', async () => { await step('Signal', async () => { await createProposal('Signal', async () => { const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + const editor = await getEditorByLabel(modal, 'Signal') + + // Valid + editor.setData('Lorem ipsum...') + await waitFor(() => expect(nextButton).toBeEnabled()) + + // Invalid + editor.setData('') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // Valid again editor.setData('Lorem ipsum...') await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) }) @@ -385,6 +405,61 @@ export const AddNewProposalHappy: Story = { expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) }) + + await step('Funding Request', async () => { + await createProposal('Funding Request', async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Valid + await userEvent.type(amountField, '100') + expect(nextButton).toBeDisabled() + await selectFromDropdown(modal, 'Recipient account', 'alice') + await waitFor(() => expect(nextButton).toBeEnabled()) + + // Invalid + await userEvent.clear(amountField) + await userEvent.type(amountField, '100000') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // Valid again + await userEvent.clear(amountField) + await userEvent.type(amountField, '100') + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.click(nextButton) + }) + }) + + await step('Set Referral Cut', async () => { + await createProposal('Set Referral Cut', async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Valid + await userEvent.type(amountField, '40') + await waitFor(() => expect(nextButton).toBeEnabled()) + + // Invalid: creation constraints + await userEvent.clear(amountField) + await userEvent.type(amountField, '200') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // Execution constraints warning + await userEvent.clear(amountField) + await userEvent.type(amountField, '100') + expect(await modal.findByText('Input must be equal or less than 50% for proposal to execute')) + expect(nextButton).toBeDisabled() + userEvent.click(modal.getByText(EXECUTION_WARNING_BOX)) + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.click(nextButton) + }) + }) }) }, } From c51942fdf52693fb7f9a2568d2220c1e9625f355 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 7 Jul 2023 16:52:59 +0200 Subject: [PATCH 07/32] Fix race condition during form validation --- packages/ui/src/common/utils/validation.tsx | 38 ++++++++++--------- .../AddNewProposal/AddNewProposalModal.tsx | 35 ++++++----------- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/packages/ui/src/common/utils/validation.tsx b/packages/ui/src/common/utils/validation.tsx index 8e6e338b3e..21abd6b935 100644 --- a/packages/ui/src/common/utils/validation.tsx +++ b/packages/ui/src/common/utils/validation.tsx @@ -1,7 +1,7 @@ import { isBn } from '@polkadot/util' import BN from 'bn.js' -import { at, get } from 'lodash' -import React, { useCallback } from 'react' +import { at, get, merge } from 'lodash' +import React, { useCallback, useRef } from 'react' import { FieldErrors, FieldValues, Resolver } from 'react-hook-form' import { FieldError } from 'react-hook-form/dist/types/errors' import { DeepMap, DeepPartial } from 'react-hook-form/dist/types/utils' @@ -188,24 +188,27 @@ interface IFormError { export const useYupValidationResolver = ( validationSchema: AnyObjectSchema, path?: string -): Resolver => - useCallback( +): Resolver => { + const validationsPromise = useRef>(Promise.resolve()) + + return useCallback( async (data, context) => { let values + + // Deep clone data since it's "by reference" attributes values might change by the time it runs + const _data = merge({}, data) + const options = { + abortEarly: false, + context, + stripUnknown: true, + } + const validate = () => + path ? validationSchema.validateSyncAt(path, _data, options) : validationSchema.validateSync(_data, options) + + validationsPromise.current = validationsPromise.current.then(validate, validate) + try { - if (path) { - values = await validationSchema.validateSyncAt(path, data, { - abortEarly: false, - context, - stripUnknown: true, - }) - } else { - values = await validationSchema.validateSync(data, { - abortEarly: false, - context, - stripUnknown: true, - }) - } + values = await validationsPromise.current return { values, @@ -220,6 +223,7 @@ export const useYupValidationResolver = ( }, [validationSchema, path] ) +} export interface ValidationHelpers { errorMessageGetter: (field: string) => string | undefined diff --git a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx index 3de2b54494..113c6a43f6 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/AddNewProposalModal.tsx @@ -86,7 +86,7 @@ export const AddNewProposalModal = () => { const stakingStatus = useStakingAccountStatus(formMap[0]?.address, activeMember?.id, [state.matches('transaction')]) const schema = useMemo(() => schemaFactory(api), [!api]) - const path = useMemo(() => machineStateConverter(state.value), [state.value]) + const path = useMemo(() => machineStateConverter(state.value) as keyof AddNewProposalForm, [state.value]) const form = useForm({ resolver: useYupValidationResolver(schema, path), mode: 'onChange', @@ -109,6 +109,11 @@ export const AddNewProposalModal = () => { defaultValues: defaultProposalValues, }) + const formValues = form.getValues() as AddNewProposalForm + const currentErrors = form.formState.errors[path] ?? {} + const serializedCurrentForm = JSON.stringify(formValues[path]) + const serializedCurrentFormErrors = JSON.stringify(currentErrors) + const mapDependencies = form.watch([ 'stakingAccount.stakingAccount', 'proposalType.type', @@ -137,12 +142,8 @@ export const AddNewProposalModal = () => { }, [path]) useEffect(() => { - setIsExecutionError( - Object.values((form.formState.errors as any)[path] ?? {}).some( - (value) => (value as FieldError).type === 'execution' - ) - ) - }, [JSON.stringify(form.formState.errors), path]) + setIsExecutionError(Object.values(currentErrors).some((value) => (value as FieldError).type === 'execution')) + }, [serializedCurrentFormErrors]) const transactionsSteps = useMemo( () => @@ -150,9 +151,6 @@ export const AddNewProposalModal = () => { [state.context.discussionMode] ) - const formValues = form.getValues() as AddNewProposalForm - const formStr = JSON.stringify(formValues) - const { transaction, isLoading, feeInfo } = useTransactionFee( activeMember?.controllerAccount, async () => { @@ -179,7 +177,7 @@ export const AddNewProposalModal = () => { ]) } }, - [api?.isConnected, activeMember, stakingStatus, formStr] + [api?.isConnected, activeMember, stakingStatus, serializedCurrentForm] ) useEffect((): any => { @@ -218,14 +216,11 @@ export const AddNewProposalModal = () => { return true } if (isExecutionError) { - const hasOtherError = Object.values((form.formState.errors as any)[path] ?? {}).some( - (value) => (value as FieldError).type !== 'execution' - ) - if (!form.formState.isDirty) { return true } + const hasOtherError = Object.values(currentErrors).some((value) => (value as FieldError).type !== 'execution') if (!hasOtherError) { return !warningAccepted } @@ -234,15 +229,7 @@ export const AddNewProposalModal = () => { } return !form.formState.isValid - }, [ - form.formState.isValid, - form.formState.isDirty, - isExecutionError, - warningAccepted, - formStr, - JSON.stringify(form.formState.errors), - isLoading, - ]) + }, [form.formState.isValid, form.formState.isDirty, isExecutionError, warningAccepted, isLoading]) if (!api || !activeMember || !feeInfo || state.matches('requirementsVerification')) { return null From df40a870e63ed50aa5929d64dad4a5f2e3850465 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 7 Jul 2023 16:54:25 +0200 Subject: [PATCH 08/32] Fix incomplete tests --- .../Proposals/CurrentProposals.stories.tsx | 70 ++++++++----------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index dd7a69c683..f29163e4e9 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -292,29 +292,26 @@ export const AddNewProposalHappy: Story = { expect(nextButton).toBeDisabled() - // Somehow reason the validation for the title field stop working once the editor setData - // However it only happens with tests `userEvent` and not when interacting manually with the story - await userEvent.type(titleField, PROPOSAL_DATA.title.padEnd(41, ' baz')) + // Invalid title rationaleEditor.setData(PROPOSAL_DATA.description) + await userEvent.clear(titleField) + await userEvent.type( + titleField, + 'Reprehenderit laborum veniam est ut magna velit velit deserunt reprehenderit dolore.' + ) const titleValidation = await modal.findByText('Title exceeds maximum length') - - expect(titleValidation) expect(nextButton).toBeDisabled() + // Invalid rational await userEvent.clear(titleField) await userEvent.type(titleField, PROPOSAL_DATA.title) - - await waitForElementToBeRemoved(titleValidation) - expect(nextButton).toBeEnabled() - rationaleEditor.setData(PROPOSAL_DATA.description.padEnd(3002, ' baz')) const rationaleValidation = await modal.findByText('Rationale exceeds maximum length') - - expect(rationaleValidation) + expect(titleValidation).not.toBeInTheDocument() expect(nextButton).toBeDisabled() + // Valid rationaleEditor.setData(PROPOSAL_DATA.description) - await waitForElementToBeRemoved(rationaleValidation) expect(nextButton).toBeEnabled() @@ -327,25 +324,25 @@ export const AddNewProposalHappy: Story = { await userEvent.click(modal.getByText('Yes')) expect(nextButton).toBeDisabled() - const blockInput = modal.getByRole('textbox') - await userEvent.type(blockInput, '5000') - await waitFor(() => expect(nextButton).toBeEnabled()) + const blockInput = modal.getByRole('textbox') - await userEvent.clear(blockInput) + // Invalid: too low await userEvent.type(blockInput, '10') - expect(await modal.findByText(/The minimum block number is \d+/)) expect(nextButton).toBeDisabled() - // This "too high" test case seems to fail due to a RHF validation bug - // await userEvent.type(blockInput, '999999999') + // Invalid: too high + await userEvent.type(blockInput, '999999999') + await waitFor(() => expect(modal.queryByText(/The minimum block number is \d+/)).toBeNull()) + expect(await modal.findByText(/The maximum block number is \d+/)) + expect(nextButton).toBeDisabled() + // Valid await userEvent.clear(blockInput) await userEvent.type(blockInput, '9999') - + await waitFor(() => expect(modal.queryByText(/The maximum block number is \d+/)).toBeNull()) expect(await modal.findByText(/^ā‰ˆ.*/)) - expect(modal.queryByText(/The minimum block number is \d+/)).toBeNull() expect(nextButton).toBeEnabled() }) @@ -367,9 +364,9 @@ export const AddNewProposalHappy: Story = { expect(modal.getByText('Specific parameters', { selector: 'h4' })) }) }) - }) - closeModal('Creating new proposal: Funding Request') + closeModal('Creating new proposal: Funding Request') + }) await step('Specific parameters', async () => { await step('Signal', async () => { @@ -379,17 +376,15 @@ export const AddNewProposalHappy: Story = { const editor = await getEditorByLabel(modal, 'Signal') - // Valid - editor.setData('Lorem ipsum...') - await waitFor(() => expect(nextButton).toBeEnabled()) - // Invalid editor.setData('') - await waitFor(() => expect(nextButton).toBeDisabled()) + const validation = await modal.findByText('Field is required') + expect(nextButton).toBeDisabled() - // Valid again + // Valid editor.setData('Lorem ipsum...') - await waitFor(() => expect(nextButton).toBeEnabled()) + await waitForElementToBeRemoved(validation) + expect(nextButton).toBeEnabled() await userEvent.click(nextButton) }) @@ -413,21 +408,18 @@ export const AddNewProposalHappy: Story = { const amountField = modal.getByTestId('amount-input') - // Valid - await userEvent.type(amountField, '100') - expect(nextButton).toBeDisabled() - await selectFromDropdown(modal, 'Recipient account', 'alice') - await waitFor(() => expect(nextButton).toBeEnabled()) - // Invalid + await selectFromDropdown(modal, 'Recipient account', 'alice') await userEvent.clear(amountField) - await userEvent.type(amountField, '100000') - await waitFor(() => expect(nextButton).toBeDisabled()) + await userEvent.type(amountField, '166667') + expect(await modal.findByText(/Maximal amount allowed is \d+/)) + expect(nextButton).toBeDisabled() // Valid again await userEvent.clear(amountField) await userEvent.type(amountField, '100') - await waitFor(() => expect(nextButton).toBeEnabled()) + await waitFor(() => expect(modal.queryByText(/Maximal amount allowed is \d+/)).toBeNull()) + await expect(nextButton).toBeEnabled() await userEvent.click(nextButton) }) From ca203f2e116efb6f924a2169328ff0a6c597373f Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 7 Jul 2023 18:53:34 +0200 Subject: [PATCH 09/32] Use one story per proposal type --- .../Proposals/CurrentProposals.stories.tsx | 349 +++++++++++------- packages/ui/src/mocks/data/proposals.ts | 20 +- 2 files changed, 226 insertions(+), 143 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index f29163e4e9..0bc807e0fb 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -2,7 +2,7 @@ import { linkTo } from '@storybook/addon-links' import { expect, jest } from '@storybook/jest' import { Meta, ReactRenderer, StoryContext, StoryObj } from '@storybook/react' import { userEvent, waitFor, waitForElementToBeRemoved, within } from '@storybook/testing-library' -import { PlayFunction, StepFunction } from '@storybook/types' +import { PlayFunction, PlayFunctionContext, StepFunction } from '@storybook/types' import { FC } from 'react' import { SearchMembersDocument } from '@/memberships/queries' @@ -23,8 +23,9 @@ type Args = { isCouncilMember: boolean proposalCount: number onCreateProposal: jest.Mock + onThreadChangeThreadMode: jest.Mock onConfirmStakingAccount: jest.Mock - onStakingAccountAdded: jest.Mock + onAddStakingAccountCandidate: jest.Mock onVote: jest.Mock } type Story = StoryObj> @@ -36,8 +37,9 @@ export default { argTypes: { proposalCount: { control: { type: 'range', max: MAX_ACTIVE_PROPOSAL } }, onCreateProposal: { action: 'ProposalsCodex.ProposalCreated' }, + onThreadChangeThreadMode: { action: 'proposalsDiscussion.ThreadModeChanged' }, onConfirmStakingAccount: { action: 'Members.StakingAccountConfirmed' }, - onStakingAccountAdded: { action: 'Members.StakingAccountAdded' }, + onAddStakingAccountCandidate: { action: 'Members.StakingAccountAdded' }, onVote: { action: 'ProposalsEngine.Voted' }, }, @@ -70,8 +72,9 @@ export default { { activeProposalCount: args.proposalCount, onCreateProposal: args.onCreateProposal, + onThreadChangeThreadMode: args.onThreadChangeThreadMode, + onAddStakingAccountCandidate: args.onAddStakingAccountCandidate, onConfirmStakingAccount: args.onConfirmStakingAccount, - onStakingAccountAdded: args.onStakingAccountAdded, }, { query: { @@ -135,93 +138,33 @@ export default { export const Default: Story = {} // ---------------------------------------------------------------------------- -// Create proposal tests +// Create proposal: General parameters tests // ---------------------------------------------------------------------------- -const EXECUTION_WARNING_BOX = 'I understand the implications of overriding the execution constraints validation.' const alice = member('alice') const waitForModal = (modal: Container, name: string) => modal.findByRole('heading', { name }) -const fillGeneralParameters = async ( - modal: Container, - step: StepFunction, - proposalType: string -) => { - let nextButton: HTMLElement - - await step('Fill General Parameters', async () => { - await step('Proposal type', async () => { - await waitForModal(modal, 'Creating new proposal') - nextButton = getButtonByText(modal, 'Next step') - - await userEvent.click(modal.getByText(proposalType)) - await waitFor(() => expect(nextButton).not.toBeDisabled()) - await userEvent.click(nextButton) - }) - - await step('Staking account', async () => { - await selectFromDropdown(modal, 'Select account for Staking', 'alice') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) - }) - await step('Proposal details', async () => { - const rationaleEditor = await getEditorByLabel(modal, 'Rationale') - await userEvent.type(modal.getByLabelText('Proposal title'), PROPOSAL_DATA.title) - rationaleEditor.setData(PROPOSAL_DATA.description) - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) - }) - - await step('Trigger & Discussion', async () => { - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) - }) - }) +const hasStakingAccountParameters = { + stakingAccountIdMemberStatus: { + memberId: alice.id, + confirmed: true, + size: 1, + }, } export const AddNewProposalHappy: Story = { - parameters: { - stakingAccountIdMemberStatus: { - memberId: alice.id, - confirmed: true, - size: 1, - }, - }, + parameters: hasStakingAccountParameters, play: async ({ args, canvasElement, step }) => { const screen = within(canvasElement) const modal = withinModal(canvasElement) - // Helpers: - const closeModal = async (heading: string | HTMLElement) => { const headingElement = heading instanceof HTMLElement ? heading : modal.getByRole('heading', { name: heading }) await userEvent.click(headingElement.nextElementSibling as HTMLElement) await userEvent.click(getButtonByText(modal, 'Close')) } - const createProposal = async (proposalType: string, specificStep: PlayFunction) => { - const createProposalButton = getButtonByText(screen, 'Add new proposal') - - await waitFor(() => expect(createProposalButton).toBeEnabled()) - await new Promise((res) => setTimeout(res, 2000)) // TODO try without a timeout - - await userEvent.click(createProposalButton) - - await fillGeneralParameters(modal, step, proposalType) - - await step(`Specific parameters: ${proposalType}`, specificStep) - - await step('Sign transaction and Create', async () => { - const signButton = getButtonByText(modal, 'Sign transaction and Create') - await userEvent.click(signButton) - const heading = await waitForModal(modal, 'Success') - await userEvent.click(heading?.nextElementSibling as HTMLElement) - }) - } - - // Tests: - await step('Warning Modal', async () => { const createProposalButton = getButtonByText(screen, 'Add new proposal') @@ -274,7 +217,7 @@ export const AddNewProposalHappy: Story = { // TODO test steps expect(nextButton).toBeDisabled() - await userEvent.click(modal.getByText('Funding Request')) + await userEvent.click(modal.getByText('Signal')) await waitFor(() => expect(nextButton).not.toBeDisabled()) await userEvent.click(nextButton) }) @@ -363,99 +306,229 @@ export const AddNewProposalHappy: Story = { expect(modal.getByText('Specific parameters', { selector: 'h4' })) }) - }) - - closeModal('Creating new proposal: Funding Request') - }) - - await step('Specific parameters', async () => { - await step('Signal', async () => { - await createProposal('Signal', async () => { - const nextButton = getButtonByText(modal, 'Create proposal') - expect(nextButton).toBeDisabled() + await step('Specific parameters', async () => { const editor = await getEditorByLabel(modal, 'Signal') - - // Invalid - editor.setData('') - const validation = await modal.findByText('Field is required') - expect(nextButton).toBeDisabled() - - // Valid editor.setData('Lorem ipsum...') - await waitForElementToBeRemoved(validation) - expect(nextButton).toBeEnabled() + await waitFor(() => expect(nextButton).toBeEnabled()) await userEvent.click(nextButton) }) + }) - const [generalParameters, specificParameters] = args.onCreateProposal.mock.calls.at(-1) - expect(specificParameters.toHuman()).toEqual({ Signal: 'Lorem ipsum...' }) + await step('Sign Create Proposal transaction', async () => { + expect(modal.getByText('You intend to create a proposal.')) + await userEvent.click(modal.getByText('Sign transaction and Create')) + }) + + await step('Sign set discussion mode transaction', async () => { + expect(await modal.findByText('You intend to change the proposal discussion thread mode.')) + await userEvent.click(modal.getByText('Sign transaction and change mode')) + expect(await waitForModal(modal, 'Success')) + }) + + step('Transaction parameters', () => { + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) + + const [generalParameters] = args.onCreateProposal.mock.calls.at(-1) expect(generalParameters).toEqual({ memberId: alice.id, title: PROPOSAL_DATA.title, description: PROPOSAL_DATA.description, stakingAccountId: alice.controllerAccount, + exactExecutionBlock: 9999, }) - expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) + const changeModeTxParams = args.onThreadChangeThreadMode.mock.calls.at(-1) + expect(changeModeTxParams.length).toBe(3) + const [memberId, threadId, mode] = changeModeTxParams + expect(memberId).toBe(alice.id) + expect(typeof threadId).toBe('number') + expect(mode.toJSON()).toEqual({ closed: [] }) }) + }) + }, +} - await step('Funding Request', async () => { - await createProposal('Funding Request', async () => { - const nextButton = getButtonByText(modal, 'Create proposal') - expect(nextButton).toBeDisabled() +// TODO: +// - No active member +// - Not enough funds - const amountField = modal.getByTestId('amount-input') +// ---------------------------------------------------------------------------- +// Create proposal: Specific parameters tests helpers +// ---------------------------------------------------------------------------- - // Invalid - await selectFromDropdown(modal, 'Recipient account', 'alice') - await userEvent.clear(amountField) - await userEvent.type(amountField, '166667') - expect(await modal.findByText(/Maximal amount allowed is \d+/)) - expect(nextButton).toBeDisabled() +const EXECUTION_WARNING_BOX = 'I understand the implications of overriding the execution constraints validation.' +const fillGeneralParameters = async ( + modal: Container, + step: StepFunction, + proposalType: string +) => { + let nextButton: HTMLElement - // Valid again - await userEvent.clear(amountField) - await userEvent.type(amountField, '100') - await waitFor(() => expect(modal.queryByText(/Maximal amount allowed is \d+/)).toBeNull()) - await expect(nextButton).toBeEnabled() + await step('Fill General Parameters', async () => { + await step('Proposal type', async () => { + await waitForModal(modal, 'Creating new proposal') + nextButton = getButtonByText(modal, 'Next step') - await userEvent.click(nextButton) - }) + await userEvent.click(modal.getByText(proposalType)) + await waitFor(() => expect(nextButton).not.toBeDisabled()) + await userEvent.click(nextButton) + }) + + await step('Staking account', async () => { + await selectFromDropdown(modal, 'Select account for Staking', 'alice') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + await step('Proposal details', async () => { + const rationaleEditor = await getEditorByLabel(modal, 'Rationale') + await userEvent.type(modal.getByLabelText('Proposal title'), PROPOSAL_DATA.title) + rationaleEditor.setData(PROPOSAL_DATA.description) + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + await step('Trigger & Discussion', async () => { + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + }) +} + +type SpecificParametersTestFunction = ( + args: Pick, 'args' | 'parameters' | 'step'> & { + modal: Container + createProposal: (create: () => Promise) => Promise + } +) => Promise +const specificParametersTest = + (proposalType: string, specificStep: SpecificParametersTestFunction): PlayFunction => + async ({ args, parameters, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + const createProposal = async (create: () => Promise) => { + localStorage.setItem('proposalCaution', 'true') + + await userEvent.click(getButtonByText(screen, 'Add new proposal')) + + await fillGeneralParameters(modal, step, proposalType) + + await step(`Specific parameters: ${proposalType}`, create) + + await step('Sign transaction and Create', async () => { + await userEvent.click(modal.getByText('Sign transaction and Create')) + expect(await waitForModal(modal, 'Success')) }) + } - await step('Set Referral Cut', async () => { - await createProposal('Set Referral Cut', async () => { - const nextButton = getButtonByText(modal, 'Create proposal') - expect(nextButton).toBeDisabled() + await specificStep({ args, parameters, createProposal, modal, step }) + } - const amountField = modal.getByTestId('amount-input') +// ---------------------------------------------------------------------------- +// Create proposal: Specific parameters tests +// ---------------------------------------------------------------------------- - // Valid - await userEvent.type(amountField, '40') - await waitFor(() => expect(nextButton).toBeEnabled()) +export const SpecificParametersSignal: Story = { + parameters: hasStakingAccountParameters, - // Invalid: creation constraints - await userEvent.clear(amountField) - await userEvent.type(amountField, '200') - await waitFor(() => expect(nextButton).toBeDisabled()) + play: specificParametersTest('Signal', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() - // Execution constraints warning - await userEvent.clear(amountField) - await userEvent.type(amountField, '100') - expect(await modal.findByText('Input must be equal or less than 50% for proposal to execute')) - expect(nextButton).toBeDisabled() - userEvent.click(modal.getByText(EXECUTION_WARNING_BOX)) - await waitFor(() => expect(nextButton).toBeEnabled()) + const editor = await getEditorByLabel(modal, 'Signal') - await userEvent.click(nextButton) - }) + // Invalid + editor.setData('') + const validation = await modal.findByText('Field is required') + expect(nextButton).toBeDisabled() + + // Valid + editor.setData('Lorem ipsum...') + await waitForElementToBeRemoved(validation) + expect(nextButton).toBeEnabled() + + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toHuman()).toEqual({ Signal: 'Lorem ipsum...' }) + }) + }), +} + +export const SpecificParametersFundingRequest: Story = { + parameters: hasStakingAccountParameters, + + play: specificParametersTest('Funding Request', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Invalid + await selectFromDropdown(modal, 'Recipient account', 'alice') + await userEvent.clear(amountField) + await userEvent.type(amountField, '166667') + expect(await modal.findByText(/Maximal amount allowed is \d+/)) + expect(nextButton).toBeDisabled() + + // Valid again + await userEvent.clear(amountField) + await userEvent.type(amountField, '100') + await waitFor(() => expect(modal.queryByText(/Maximal amount allowed is \d+/)).toBeNull()) + await expect(nextButton).toBeEnabled() + + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + fundingRequest: [{ account: alice.controllerAccount, amount: 100_0000000000 }], }) }) - }, + }), } -// TODO: -// - No active member -// - Not enough funds +export const SpecificParametersSetReferralCut: Story = { + parameters: hasStakingAccountParameters, + + play: specificParametersTest('Set Referral Cut', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Valid + await userEvent.type(amountField, '40') + await waitFor(() => expect(nextButton).toBeEnabled()) + + // Invalid: creation constraints + await userEvent.clear(amountField) + await userEvent.type(amountField, '200') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // Execution constraints warning + await userEvent.clear(amountField) + await userEvent.type(amountField, '100') + expect(await modal.findByText('Input must be equal or less than 50% for proposal to execute')) + expect(nextButton).toBeDisabled() + userEvent.click(modal.getByText(EXECUTION_WARNING_BOX)) + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setReferralCut: 100 }) + }) + }), +} diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 64d183cfc6..8935ac3633 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -134,13 +134,20 @@ export const generateProposals = ( type ProposalChainProps = { activeProposalCount: number - onCreateProposal?: CallableFunction - onConfirmStakingAccount?: CallableFunction - onStakingAccountAdded?: CallableFunction + onCreateProposal?: jest.Mock + onThreadChangeThreadMode?: jest.Mock + onAddStakingAccountCandidate?: jest.Mock + onConfirmStakingAccount?: jest.Mock } type Chain = MocksParameters['chain'] export const proposalsPagesChain = ( - { activeProposalCount, onCreateProposal, onConfirmStakingAccount, onStakingAccountAdded }: ProposalChainProps, + { + activeProposalCount, + onCreateProposal, + onThreadChangeThreadMode, + onAddStakingAccountCandidate, + onConfirmStakingAccount, + }: ProposalChainProps, extra?: Chain ): Chain => merge( @@ -201,9 +208,12 @@ export const proposalsPagesChain = ( proposalsCodex: { createProposal: { event: 'ProposalCreated', onSend: onCreateProposal }, }, + proposalsDiscussion: { + changeThreadMode: { event: 'ThreadModeChanged', onSend: onThreadChangeThreadMode }, + }, members: { - addStakingAccountCandidate: { event: 'StakingAccountAdded', onSend: onStakingAccountAdded }, + addStakingAccountCandidate: { event: 'StakingAccountAdded', onSend: onAddStakingAccountCandidate }, confirmStakingAccount: { event: 'StakingAccountConfirmed', onSend: onConfirmStakingAccount }, }, From f6b3c99e1e0413377f2ec98f633c9ccae6317739 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Sat, 8 Jul 2023 19:12:23 +0200 Subject: [PATCH 10/32] Add the "Decrease Working Group Lead Stake" test --- .../Proposals/CurrentProposals.stories.tsx | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 0bc807e0fb..df2787a61d 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -8,9 +8,10 @@ import { FC } from 'react' import { SearchMembersDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { generateProposals, MAX_ACTIVE_PROPOSAL, proposalsPagesChain } from '@/mocks/data/proposals' -import { Container, getButtonByText, getEditorByLabel, selectFromDropdown, withinModal } from '@/mocks/helpers' +import { Container, getButtonByText, getEditorByLabel, joy, selectFromDropdown, withinModal } from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' import { GetProposalVotesDocument, GetProposalsCountDocument, GetProposalsDocument } from '@/proposals/queries' +import { GetWorkingGroupDocument, GetWorkingGroupsDocument } from '@/working-groups/queries' import { Proposals } from './Proposals' @@ -65,6 +66,24 @@ export default { mocks: ({ args, parameters }: StoryContext): MocksParameters => { const alice = member('alice', { isCouncilMember: args.isCouncilMember }) + const forumWG = { + id: 'forumWorkingGroup', + name: 'forumWorkingGroup', + budget: joy(100), + workers: [{ stake: joy(parameters.wgLeadStake ?? 0) }, { stake: joy(50) }], + leader: parameters.wgLeadStake + ? { + id: 'forumWorkingGroup-10', + runtimeId: '10', + stake: joy(parameters.wgLeadStake), + rewardPerBlock: joy(5), + membershipId: alice.id, + isActive: true, + } + : undefined, + } + const storageWG = { id: 'storageWorkingGroup', name: 'storageWorkingGroup', budget: joy(100), workers: [] } + return { accounts: { active: { member: alice } }, @@ -129,6 +148,19 @@ export default { memberships: [alice], }, }, + + { + query: GetWorkingGroupsDocument, + data: { + workingGroups: [forumWG, storageWG], + }, + }, + { + query: GetWorkingGroupDocument, + data: { + workingGroupByUniqueInput: forumWG, + }, + }, ], } }, @@ -532,3 +564,56 @@ export const SpecificParametersSetReferralCut: Story = { }) }), } + +export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Decrease Working Group Lead Stake', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const body = within(document.body) + + // WGs without a lead are disabled + await userEvent.click(modal.getByPlaceholderText('Select Working Group or type group name')) + const storageWG = body.getByText('Storage') + expect(storageWG.nextElementSibling?.firstElementChild?.textContent).toMatch(/This group has no lead/) + expect(storageWG).toHaveStyle({ 'pointer-events': 'none' }) + + // NOTE: This should be valid but here the button is still disabled + userEvent.click(body.getByText('Forum')) + const stakeMessage = modal.getByText(/The actual stake for Forum Working Group Lead is/) + expect(within(stakeMessage).getByText('1,000')) + + const amountField = modal.getByTestId('amount-input') + + await waitFor(() => expect(amountField).toHaveValue('500')) + + // Invalid: stake set to 0 + await userEvent.clear(amountField) + expect(await modal.findByText('Amount must be greater than zero')) + expect(nextButton).toBeDisabled() + + // Valid 1/3 + userEvent.click(modal.getByText('By 1/3')) + waitFor(() => expect(modal.queryByText('Amount must be greater than zero')).toBeNull()) + expect(amountField).toHaveValue('333.3333333333') + + // Valid 1/2 + userEvent.click(modal.getByText('By half')) + expect(amountField).toHaveValue('500') + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const leaderId = 10 // Set on the mock QN query + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + decreaseWorkingGroupLeadStake: [leaderId, Number(joy(500)), 'Forum'], + }) + }) + }), +} From fa91713c25d0cbca7fcdd88a2403b400a304184e Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Mon, 10 Jul 2023 09:04:13 +0200 Subject: [PATCH 11/32] Add "Terminate Working Group Lead" test --- .../Proposals/CurrentProposals.stories.tsx | 54 ++++++++++++++++++- packages/ui/src/mocks/data/members.ts | 5 +- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index df2787a61d..91e046e260 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -5,7 +5,7 @@ import { userEvent, waitFor, waitForElementToBeRemoved, within } from '@storyboo import { PlayFunction, PlayFunctionContext, StepFunction } from '@storybook/types' import { FC } from 'react' -import { SearchMembersDocument } from '@/memberships/queries' +import { GetMemberDocument, SearchMembersDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { generateProposals, MAX_ACTIVE_PROPOSAL, proposalsPagesChain } from '@/mocks/data/proposals' import { Container, getButtonByText, getEditorByLabel, joy, selectFromDropdown, withinModal } from '@/mocks/helpers' @@ -148,6 +148,12 @@ export default { memberships: [alice], }, }, + { + query: GetMemberDocument, + data: { + membershipByUniqueInput: alice, + }, + }, { query: GetWorkingGroupsDocument, @@ -617,3 +623,49 @@ export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { }) }), } + +export const SpecificParametersTerminateWorkingGroupLead: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Terminate Working Group Lead', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const body = within(document.body) + + // WGs without a lead are disabled + await userEvent.click(modal.getByPlaceholderText('Select Working Group or type group name')) + const storageWG = body.getByText('Storage') + expect(storageWG.nextElementSibling?.firstElementChild?.textContent).toMatch(/This group has no lead/) + expect(storageWG).toHaveStyle({ 'pointer-events': 'none' }) + + // Valid: Don't Slash lead + userEvent.click(body.getByText('Forum')) + expect(await modal.findByText('alice')) + await waitFor(() => expect(nextButton).toBeEnabled()) + + // Valid: Slash the lead 2000 JOY + userEvent.click(modal.getByText('Yes')) + const amountField = modal.getByTestId('amount-input') + expect(amountField).toHaveValue('') + userEvent.type(amountField, '2000') + await waitFor(() => expect(nextButton).toBeDisabled()) + await waitFor(() => expect(nextButton).toBeEnabled()) + + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const leaderId = 10 // Set on the mock QN query + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + terminateWorkingGroupLead: { + workerId: leaderId, + slashingAmount: Number(joy(2000)), + group: 'Forum', + }, + }) + }) + }), +} diff --git a/packages/ui/src/mocks/data/members.ts b/packages/ui/src/mocks/data/members.ts index 1cf2171c0d..1d2fdb0704 100644 --- a/packages/ui/src/mocks/data/members.ts +++ b/packages/ui/src/mocks/data/members.ts @@ -1,11 +1,12 @@ -import { MemberFieldsFragment } from '@/memberships/queries' +import { MemberWithDetailsFieldsFragment } from '@/memberships/queries' import rawMembers from './raw/members.json' -export type Membership = Omit +export type Membership = Omit export const member = (handle: string, { roles = [], ...extra }: Partial = {}) => ({ ...rawMembers.find((member) => member.handle === handle), + invitees: [], ...extra, roles, } as Membership) From b9caea8fd5da93de87aceea1b43f55b2c5ad7d4d Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 11:12:30 +0200 Subject: [PATCH 12/32] Don't initialize number inputs with a 0 --- .../common/components/forms/InputNumber.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/common/components/forms/InputNumber.tsx b/packages/ui/src/common/components/forms/InputNumber.tsx index 1d15175de4..0959ea2501 100644 --- a/packages/ui/src/common/components/forms/InputNumber.tsx +++ b/packages/ui/src/common/components/forms/InputNumber.tsx @@ -4,6 +4,8 @@ import { useFormContext, Controller } from 'react-hook-form' import NumberFormat, { NumberFormatValues, SourceInfo } from 'react-number-format' import styled from 'styled-components' +import { asBN, whenDefined } from '@/common/utils' + import { Input, InputProps } from './InputComponent' interface BaseNumberInputProps extends Omit { @@ -56,16 +58,14 @@ export const InputNumber = React.memo(({ name, isInBN = false, ...props }: Numbe { - return ( - field.onChange(isInBN ? new BN(String(value)) : value)} - onBlur={field.onBlur} - /> - ) - }} + render={({ field }) => ( + field.onChange(isInBN ? new BN(String(value)) : value)} + onBlur={field.onBlur} + /> + )} /> ) }) From b1bd456ae712025d7d1b495aed55b3baaff8e500 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 12:20:00 +0200 Subject: [PATCH 13/32] Add Create Working Group Lead Opening test --- .../Proposals/CurrentProposals.stories.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 91e046e260..5dd0714269 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -1,3 +1,4 @@ +import { OpeningMetadata } from '@joystream/metadata-protobuf' import { linkTo } from '@storybook/addon-links' import { expect, jest } from '@storybook/jest' import { Meta, ReactRenderer, StoryContext, StoryObj } from '@storybook/react' @@ -5,6 +6,7 @@ import { userEvent, waitFor, waitForElementToBeRemoved, within } from '@storyboo import { PlayFunction, PlayFunctionContext, StepFunction } from '@storybook/types' import { FC } from 'react' +import { metadataFromBytes } from '@/common/model/JoystreamNode/metadataFromBytes' import { GetMemberDocument, SearchMembersDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { generateProposals, MAX_ACTIVE_PROPOSAL, proposalsPagesChain } from '@/mocks/data/proposals' @@ -669,3 +671,86 @@ export const SpecificParametersTerminateWorkingGroupLead: Story = { }) }), } + +export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Create Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Next step') + expect(nextButton).toBeDisabled() + + const body = within(document.body) + + // WGs without a lead are enabled + await userEvent.click(modal.getByPlaceholderText('Select Working Group or type group name')) + const storageWG = body.getByText('Storage') + expect(storageWG).not.toHaveStyle({ 'pointer-events': 'none' }) + + // Step 1 valid + await userEvent.click(body.getByText('Forum')) + await userEvent.type(modal.getByLabelText('Opening title'), 'Foo') + await userEvent.type(modal.getByLabelText('Short description'), 'Bar') + ;(await getEditorByLabel(modal, 'Description')).setData('Baz') + expect(nextButton).toBeDisabled() + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + + // Step 2 + expect(nextButton).toBeDisabled() + ;(await getEditorByLabel(modal, 'Application process')).setData('Lorem ipsum...') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(modal.getByText('Limited')) + await waitFor(() => expect(nextButton).toBeDisabled()) + await userEvent.type(modal.getByLabelText('Expected length of the application period'), '1000') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + + // Step 3 + expect(nextButton).toBeDisabled() + await userEvent.type(modal.getByRole('textbox'), 'šŸ?') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(modal.getByText('Add new question')) + await waitFor(() => expect(nextButton).toBeDisabled()) + await userEvent.click(modal.getAllByText('Long answer')[1]) + await userEvent.type(modal.getAllByRole('textbox')[1], 'šŸ˜?') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + + // Step 4 + expect(nextButton).toBeDisabled() + await userEvent.type(modal.getByLabelText('Staking amount *'), '100') + await userEvent.type(modal.getByLabelText('Role cooldown period'), '0') + await userEvent.type(modal.getByLabelText('Reward amount per Block'), '0.1') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + const { description, ...data } = specificParameters.asCreateWorkingGroupLeadOpening.toJSON() + + expect(data).toEqual({ + rewardPerBlock: Number(joy(0.1)), + stakePolicy: { + stakeAmount: Number(joy(100)), + leavingUnstakingPeriod: 0, + }, + group: 'Forum', + }) + + expect(metadataFromBytes(OpeningMetadata, description)).toEqual({ + title: 'Foo', + shortDescription: 'Bar', + description: 'Baz', + hiringLimit: 1, + expectedEndingTimestamp: 1000, + applicationDetails: 'Lorem ipsum...', + applicationFormQuestions: [ + { question: 'šŸ?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXT }, + { question: 'šŸ˜?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA }, + ], + }) + }) + }), +} From 8d4efc40034a619c8e71d3d027e7ccc05287df7f Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 12:49:08 +0200 Subject: [PATCH 14/32] Add the Set Working Group Lead Reward test --- .../Proposals/CurrentProposals.stories.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 5dd0714269..0020dbd89f 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -754,3 +754,51 @@ export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { }) }), } + +export const SpecificParametersSetWorkingGroupLeadReward: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Set Working Group Lead Reward', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const body = within(document.body) + + // WGs without a lead are disabled + await userEvent.click(modal.getByPlaceholderText('Select Working Group or type group name')) + const storageWG = body.getByText('Storage') + expect(storageWG.nextElementSibling?.firstElementChild?.textContent).toMatch(/This group has no lead/) + expect(storageWG).toHaveStyle({ 'pointer-events': 'none' }) + + // Valid + userEvent.click(body.getByText('Forum')) + expect(await modal.findByText('alice')) + const stakeMessage = modal.getByText(/Current reward per block for Forum Working Group Lead is/) + expect(within(stakeMessage).getByText('5')) + expect(nextButton).toBeDisabled() + const amountField = modal.getByTestId('amount-input') + await userEvent.type(amountField, '1') + await waitFor(() => expect(nextButton).toBeEnabled()) + + // Invalid + await userEvent.clear(amountField) + await userEvent.type(amountField, '0') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // Valid again + await userEvent.clear(amountField) + await userEvent.type(amountField, '10') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const leaderId = 10 // Set on the mock QN query + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + setWorkingGroupLeadReward: [leaderId, Number(joy(10)), 'Forum'], + }) + }) + }), +} From a1d54d88a706adfcc3ce50ba0e13c902e503265e Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 13:11:35 +0200 Subject: [PATCH 15/32] Add the Set Max Validator Count test --- .../Proposals/CurrentProposals.stories.tsx | 42 +++++++++++++++++++ packages/ui/src/mocks/data/proposals.ts | 8 +++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 0020dbd89f..a0b11d7c55 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -92,6 +92,8 @@ export default { chain: proposalsPagesChain( { activeProposalCount: args.proposalCount, + minimumValidatorCount: parameters.minimumValidatorCount, + setMaxValidatorCountProposalMaxValidators: parameters.setMaxValidatorCountProposalMaxValidators, onCreateProposal: args.onCreateProposal, onThreadChangeThreadMode: args.onThreadChangeThreadMode, onAddStakingAccountCandidate: args.onAddStakingAccountCandidate, @@ -802,3 +804,43 @@ export const SpecificParametersSetWorkingGroupLeadReward: Story = { }) }), } + +export const SpecificParametersSetMaxValidatorCount: Story = { + parameters: { + ...hasStakingAccountParameters, + minimumValidatorCount: 4, + setMaxValidatorCountProposalMaxValidators: 100, + }, + + play: specificParametersTest('Set Max Validator Count', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Invalid: too low + await userEvent.type(amountField, '1') + const validation = await modal.findByText('Minimal amount allowed is 4') + expect(validation) + expect(nextButton).toBeDisabled() + + // Invalid: too high + await userEvent.type(amountField, '999') + // console.log(validation) + await waitFor(() => expect(validation).toHaveTextContent('Maximal amount allowed is 100')) + expect(nextButton).toBeDisabled() + + // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '10') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setMaxValidatorCount: 10 }) + }) + }), +} diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 8935ac3633..e70fe1bddb 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -134,6 +134,8 @@ export const generateProposals = ( type ProposalChainProps = { activeProposalCount: number + minimumValidatorCount?: number + setMaxValidatorCountProposalMaxValidators?: number onCreateProposal?: jest.Mock onThreadChangeThreadMode?: jest.Mock onAddStakingAccountCandidate?: jest.Mock @@ -143,6 +145,8 @@ type Chain = MocksParameters['chain'] export const proposalsPagesChain = ( { activeProposalCount, + minimumValidatorCount = 4, + setMaxValidatorCountProposalMaxValidators = 100, onCreateProposal, onThreadChangeThreadMode, onAddStakingAccountCandidate, @@ -170,7 +174,7 @@ export const proposalsPagesChain = ( proposalsCodex: { fundingRequestProposalMaxTotalAmount: joy(166_666), - setMaxValidatorCountProposalMaxValidators: 100, + setMaxValidatorCountProposalMaxValidators, ...Object.fromEntries( proposalTypes.map((type) => [ @@ -201,7 +205,7 @@ export const proposalsPagesChain = ( }, proposalsEngine: { activeProposalCount }, - staking: { minimumValidatorCount: 4 }, + staking: { minimumValidatorCount }, }, tx: { From def992136f22c8d49a0fb00429402027885a4f80 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 14:04:39 +0200 Subject: [PATCH 16/32] Add the Cancel Working Group Lead Opening test --- .../Proposals/CurrentProposals.stories.tsx | 82 ++++++++++++++++++- packages/ui/src/proposals/queries/index.ts | 1 + 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index a0b11d7c55..37a642f652 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -10,10 +10,27 @@ import { metadataFromBytes } from '@/common/model/JoystreamNode/metadataFromByte import { GetMemberDocument, SearchMembersDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { generateProposals, MAX_ACTIVE_PROPOSAL, proposalsPagesChain } from '@/mocks/data/proposals' -import { Container, getButtonByText, getEditorByLabel, joy, selectFromDropdown, withinModal } from '@/mocks/helpers' +import { + Container, + getButtonByText, + getEditorByLabel, + isoDate, + joy, + selectFromDropdown, + withinModal, +} from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' -import { GetProposalVotesDocument, GetProposalsCountDocument, GetProposalsDocument } from '@/proposals/queries' -import { GetWorkingGroupDocument, GetWorkingGroupsDocument } from '@/working-groups/queries' +import { + GetProposalsEventsDocument, + GetProposalVotesDocument, + GetProposalsCountDocument, + GetProposalsDocument, +} from '@/proposals/queries' +import { + GetWorkingGroupDocument, + GetWorkingGroupOpeningsDocument, + GetWorkingGroupsDocument, +} from '@/working-groups/queries' import { Proposals } from './Proposals' @@ -146,6 +163,11 @@ export default { }, }, + { + query: GetProposalsEventsDocument, + data: { events: [] }, + }, + { query: SearchMembersDocument, data: { @@ -171,6 +193,36 @@ export default { workingGroupByUniqueInput: forumWG, }, }, + { + query: GetWorkingGroupOpeningsDocument, + data: { + workingGroupOpenings: [ + { + id: 'storageWorkingGroup-12', + runtimeId: 12, + groupId: 'storageWorkingGroup', + group: { + name: 'storageWorkingGroup', + budget: '962651993476422', + leaderId: 'storageWorkingGroup-0', + }, + type: 'LEADER', + stakeAmount: '2500000000000000', + rewardPerBlock: '1930000000', + createdInEvent: { inBlock: 123, createdAt: isoDate('2023/01/02') }, + metadata: { + title: 'Hire Storage Working Group Lead', + applicationDetails: 'answers to questions', + shortDescription: 'Hire Storage Working Group Lead', + description: 'Lorem ipsum...', + hiringLimit: 1, + expectedEnding: null, + }, + status: { __typename: 'OpeningStatusOpen' }, + }, + ], + }, + }, ], } }, @@ -844,3 +896,27 @@ export const SpecificParametersSetMaxValidatorCount: Story = { }) }), } + +export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Cancel Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const body = within(document.body) + + // Valid + await userEvent.click(modal.getByPlaceholderText('Choose opening to cancel')) + userEvent.click(body.getByText('Hire Storage Working Group Lead')) + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ cancelWorkingGroupLeadOpening: [12, 'Storage'] }) + }) + }), +} diff --git a/packages/ui/src/proposals/queries/index.ts b/packages/ui/src/proposals/queries/index.ts index ce2fb70764..05623bbbd2 100644 --- a/packages/ui/src/proposals/queries/index.ts +++ b/packages/ui/src/proposals/queries/index.ts @@ -1 +1,2 @@ export * from './__generated__/proposals.generated' +export * from './__generated__/proposalsEvents.generated' From 3f7e9a36903809e62bf029a578e384941d8a7939 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 15:04:01 +0200 Subject: [PATCH 17/32] Add the Set Council Budget Increment test --- .../Proposals/CurrentProposals.stories.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 37a642f652..0ea22f342b 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -920,3 +920,40 @@ export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { }) }), } + +export const SpecificParametersSetCouncilBudgetIncrement: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Set Council Budget Increment', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Invalid budget 0 + await userEvent.type(amountField, '1') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.clear(amountField) + await userEvent.type(amountField, '0') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // The value remains less than 2^128 + await userEvent.clear(amountField) + await userEvent.type(amountField, ''.padEnd(39, '9')) + const value = Number((amountField as HTMLInputElement).value.replace(/,/g, '')) + expect(value).toBeLessThan(2 ** 128) + + // // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '500') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setCouncilBudgetIncrement: Number(joy(500)) }) + }) + }), +} From dea440ebd09c76ddca3bf751f53cdb14740d8e64 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 15:08:38 +0200 Subject: [PATCH 18/32] Add the Set Councilor Reward test --- .../Proposals/CurrentProposals.stories.tsx | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 0ea22f342b..f070133935 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -944,7 +944,7 @@ export const SpecificParametersSetCouncilBudgetIncrement: Story = { const value = Number((amountField as HTMLInputElement).value.replace(/,/g, '')) expect(value).toBeLessThan(2 ** 128) - // // Valid + // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '500') await waitFor(() => expect(nextButton).toBeEnabled()) @@ -957,3 +957,34 @@ export const SpecificParametersSetCouncilBudgetIncrement: Story = { }) }), } + +export const SpecificParametersSetCouncilorReward: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest('Set Councilor Reward', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Invalid budget 0 + await userEvent.type(amountField, '1') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.clear(amountField) + await userEvent.type(amountField, '0') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '10') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setCouncilorReward: Number(joy(10)) }) + }) + }), +} From fffef09b37101e7fd5113f2b024183c15b77d36c Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 15:38:02 +0200 Subject: [PATCH 19/32] Add the Set Membership Lead Invitation Quota test --- .../Proposals/CurrentProposals.stories.tsx | 40 +++++++++++++++++++ .../modals/AddNewProposal/helpers.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index f070133935..dfff0a380b 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -988,3 +988,43 @@ export const SpecificParametersSetCouncilorReward: Story = { }) }), } + +export const SpecificParametersSetMembershipLeadInvitationQuota: Story = { + parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + + play: specificParametersTest( + 'Set Membership Lead Invitation Quota', + async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Invalid budget 0 + await userEvent.type(amountField, '1') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.clear(amountField) + await userEvent.type(amountField, '0') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // The value remains less than 2^32 + await userEvent.clear(amountField) + await userEvent.type(amountField, ''.padEnd(39, '9')) + const value = Number((amountField as HTMLInputElement).value.replace(/,/g, '')) + expect(value).toBeLessThan(2 ** 32) + + // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '3') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setMembershipLeadInvitationQuota: 3 }) + }) + } + ), +} diff --git a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts index 41ee89adb5..4b9e16d647 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts +++ b/packages/ui/src/proposals/modals/AddNewProposal/helpers.ts @@ -321,7 +321,7 @@ export const schemaFactory = (api?: Api) => { .required('Field is required'), }), setMembershipLeadInvitationQuota: Yup.object().shape({ - count: BNSchema.test(moreThanMixed(0, 'Quota must be greater than zero')).required('Field is required'), + count: Yup.number().min(1, 'Quota must be greater than zero').required('Field is required'), leadId: Yup.string().test('execution', (value) => !!value), }), setInitialInvitationBalance: Yup.object().shape({ From 030da88f7dd796db41e47c571c238da2195ec5a6 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 16:22:48 +0200 Subject: [PATCH 20/32] Add the Fill Working Group Lead Opening test --- .../Proposals/CurrentProposals.stories.tsx | 118 ++++++++++++++---- 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index dfff0a380b..694e86ff2a 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -27,6 +27,7 @@ import { GetProposalsDocument, } from '@/proposals/queries' import { + GetWorkingGroupApplicationsDocument, GetWorkingGroupDocument, GetWorkingGroupOpeningsDocument, GetWorkingGroupsDocument, @@ -39,6 +40,30 @@ const PROPOSAL_DATA = { description: '## est minus rerum sed\n\nAssumenda et laboriosam minus accusantium. Sed in quo illum.', } +const OPENING_DATA = { + id: 'storageWorkingGroup-12', + runtimeId: 12, + groupId: 'storageWorkingGroup', + group: { + name: 'storageWorkingGroup', + budget: '962651993476422', + leaderId: 'storageWorkingGroup-0', + }, + type: 'LEADER', + stakeAmount: '2500000000000000', + rewardPerBlock: '1930000000', + createdInEvent: { inBlock: 123, createdAt: isoDate('2023/01/02') }, + metadata: { + title: 'Hire Storage Working Group Lead', + applicationDetails: 'answers to questions', + shortDescription: 'Hire Storage Working Group Lead', + description: 'Lorem ipsum...', + hiringLimit: 1, + expectedEnding: null, + }, + status: { __typename: 'OpeningStatusOpen' }, +} + type Args = { isCouncilMember: boolean proposalCount: number @@ -196,29 +221,24 @@ export default { { query: GetWorkingGroupOpeningsDocument, data: { - workingGroupOpenings: [ + workingGroupOpenings: [OPENING_DATA], + }, + }, + { + query: GetWorkingGroupApplicationsDocument, + data: { + workingGroupApplications: [ { - id: 'storageWorkingGroup-12', - runtimeId: 12, - groupId: 'storageWorkingGroup', - group: { - name: 'storageWorkingGroup', - budget: '962651993476422', - leaderId: 'storageWorkingGroup-0', - }, - type: 'LEADER', - stakeAmount: '2500000000000000', - rewardPerBlock: '1930000000', - createdInEvent: { inBlock: 123, createdAt: isoDate('2023/01/02') }, - metadata: { - title: 'Hire Storage Working Group Lead', - applicationDetails: 'answers to questions', - shortDescription: 'Hire Storage Working Group Lead', - description: 'Lorem ipsum...', - hiringLimit: 1, - expectedEnding: null, - }, - status: { __typename: 'OpeningStatusOpen' }, + id: 'storageWorkingGroup-15', + runtimeId: 15, + opening: OPENING_DATA, + answers: [ + { answer: 'Foo', question: { question: 'šŸ?' } }, + { answer: 'Bar', question: { question: 'šŸ˜?' } }, + ], + status: { __typename: 'ApplicationStatusPending' }, + applicant: alice, + createdInEvent: { inBlock: 234, createdAt: isoDate('2023/01/04') }, }, ], }, @@ -898,7 +918,7 @@ export const SpecificParametersSetMaxValidatorCount: Story = { } export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: hasStakingAccountParameters, play: specificParametersTest('Cancel Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -922,7 +942,7 @@ export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { } export const SpecificParametersSetCouncilBudgetIncrement: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: hasStakingAccountParameters, play: specificParametersTest('Set Council Budget Increment', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -959,7 +979,7 @@ export const SpecificParametersSetCouncilBudgetIncrement: Story = { } export const SpecificParametersSetCouncilorReward: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: hasStakingAccountParameters, play: specificParametersTest('Set Councilor Reward', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -1028,3 +1048,51 @@ export const SpecificParametersSetMembershipLeadInvitationQuota: Story = { } ), } + +export const SpecificParametersFillWorkingGroupLeadOpening: Story = { + parameters: hasStakingAccountParameters, + + play: specificParametersTest('Fill Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const body = within(document.body) + + // Select Opening + await userEvent.click(modal.getByPlaceholderText('Choose opening to fill')) + userEvent.click(body.getByText('Hire Storage Working Group Lead')) + + // Select Application + const applicationSelector = await modal.findByPlaceholderText('Choose application') + const options = await waitFor(async () => { + await userEvent.click(applicationSelector) + const options = document.getElementById('select-popper-wrapper') + expect(options).not.toBeNull() + return within(options as HTMLElement) + }) + expect(nextButton).toBeDisabled() + userEvent.click(options.getByText('alice')) + + // Check application + expect(await modal.findByText('šŸ?')) + expect(modal.getByText('Foo')) + expect(modal.getByText('šŸ˜?')) + expect(modal.getByText('Bar')) + + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + fillWorkingGroupLeadOpening: { + applicationId: 15, + openingId: 12, + workingGroup: 'Storage', + }, + }) + }) + }), +} From 93f33b6eed71362961b78764cc5e9c9456e1c509 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 16:35:31 +0200 Subject: [PATCH 21/32] Add the Set Initial Invitation Count test --- .../Proposals/CurrentProposals.stories.tsx | 40 +++++++++++++++++++ packages/ui/src/mocks/data/proposals.ts | 3 ++ 2 files changed, 43 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 694e86ff2a..e72779fe90 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -136,6 +136,7 @@ export default { activeProposalCount: args.proposalCount, minimumValidatorCount: parameters.minimumValidatorCount, setMaxValidatorCountProposalMaxValidators: parameters.setMaxValidatorCountProposalMaxValidators, + initialInvitationCount: parameters.initialInvitationCount, onCreateProposal: args.onCreateProposal, onThreadChangeThreadMode: args.onThreadChangeThreadMode, onAddStakingAccountCandidate: args.onAddStakingAccountCandidate, @@ -1096,3 +1097,42 @@ export const SpecificParametersFillWorkingGroupLeadOpening: Story = { }) }), } + +export const SpecificParametersSetInitialInvitationCount: Story = { + parameters: { ...hasStakingAccountParameters, initialInvitationCount: 5 }, + + play: specificParametersTest('Set Initial Invitation Count', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + expect(modal.getByText('The current initial invitation count is 5.')) + + const countField = modal.getByLabelText('New Count') + + // Invalid 0 invitation + await userEvent.type(countField, '1') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.clear(countField) + await userEvent.type(countField, '0') + await waitFor(() => expect(nextButton).toBeDisabled()) + + // The value remains less than 2^32 + await userEvent.clear(countField) + await userEvent.type(countField, ''.padEnd(39, '9')) + const value = Number((countField as HTMLInputElement).value.replace(/,/g, '')) + expect(value).toBeLessThan(2 ** 32) + + // Valid + await userEvent.clear(countField) + await userEvent.type(countField, '7') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setInitialInvitationCount: 7 }) + }) + }), +} diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index e70fe1bddb..1c896ec531 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -136,6 +136,7 @@ type ProposalChainProps = { activeProposalCount: number minimumValidatorCount?: number setMaxValidatorCountProposalMaxValidators?: number + initialInvitationCount?: number onCreateProposal?: jest.Mock onThreadChangeThreadMode?: jest.Mock onAddStakingAccountCandidate?: jest.Mock @@ -147,6 +148,7 @@ export const proposalsPagesChain = ( activeProposalCount, minimumValidatorCount = 4, setMaxValidatorCountProposalMaxValidators = 100, + initialInvitationCount = 5, onCreateProposal, onThreadChangeThreadMode, onAddStakingAccountCandidate, @@ -196,6 +198,7 @@ export const proposalsPagesChain = ( query: { members: { + initialInvitationCount, membershipPrice: joy(20), stakingAccountIdMemberStatus: { memberId: 0, From da8413b241e5bcf4959612392080f8bc192b7412 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 17:05:33 +0200 Subject: [PATCH 22/32] Add the Set Initial Invitation Balance test --- .../Proposals/CurrentProposals.stories.tsx | 41 ++++++++++++++++--- packages/ui/src/mocks/data/proposals.ts | 3 ++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index e72779fe90..0a9947fdc7 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -137,6 +137,7 @@ export default { minimumValidatorCount: parameters.minimumValidatorCount, setMaxValidatorCountProposalMaxValidators: parameters.setMaxValidatorCountProposalMaxValidators, initialInvitationCount: parameters.initialInvitationCount, + initialInvitationBalance: parameters.initialInvitationBalance, onCreateProposal: args.onCreateProposal, onThreadChangeThreadMode: args.onThreadChangeThreadMode, onAddStakingAccountCandidate: args.onAddStakingAccountCandidate, @@ -1110,12 +1111,10 @@ export const SpecificParametersSetInitialInvitationCount: Story = { const countField = modal.getByLabelText('New Count') - // Invalid 0 invitation - await userEvent.type(countField, '1') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.clear(countField) + // Invalid 0 invitations await userEvent.type(countField, '0') - await waitFor(() => expect(nextButton).toBeDisabled()) + expect(await modal.findByText('Amount must be greater than zero')) + expect(nextButton).toBeDisabled() // The value remains less than 2^32 await userEvent.clear(countField) @@ -1136,3 +1135,35 @@ export const SpecificParametersSetInitialInvitationCount: Story = { }) }), } + +export const SpecificParametersSetInitialInvitationBalance: Story = { + parameters: { ...hasStakingAccountParameters, initialInvitationBalance: joy(5) }, + + play: specificParametersTest('Set Initial Invitation Balance', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const current = modal.getByText(/The current balance is/) + expect(within(current).getByText('5')) + + const amountField = modal.getByTestId('amount-input') + + // Invalid balance 0 + await userEvent.type(amountField, '0') + expect(await modal.findByText('Amount must be greater than zero')) + expect(nextButton).toBeDisabled() + + // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '7') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setInitialInvitationBalance: Number(joy(7)) }) + }) + }), +} diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 1c896ec531..fc4e825fee 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -137,6 +137,7 @@ type ProposalChainProps = { minimumValidatorCount?: number setMaxValidatorCountProposalMaxValidators?: number initialInvitationCount?: number + initialInvitationBalance?: string onCreateProposal?: jest.Mock onThreadChangeThreadMode?: jest.Mock onAddStakingAccountCandidate?: jest.Mock @@ -149,6 +150,7 @@ export const proposalsPagesChain = ( minimumValidatorCount = 4, setMaxValidatorCountProposalMaxValidators = 100, initialInvitationCount = 5, + initialInvitationBalance = joy(5), onCreateProposal, onThreadChangeThreadMode, onAddStakingAccountCandidate, @@ -199,6 +201,7 @@ export const proposalsPagesChain = ( query: { members: { initialInvitationCount, + initialInvitationBalance, membershipPrice: joy(20), stakingAccountIdMemberStatus: { memberId: 0, From 31a38bea4178f8e288e04dabf4b38a7a8cc5bf91 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 17:11:10 +0200 Subject: [PATCH 23/32] Add the Set Membership Price test --- .../Proposals/CurrentProposals.stories.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 0a9947fdc7..2382f9f8c6 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -1167,3 +1167,32 @@ export const SpecificParametersSetInitialInvitationBalance: Story = { }) }), } + +export const SpecificParametersSetMembershipPrice: Story = { + parameters: hasStakingAccountParameters, + + play: specificParametersTest('Set Membership Price', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + // Invalid price set to 0 + await userEvent.type(amountField, '0') + expect(await modal.findByText('Amount must be greater than zero')) + expect(nextButton).toBeDisabled() + + // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '8') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setMembershipPrice: Number(joy(8)) }) + }) + }), +} From 86a789601b5bf00ef3e523b92b4ac63c68186e88 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 17:58:49 +0200 Subject: [PATCH 24/32] Add the Update Working Group Budget test --- .../Proposals/CurrentProposals.stories.tsx | 70 +++++++++++++++++++ packages/ui/src/mocks/data/proposals.ts | 22 ++++++ 2 files changed, 92 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 2382f9f8c6..4254b90f0d 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -138,6 +138,12 @@ export default { setMaxValidatorCountProposalMaxValidators: parameters.setMaxValidatorCountProposalMaxValidators, initialInvitationCount: parameters.initialInvitationCount, initialInvitationBalance: parameters.initialInvitationBalance, + + councilSize: parameters.councilSize, + councilBudget: parameters.councilBudget, + councilorReward: parameters.councilorReward, + nextRewardPayments: parameters.nextRewardPayments, + onCreateProposal: args.onCreateProposal, onThreadChangeThreadMode: args.onThreadChangeThreadMode, onAddStakingAccountCandidate: args.onAddStakingAccountCandidate, @@ -1196,3 +1202,67 @@ export const SpecificParametersSetMembershipPrice: Story = { }) }), } + +export const SpecificParametersUpdateWorkingGroupBudget: Story = { + parameters: { + ...hasStakingAccountParameters, + councilSize: 3, + councilBudget: joy(2000), + councilorReward: joy(100), + nextRewardPayments: 12345, + }, + + play: specificParametersTest('Update Working Group Budget', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const amountField = modal.getByTestId('amount-input') + + const currentCouncilBudget = modal.getByText(/Current budget for Council is/) + expect(within(currentCouncilBudget).getByText('2,000')) + + const councilSummary = modal.getByText(/Next Council payment is in/) + expect(within(councilSummary).getByText('12,345')) // Next reward payment block + expect(within(councilSummary).getByText(100 * 3)) // Next reward payment block + + expect( + modal.getByText( + 'If the Councils budget is less than provided amount at attempted execution, this proposal will fail to execute, and the budget size will not be changed.' + ) + ) + + userEvent.click(modal.getByText('Yes')) + expect( + modal.getByText( + 'If the budget is less than provided amount at attempted execution, this proposal will fail to execute and the budget size will not be changed' + ) + ) + + // Select working group + await userEvent.click(modal.getByPlaceholderText('Select Working Group or type group name')) + userEvent.click(within(document.body).getByText('Forum')) + + const currentWgBudget = modal.getByText(/Current budget for Forum Working Group is/) + expect(within(currentWgBudget).getByText('100')) + + // Invalid price set to 0 + await userEvent.type(amountField, '0') + expect(await modal.findByText('Amount must be greater than zero')) + expect(nextButton).toBeDisabled() + + // Valid + await userEvent.clear(amountField) + await userEvent.type(amountField, '99') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + updateWorkingGroupBudget: [Number(joy(99)), 'Forum', 'Negative'], + }) + }) + }), +} diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index fc4e825fee..25d2e03779 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -138,6 +138,12 @@ type ProposalChainProps = { setMaxValidatorCountProposalMaxValidators?: number initialInvitationCount?: number initialInvitationBalance?: string + + councilSize?: number + councilBudget?: string + councilorReward?: string + nextRewardPayments?: number + onCreateProposal?: jest.Mock onThreadChangeThreadMode?: jest.Mock onAddStakingAccountCandidate?: jest.Mock @@ -151,6 +157,12 @@ export const proposalsPagesChain = ( setMaxValidatorCountProposalMaxValidators = 100, initialInvitationCount = 5, initialInvitationBalance = joy(5), + + councilSize = 3, + councilBudget = joy(2000), + councilorReward = joy(200), + nextRewardPayments = 12345, + onCreateProposal, onThreadChangeThreadMode, onAddStakingAccountCandidate, @@ -166,6 +178,8 @@ export const proposalsPagesChain = ( maximumCashoutAllowedLimit: joy(1_666_666), }, + council: { councilSize, idlePeriodDuration: 1, announcingPeriodDuration: 1 }, + members: { referralCutMaximumPercent: 50, }, @@ -199,6 +213,14 @@ export const proposalsPagesChain = ( }, query: { + council: { + budget: councilBudget, + councilorReward, + nextRewardPayments, + stage: { stage: { isIdle: true }, changedAt: 123 }, + }, + referendum: { stage: {} }, + members: { initialInvitationCount, initialInvitationBalance, From 435d0cfa2d46e092f614883ecd2085ffa45227b8 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 19:05:42 +0200 Subject: [PATCH 25/32] Add the Runtime Upgrade test --- .../Proposals/CurrentProposals.stories.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 4254b90f0d..864f7f47b5 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -1266,3 +1266,43 @@ export const SpecificParametersUpdateWorkingGroupBudget: Story = { }) }), } + +export const SpecificParametersRuntimeUpgrade: Story = { + parameters: hasStakingAccountParameters, + + play: specificParametersTest('Runtime Upgrade', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + const uploadField = modal.getByTestId('runtime-upgrade-input') + + // Invalid + await userEvent.upload(uploadField, new File([], 'invalid.wasm', { type: 'application/wasm' })) + const validation = await modal.findByText(/was not loaded because of: "not valid WASM file"./) + expect(within(validation).getByText('invalid.wasm')) + + // Valid + const setIsValidWASM = jest.fn() + const validFile = Object.defineProperties(new File([], 'valid.wasm', { type: 'application/wasm' }), { + isValidWASM: { get: () => true, set: setIsValidWASM }, + arrayBuffer: { value: () => Promise.resolve(new ArrayBuffer(1)) }, + size: { value: 1 }, + }) + await userEvent.upload(uploadField, validFile) + await waitFor(() => expect(setIsValidWASM).toHaveBeenCalledWith(false)) + const confirmation = await modal.findByText(/was loaded successfully!/) + expect(within(confirmation).getByText('valid.wasm')) + + await waitFor(() => expect(nextButton).toBeEnabled()) + await waitFor(() => expect(nextButton).toBeDisabled()) // The button gets enabled 1 rendering frame due to isLoading lagging behind + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ runtimeUpgrade: '0x' }) + }) + }), +} From 6799ca9117eeceda3ab2bc41d60e486b27992440 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 22:08:27 +0200 Subject: [PATCH 26/32] Complete the AddNewProposalHappy test --- .../Proposals/CurrentProposals.stories.tsx | 89 ++++++++----------- 1 file changed, 39 insertions(+), 50 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 864f7f47b5..3c89809c0b 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -69,8 +69,8 @@ type Args = { proposalCount: number onCreateProposal: jest.Mock onThreadChangeThreadMode: jest.Mock - onConfirmStakingAccount: jest.Mock onAddStakingAccountCandidate: jest.Mock + onConfirmStakingAccount: jest.Mock onVote: jest.Mock } type Story = StoryObj> @@ -83,8 +83,8 @@ export default { proposalCount: { control: { type: 'range', max: MAX_ACTIVE_PROPOSAL } }, onCreateProposal: { action: 'ProposalsCodex.ProposalCreated' }, onThreadChangeThreadMode: { action: 'proposalsDiscussion.ThreadModeChanged' }, - onConfirmStakingAccount: { action: 'Members.StakingAccountConfirmed' }, onAddStakingAccountCandidate: { action: 'Members.StakingAccountAdded' }, + onConfirmStakingAccount: { action: 'Members.StakingAccountConfirmed' }, onVote: { action: 'ProposalsEngine.Voted' }, }, @@ -101,10 +101,12 @@ export default { }, }, + isLoggedIn: true, + stakingAccountIdMemberStatus: { memberId: 0, - confirmed: false, - size: 0, + confirmed: true, + size: 1, }, mocks: ({ args, parameters }: StoryContext): MocksParameters => { @@ -129,7 +131,7 @@ export default { const storageWG = { id: 'storageWorkingGroup', name: 'storageWorkingGroup', budget: joy(100), workers: [] } return { - accounts: { active: { member: alice } }, + accounts: parameters.isLoggedIn ? { active: { member: alice } } : { list: [{ member: alice }] }, chain: proposalsPagesChain( { @@ -266,16 +268,16 @@ export const Default: Story = {} const alice = member('alice') const waitForModal = (modal: Container, name: string) => modal.findByRole('heading', { name }) -const hasStakingAccountParameters = { - stakingAccountIdMemberStatus: { - memberId: alice.id, - confirmed: true, - size: 1, - }, -} - export const AddNewProposalHappy: Story = { - parameters: hasStakingAccountParameters, + parameters: { + isLoggedIn: false, + + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: false, + size: 0, + }, + }, play: async ({ args, canvasElement, step }) => { const screen = within(canvasElement) @@ -287,11 +289,16 @@ export const AddNewProposalHappy: Story = { await userEvent.click(getButtonByText(modal, 'Close')) } + await step('Select Membership Modal', async () => { + await userEvent.click(screen.getByText('Add new proposal')) + expect(modal.getByText('Select Membership')) + await userEvent.click(modal.getByText('alice')) + }) + await step('Warning Modal', async () => { const createProposalButton = getButtonByText(screen, 'Add new proposal') await step('Temporarily close ', async () => { - await userEvent.click(createProposalButton) await waitForModal(modal, 'Caution') const nextButton = getButtonByText(modal, 'Create A Proposal') @@ -336,8 +343,6 @@ export const AddNewProposalHappy: Story = { await waitForModal(modal, 'Creating new proposal') nextButton = getButtonByText(modal, 'Next step') - // TODO test steps - expect(nextButton).toBeDisabled() await userEvent.click(modal.getByText('Signal')) await waitFor(() => expect(nextButton).not.toBeDisabled()) @@ -438,8 +443,14 @@ export const AddNewProposalHappy: Story = { }) }) + await step('Bind Staking Account', async () => { + expect(modal.getByText('You intend to bind account for staking')) + expect(modal.getAllByText('alice')).toHaveLength(2) + await userEvent.click(modal.getByText('Sign transaction and Bind Staking Account')) + }) + await step('Sign Create Proposal transaction', async () => { - expect(modal.getByText('You intend to create a proposal.')) + expect(await modal.findByText('You intend to create a proposal.')) await userEvent.click(modal.getByText('Sign transaction and Create')) }) @@ -450,6 +461,8 @@ export const AddNewProposalHappy: Story = { }) step('Transaction parameters', () => { + expect(args.onAddStakingAccountCandidate).toHaveBeenCalledWith(alice.id) + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(alice.id, alice.controllerAccount) const [generalParameters] = args.onCreateProposal.mock.calls.at(-1) @@ -473,7 +486,6 @@ export const AddNewProposalHappy: Story = { } // TODO: -// - No active member // - Not enough funds // ---------------------------------------------------------------------------- @@ -554,8 +566,6 @@ const specificParametersTest = // ---------------------------------------------------------------------------- export const SpecificParametersSignal: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Signal', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -584,8 +594,6 @@ export const SpecificParametersSignal: Story = { } export const SpecificParametersFundingRequest: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Funding Request', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -619,8 +627,6 @@ export const SpecificParametersFundingRequest: Story = { } export const SpecificParametersSetReferralCut: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Set Referral Cut', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -656,7 +662,7 @@ export const SpecificParametersSetReferralCut: Story = { } export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: { wgLeadStake: 1000 }, play: specificParametersTest('Decrease Working Group Lead Stake', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -709,7 +715,7 @@ export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { } export const SpecificParametersTerminateWorkingGroupLead: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: { wgLeadStake: 1000 }, play: specificParametersTest('Terminate Working Group Lead', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -755,7 +761,7 @@ export const SpecificParametersTerminateWorkingGroupLead: Story = { } export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: { wgLeadStake: 1000 }, play: specificParametersTest('Create Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -838,7 +844,7 @@ export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { } export const SpecificParametersSetWorkingGroupLeadReward: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: { wgLeadStake: 1000 }, play: specificParametersTest('Set Working Group Lead Reward', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -886,11 +892,7 @@ export const SpecificParametersSetWorkingGroupLeadReward: Story = { } export const SpecificParametersSetMaxValidatorCount: Story = { - parameters: { - ...hasStakingAccountParameters, - minimumValidatorCount: 4, - setMaxValidatorCountProposalMaxValidators: 100, - }, + parameters: { minimumValidatorCount: 4, setMaxValidatorCountProposalMaxValidators: 100 }, play: specificParametersTest('Set Max Validator Count', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -926,8 +928,6 @@ export const SpecificParametersSetMaxValidatorCount: Story = { } export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Cancel Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -950,8 +950,6 @@ export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { } export const SpecificParametersSetCouncilBudgetIncrement: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Set Council Budget Increment', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -987,8 +985,6 @@ export const SpecificParametersSetCouncilBudgetIncrement: Story = { } export const SpecificParametersSetCouncilorReward: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Set Councilor Reward', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -1018,7 +1014,7 @@ export const SpecificParametersSetCouncilorReward: Story = { } export const SpecificParametersSetMembershipLeadInvitationQuota: Story = { - parameters: { ...hasStakingAccountParameters, wgLeadStake: 1000 }, + parameters: { wgLeadStake: 1000 }, play: specificParametersTest( 'Set Membership Lead Invitation Quota', @@ -1058,8 +1054,6 @@ export const SpecificParametersSetMembershipLeadInvitationQuota: Story = { } export const SpecificParametersFillWorkingGroupLeadOpening: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Fill Working Group Lead Opening', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -1106,7 +1100,7 @@ export const SpecificParametersFillWorkingGroupLeadOpening: Story = { } export const SpecificParametersSetInitialInvitationCount: Story = { - parameters: { ...hasStakingAccountParameters, initialInvitationCount: 5 }, + parameters: { initialInvitationCount: 5 }, play: specificParametersTest('Set Initial Invitation Count', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -1143,7 +1137,7 @@ export const SpecificParametersSetInitialInvitationCount: Story = { } export const SpecificParametersSetInitialInvitationBalance: Story = { - parameters: { ...hasStakingAccountParameters, initialInvitationBalance: joy(5) }, + parameters: { initialInvitationBalance: joy(5) }, play: specificParametersTest('Set Initial Invitation Balance', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { @@ -1175,8 +1169,6 @@ export const SpecificParametersSetInitialInvitationBalance: Story = { } export const SpecificParametersSetMembershipPrice: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Set Membership Price', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') @@ -1205,7 +1197,6 @@ export const SpecificParametersSetMembershipPrice: Story = { export const SpecificParametersUpdateWorkingGroupBudget: Story = { parameters: { - ...hasStakingAccountParameters, councilSize: 3, councilBudget: joy(2000), councilorReward: joy(100), @@ -1268,8 +1259,6 @@ export const SpecificParametersUpdateWorkingGroupBudget: Story = { } export const SpecificParametersRuntimeUpgrade: Story = { - parameters: hasStakingAccountParameters, - play: specificParametersTest('Runtime Upgrade', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { const nextButton = getButtonByText(modal, 'Create proposal') From 811d884f11690d9d66683130833df7f397a56000 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 12 Jul 2023 22:49:23 +0200 Subject: [PATCH 27/32] Test not enough funds case --- .../Proposals/CurrentProposals.stories.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 3c89809c0b..039086c10d 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -102,6 +102,7 @@ export default { }, isLoggedIn: true, + balance: 100, stakingAccountIdMemberStatus: { memberId: 0, @@ -131,7 +132,9 @@ export default { const storageWG = { id: 'storageWorkingGroup', name: 'storageWorkingGroup', budget: joy(100), workers: [] } return { - accounts: parameters.isLoggedIn ? { active: { member: alice } } : { list: [{ member: alice }] }, + accounts: parameters.isLoggedIn + ? { active: { member: alice, balances: parameters.balance } } + : { list: [{ member: alice }] }, chain: proposalsPagesChain( { @@ -485,8 +488,28 @@ export const AddNewProposalHappy: Story = { }, } +export const NotEnoughFunds: Story = { + parameters: { balance: 1 }, + + play: async ({ canvasElement }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + await userEvent.click(screen.getByText('Add new proposal')) + expect( + await modal.findByText( + /^Unfortunately the account associated with the currently selected membership has insufficient balance/ + ) + ) + expect(modal.getByText('Move funds')) + }, +} + // TODO: -// - Not enough funds +// - Staking account not bound nor staking candidate > Bind account failure +// - Staking account not bound nor staking candidate > Create proposal failure +// - Staking account is a candidate > Create proposal failure +// - Staking account is confirmed > Create proposal failure +// - Discussion mode transaction > Failure // ---------------------------------------------------------------------------- // Create proposal: Specific parameters tests helpers From 5a349b9b1d0ed7e7a25b081a9f1ad0fa5ae3a57c Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 13 Jul 2023 09:49:49 +0200 Subject: [PATCH 28/32] Add transaction failure tests --- .../Proposals/CurrentProposals.stories.tsx | 200 +++++++++++++++--- packages/ui/src/mocks/data/proposals.ts | 39 +++- .../modals/AddNewProposalModal.test.tsx | 2 +- 3 files changed, 197 insertions(+), 44 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 039086c10d..14ba6d5be9 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -67,10 +67,10 @@ const OPENING_DATA = { type Args = { isCouncilMember: boolean proposalCount: number - onCreateProposal: jest.Mock - onThreadChangeThreadMode: jest.Mock onAddStakingAccountCandidate: jest.Mock onConfirmStakingAccount: jest.Mock + onCreateProposal: jest.Mock + onChangeThreadMode: jest.Mock onVote: jest.Mock } type Story = StoryObj> @@ -81,10 +81,10 @@ export default { argTypes: { proposalCount: { control: { type: 'range', max: MAX_ACTIVE_PROPOSAL } }, - onCreateProposal: { action: 'ProposalsCodex.ProposalCreated' }, - onThreadChangeThreadMode: { action: 'proposalsDiscussion.ThreadModeChanged' }, onAddStakingAccountCandidate: { action: 'Members.StakingAccountAdded' }, onConfirmStakingAccount: { action: 'Members.StakingAccountConfirmed' }, + onCreateProposal: { action: 'ProposalsCodex.ProposalCreated' }, + onChangeThreadMode: { action: 'proposalsDiscussion.ThreadModeChanged' }, onVote: { action: 'ProposalsEngine.Voted' }, }, @@ -106,7 +106,7 @@ export default { stakingAccountIdMemberStatus: { memberId: 0, - confirmed: true, + confirmed: { isTrue: true }, size: 1, }, @@ -149,10 +149,15 @@ export default { councilorReward: parameters.councilorReward, nextRewardPayments: parameters.nextRewardPayments, - onCreateProposal: args.onCreateProposal, - onThreadChangeThreadMode: args.onThreadChangeThreadMode, onAddStakingAccountCandidate: args.onAddStakingAccountCandidate, onConfirmStakingAccount: args.onConfirmStakingAccount, + onCreateProposal: args.onCreateProposal, + onChangeThreadMode: args.onChangeThreadMode, + + addStakingAccountCandidateFailure: parameters.addStakingAccountCandidateFailure, + confirmStakingAccountFailure: parameters.confirmStakingAccountFailure, + createProposalFailure: parameters.createProposalFailure, + changeThreadModeFailure: parameters.changeThreadModeFailure, }, { query: { @@ -265,19 +270,28 @@ export default { export const Default: Story = {} // ---------------------------------------------------------------------------- -// Create proposal: General parameters tests +// Create proposal: Happy case // ---------------------------------------------------------------------------- const alice = member('alice') const waitForModal = (modal: Container, name: string) => modal.findByRole('heading', { name }) +const fillSetReferralCutStep = async (modal: Container, step: StepFunction) => { + await step('Specific parameters', async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + await userEvent.type(modal.getByTestId('amount-input'), '40') + await waitFor(() => expect(nextButton).toBeEnabled()) + await userEvent.click(nextButton) + }) +} + export const AddNewProposalHappy: Story = { parameters: { isLoggedIn: false, stakingAccountIdMemberStatus: { memberId: 0, - confirmed: false, + confirmed: { isTrue: false }, size: 0, }, }, @@ -347,7 +361,7 @@ export const AddNewProposalHappy: Story = { nextButton = getButtonByText(modal, 'Next step') expect(nextButton).toBeDisabled() - await userEvent.click(modal.getByText('Signal')) + await userEvent.click(modal.getByText('Set Referral Cut')) await waitFor(() => expect(nextButton).not.toBeDisabled()) await userEvent.click(nextButton) }) @@ -437,13 +451,7 @@ export const AddNewProposalHappy: Story = { expect(modal.getByText('Specific parameters', { selector: 'h4' })) }) - await step('Specific parameters', async () => { - const editor = await getEditorByLabel(modal, 'Signal') - editor.setData('Lorem ipsum...') - await waitFor(() => expect(nextButton).toBeEnabled()) - - await userEvent.click(nextButton) - }) + await fillSetReferralCutStep(modal, step) }) await step('Bind Staking Account', async () => { @@ -477,7 +485,7 @@ export const AddNewProposalHappy: Story = { exactExecutionBlock: 9999, }) - const changeModeTxParams = args.onThreadChangeThreadMode.mock.calls.at(-1) + const changeModeTxParams = args.onChangeThreadMode.mock.calls.at(-1) expect(changeModeTxParams.length).toBe(3) const [memberId, threadId, mode] = changeModeTxParams expect(memberId).toBe(alice.id) @@ -488,6 +496,10 @@ export const AddNewProposalHappy: Story = { }, } +// ---------------------------------------------------------------------------- +// Create proposal: Failure cases +// ---------------------------------------------------------------------------- + export const NotEnoughFunds: Story = { parameters: { balance: 1 }, @@ -504,22 +516,11 @@ export const NotEnoughFunds: Story = { }, } -// TODO: -// - Staking account not bound nor staking candidate > Bind account failure -// - Staking account not bound nor staking candidate > Create proposal failure -// - Staking account is a candidate > Create proposal failure -// - Staking account is confirmed > Create proposal failure -// - Discussion mode transaction > Failure - -// ---------------------------------------------------------------------------- -// Create proposal: Specific parameters tests helpers -// ---------------------------------------------------------------------------- - -const EXECUTION_WARNING_BOX = 'I understand the implications of overriding the execution constraints validation.' const fillGeneralParameters = async ( modal: Container, step: StepFunction, - proposalType: string + proposalType: string, + closeDiscussion = false ) => { let nextButton: HTMLElement @@ -548,12 +549,145 @@ const fillGeneralParameters = async ( }) await step('Trigger & Discussion', async () => { + if (closeDiscussion) await userEvent.click(modal.getByText('Closed')) await waitFor(() => expect(nextButton).toBeEnabled()) await userEvent.click(nextButton) }) }) } +const completeForms = async (canvasElement: HTMLElement, step: StepFunction) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + localStorage.setItem('proposalCaution', 'true') + await userEvent.click(getButtonByText(screen, 'Add new proposal')) + await fillGeneralParameters(modal, step, 'Set Referral Cut', true) + await fillSetReferralCutStep(modal, step) +} + +export const BindAccountFailure: Story = { + parameters: { + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: { isTrue: false }, + size: 0, + }, + addStakingAccountCandidateFailure: 'It failed šŸ™€', + }, + + play: async ({ args, canvasElement, step }) => { + const modal = withinModal(canvasElement) + await completeForms(canvasElement, step) + await userEvent.click(modal.getByText('Sign transaction and Bind Staking Account')) + + expect(await modal.findByText('It failed šŸ™€')) + within(document.body).getByText('Transaction failed') + + expect(args.onAddStakingAccountCandidate).toHaveBeenCalled() + expect(args.onConfirmStakingAccount).not.toHaveBeenCalled() + expect(args.onCreateProposal).not.toHaveBeenCalled() + expect(args.onChangeThreadMode).not.toHaveBeenCalled() + }, +} + +export const BindAccountThenCreateProposalFailure: Story = { + parameters: { + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: { isTrue: false }, + size: 0, + }, + createProposalFailure: 'It failed šŸ™€', + }, + + play: async ({ args, canvasElement, step }) => { + const modal = withinModal(canvasElement) + await completeForms(canvasElement, step) + await userEvent.click(modal.getByText('Sign transaction and Bind Staking Account')) + await userEvent.click(await modal.findByText('Sign transaction and Create')) + + expect(await modal.findByText('It failed šŸ™€')) + within(document.body).getByText('Transaction failed') + + expect(args.onAddStakingAccountCandidate).toHaveBeenCalled() + expect(args.onConfirmStakingAccount).toHaveBeenCalled() + expect(args.onCreateProposal).toHaveBeenCalled() + expect(args.onChangeThreadMode).not.toHaveBeenCalled() + }, +} + +export const ConfirmAccountThenCreateProposalFailure: Story = { + parameters: { + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: { isTrue: false }, + size: 1, + }, + createProposalFailure: 'It failed šŸ™€', + }, + + play: async ({ args, canvasElement, step }) => { + const modal = withinModal(canvasElement) + await completeForms(canvasElement, step) + await userEvent.click(await modal.findByText('Sign transaction and Create')) + + expect(await modal.findByText('It failed šŸ™€')) + within(document.body).getByText('Transaction failed') + + expect(args.onAddStakingAccountCandidate).not.toHaveBeenCalled() + expect(args.onConfirmStakingAccount).toHaveBeenCalled() + expect(args.onCreateProposal).toHaveBeenCalled() + expect(args.onChangeThreadMode).not.toHaveBeenCalled() + }, +} + +export const CreateProposalFailure: Story = { + parameters: { + createProposalFailure: 'It failed šŸ™€', + }, + + play: async ({ args, canvasElement, step }) => { + const modal = withinModal(canvasElement) + await completeForms(canvasElement, step) + await userEvent.click(await modal.findByText('Sign transaction and Create')) + + expect(await modal.findByText('It failed šŸ™€')) + within(document.body).getByText('Transaction failed') + + expect(args.onAddStakingAccountCandidate).not.toHaveBeenCalled() + expect(args.onConfirmStakingAccount).not.toHaveBeenCalled() + expect(args.onCreateProposal).toHaveBeenCalled() + expect(args.onChangeThreadMode).not.toHaveBeenCalled() + }, +} + +export const ChangeThreadModeFailure: Story = { + parameters: { + changeThreadModeFailure: 'It failed šŸ™€', + }, + + play: async ({ args, canvasElement, step }) => { + const modal = withinModal(canvasElement) + await completeForms(canvasElement, step) + await userEvent.click(await modal.findByText('Sign transaction and Create')) + await userEvent.click(await modal.findByText('Sign transaction and change mode')) + + expect(await modal.findByText('It failed šŸ™€')) + // within(document.body).getByText('Transaction failed') + + expect(args.onAddStakingAccountCandidate).not.toHaveBeenCalled() + expect(args.onConfirmStakingAccount).not.toHaveBeenCalled() + expect(args.onCreateProposal).toHaveBeenCalled() + expect(args.onChangeThreadMode).toHaveBeenCalled() + }, +} + +// ---------------------------------------------------------------------------- +// Create proposal: Specific parameters tests +// ---------------------------------------------------------------------------- + +const EXECUTION_WARNING_BOX = 'I understand the implications of overriding the execution constraints validation.' type SpecificParametersTestFunction = ( args: Pick, 'args' | 'parameters' | 'step'> & { modal: Container @@ -584,10 +718,6 @@ const specificParametersTest = await specificStep({ args, parameters, createProposal, modal, step }) } -// ---------------------------------------------------------------------------- -// Create proposal: Specific parameters tests -// ---------------------------------------------------------------------------- - export const SpecificParametersSignal: Story = { play: specificParametersTest('Signal', async ({ args, createProposal, modal, step }) => { await createProposal(async () => { diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 25d2e03779..1f649861e4 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -144,10 +144,15 @@ type ProposalChainProps = { councilorReward?: string nextRewardPayments?: number - onCreateProposal?: jest.Mock - onThreadChangeThreadMode?: jest.Mock onAddStakingAccountCandidate?: jest.Mock onConfirmStakingAccount?: jest.Mock + onCreateProposal?: jest.Mock + onChangeThreadMode?: jest.Mock + + addStakingAccountCandidateFailure?: string + confirmStakingAccountFailure?: string + createProposalFailure?: string + changeThreadModeFailure?: string } type Chain = MocksParameters['chain'] export const proposalsPagesChain = ( @@ -163,10 +168,15 @@ export const proposalsPagesChain = ( councilorReward = joy(200), nextRewardPayments = 12345, - onCreateProposal, - onThreadChangeThreadMode, onAddStakingAccountCandidate, onConfirmStakingAccount, + onCreateProposal, + onChangeThreadMode, + + addStakingAccountCandidateFailure, + confirmStakingAccountFailure, + createProposalFailure, + changeThreadModeFailure, }: ProposalChainProps, extra?: Chain ): Chain => @@ -238,19 +248,32 @@ export const proposalsPagesChain = ( tx: { proposalsCodex: { - createProposal: { event: 'ProposalCreated', onSend: onCreateProposal }, + createProposal: { event: 'ProposalCreated', onSend: onCreateProposal, failure: createProposalFailure }, }, proposalsDiscussion: { - changeThreadMode: { event: 'ThreadModeChanged', onSend: onThreadChangeThreadMode }, + changeThreadMode: { + event: 'ThreadModeChanged', + onSend: onChangeThreadMode, + failure: changeThreadModeFailure, + }, }, members: { - addStakingAccountCandidate: { event: 'StakingAccountAdded', onSend: onAddStakingAccountCandidate }, - confirmStakingAccount: { event: 'StakingAccountConfirmed', onSend: onConfirmStakingAccount }, + addStakingAccountCandidate: { + event: 'StakingAccountAdded', + onSend: onAddStakingAccountCandidate, + failure: addStakingAccountCandidateFailure, + }, + confirmStakingAccount: { + event: 'StakingAccountConfirmed', + onSend: onConfirmStakingAccount, + failure: confirmStakingAccountFailure, + }, }, utility: { batch: { + failure: createProposalFailure, onSend: (transactions: SubmittableExtrinsic<'rxjs'>[]) => transactions.forEach((transaction) => transaction.signAndSend('')), }, diff --git a/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx b/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx index 6ebccd8577..daa4fe2bf0 100644 --- a/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx +++ b/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx @@ -1513,7 +1513,7 @@ describe('UI: AddNewProposalModal', () => { }) const getCheckbox = async () => - await screen.queryByText('Iā€™m aware of the possible risks associated with creating a proposal.') + await screen.queryByText("I'm aware of the possible risks associated with creating a proposal.") async function finishWarning() { await renderModal() From 3532fc2e5ce946d690e140534801acaf7f535980 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 13 Jul 2023 09:55:10 +0200 Subject: [PATCH 29/32] Remove the "UI: AddNewProposalModal" test suite --- .../modals/AddNewProposalModal.test.tsx | 1725 ----------------- 1 file changed, 1725 deletions(-) diff --git a/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx b/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx index daa4fe2bf0..68729d7551 100644 --- a/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx +++ b/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx @@ -1,119 +1,6 @@ -import { OpeningMetadata } from '@joystream/metadata-protobuf' -import { cryptoWaitReady } from '@polkadot/util-crypto' -import { act, configure, fireEvent, render, screen, waitFor } from '@testing-library/react' import BN from 'bn.js' -import { findLast } from 'lodash' -import React from 'react' -import { MemoryRouter } from 'react-router' -import { interpret } from 'xstate' -import { MoveFundsModalCall } from '@/accounts/modals/MoveFundsModal' -import { ApiContext } from '@/api/providers/context' -import { CurrencyName } from '@/app/constants/currency' -import { GlobalModals } from '@/app/GlobalModals' -import { CKEditorProps } from '@/common/components/CKEditor' -import { camelCaseToText } from '@/common/helpers' import { createType } from '@/common/model/createType' -import { metadataFromBytes } from '@/common/model/JoystreamNode/metadataFromBytes' -import { getSteps } from '@/common/model/machines/getSteps' -import { ModalContextProvider } from '@/common/providers/modal/provider' -import { powerOf2 } from '@/common/utils/bn' -import { MembershipContext } from '@/memberships/providers/membership/context' -import { MyMemberships } from '@/memberships/providers/membership/provider' -import { - seedApplication, - seedApplications, - seedMembers, - seedOpening, - seedOpenings, - seedOpeningStatuses, - seedUpcomingOpenings, - seedWorkers, - seedWorkingGroups, - updateWorkingGroups, -} from '@/mocks/data' -import workingGroups from '@/mocks/data/raw/workingGroups.json' -import { addNewProposalMachine } from '@/proposals/modals/AddNewProposal/machine' -import { ProposalType } from '@/proposals/types' - -import { getButton } from '../../_helpers/getButton' -import { selectFromDropdown } from '../../_helpers/selectFromDropdown' -import { toggleCheckBox } from '../../_helpers/toggleCheckBox' -import { mockCKEditor } from '../../_mocks/components/CKEditor' -import { mockUseCurrentBlockNumber } from '../../_mocks/hooks/useCurrentBlockNumber' -import { alice, bob } from '../../_mocks/keyring' -import { getMember } from '../../_mocks/members' -import { MockKeyringProvider, MockQueryNodeProviders } from '../../_mocks/providers' -import { setupMockServer } from '../../_mocks/server' -import { - stubAccounts, - stubApi, - stubConst, - stubCouncilAndReferendum, - stubCouncilConstants, - stubDefaultBalances, - stubProposalConstants, - stubQuery, - stubTransaction, - stubTransactionFailure, - stubTransactionSuccess, -} from '../../_mocks/transactions' -import { mockedTransactionFee, mockTransactionFee, mockUseModalCall } from '../../setup' - -const QUESTION_INPUT = OpeningMetadata.ApplicationFormQuestion.InputType - -configure({ testIdAttribute: 'id' }) - -jest.mock('@/common/components/CKEditor', () => ({ - CKEditor: (props: CKEditorProps) => mockCKEditor(props), -})) - -jest.mock('@/common/hooks/useCurrentBlockNumber', () => ({ - useCurrentBlockNumber: () => mockUseCurrentBlockNumber(), -})) - -jest.mock('react-dropzone', () => { - const reactDropzone = jest.requireActual('react-dropzone') - return { - ...reactDropzone, - useDropzone: (props: any) => - reactDropzone.useDropzone({ - ...props, - getFilesFromEvent: (event: React.ChangeEvent) => event.target.files, - }), - } -}) - -const OPENING_DATA = { - id: 'forumWorkingGroup-1337', - runtimeId: 1337, - groupId: 'forumWorkingGroup', - stakeAmount: 4000, - rewardPerBlock: 200, - version: 1, - type: 'LEADER', - status: 'open', - unstakingPeriod: 25110, - metadata: { - title: 'Foo', - shortDescription: '', - description: '', - hiringLimit: 1, - applicationDetails: '', - applicationFormQuestions: [], - expectedEnding: '2021-12-06T14:26:06.283Z', - }, -} - -const APPLICATION_DATA = { - id: 'forumWorkingGroup-1337', - runtimeId: 1337, - openingId: 'forumWorkingGroup-1337', - applicantId: '0', - answers: [], - status: 'pending', - stake: new BN(10000), -} describe('AddNewProposalModal types parameters', () => { describe('Specific parameters', () => { @@ -137,1615 +24,3 @@ describe('AddNewProposalModal types parameters', () => { }) }) }) - -describe('UI: AddNewProposalModal', () => { - const api = stubApi() - - const useMyMemberships: MyMemberships = { - active: undefined, - members: [], - setActive: (member) => (useMyMemberships.active = member), - isLoading: false, - hasMembers: true, - helpers: { - getMemberIdByBoundAccountAddress: () => undefined, - }, - } - const forumLeadId = workingGroups.find((group) => group.id === 'forumWorkingGroup')?.leadId - const showModal = jest.fn() - let createProposalTx: any - let batchTx: any - let bindAccountTx: any - let changeModeTx: any - let createProposalTxMock: jest.Mock - - const server = setupMockServer({ noCleanupAfterEach: true }) - - beforeAll(async () => { - await cryptoWaitReady() - mockUseModalCall({ showModal, modal: 'AddNewProposalModal' }) - seedMembers(server.server) - seedWorkingGroups(server.server) - seedOpeningStatuses(server.server) - seedOpenings(server.server) - seedUpcomingOpenings(server.server) - seedApplications(server.server) - seedOpening(OPENING_DATA, server.server) - seedApplication(APPLICATION_DATA, server.server) - seedWorkers(server.server) - updateWorkingGroups(server.server) - stubAccounts([alice, bob]) - }) - - beforeEach(async () => { - mockTransactionFee({ feeInfo: { transactionFee: new BN(100), canAfford: true } }) - - useMyMemberships.members = [getMember('alice'), getMember('bob')] - useMyMemberships.setActive(getMember('alice')) - - stubDefaultBalances() - stubProposalConstants(api) - stubCouncilConstants(api) - stubCouncilAndReferendum(api, 'Announcing', 'Inactive') - - createProposalTx = stubTransaction(api, 'api.tx.proposalsCodex.createProposal', 25) - createProposalTxMock = api.api.tx.proposalsCodex.createProposal as unknown as jest.Mock - - stubTransaction(api, 'api.tx.members.confirmStakingAccount', 25) - stubQuery( - api, - 'members.stakingAccountIdMemberStatus', - createType('PalletMembershipStakingAccountMemberBinding', { - memberId: 0, - confirmed: false, - }) - ) - stubQuery(api, 'members.stakingAccountIdMemberStatus.size', createType('u64', 0)) - stubQuery(api, 'content.minCashoutAllowed', new BN(5)) - stubQuery(api, 'content.maxCashoutAllowed', new BN(5000)) - stubConst(api, 'content.minimumCashoutAllowedLimit', new BN(5)) - stubConst(api, 'content.maximumCashoutAllowedLimit', new BN(5000)) - stubConst(api, 'proposalsEngine.titleMaxLength', createType('u32', 1000)) - stubConst(api, 'proposalsEngine.descriptionMaxLength', createType('u32', 1000)) - batchTx = stubTransaction(api, 'api.tx.utility.batch') - bindAccountTx = stubTransaction(api, 'api.tx.members.addStakingAccountCandidate', 42) - changeModeTx = stubTransaction(api, 'api.tx.proposalsDiscussion.changeThreadMode', 10) - }) - - describe('Requirements', () => { - beforeEach(async () => { - await renderModal() - }) - - it('No active member', async () => { - useMyMemberships.active = undefined - - await renderModal() - - expect(showModal).toBeCalledWith({ - modal: 'SwitchMember', - data: { originalModalName: 'AddNewProposalModal', originalModalData: null }, - }) - }) - }) - - describe('Warning modal', () => { - beforeEach(async () => { - await renderModal() - }) - it('Not checked', async () => { - const button = await getWarningNextButton() - expect(await screen.queryByText('Do not show this message again.')).toBeDefined() - expect(button).toBeDisabled() - }) - - it('Checked', async () => { - const button = await getWarningNextButton() - - const checkbox = await getCheckbox() - fireEvent.click(checkbox as HTMLElement) - - expect(button).toBeEnabled() - }) - }) - - describe('Stepper modal', () => { - it('Renders a modal', async () => { - await finishWarning() - - expect(await screen.findByText('Creating new proposal')).toBeDefined() - }) - - it('Steps', () => { - const service = interpret(addNewProposalMachine) - service.start() - - expect(getSteps(service)).toEqual([ - { title: 'Proposal type', type: 'next' }, - { title: 'General parameters', type: 'next' }, - { title: 'Staking account', type: 'next', isBaby: true }, - { title: 'Proposal details', type: 'next', isBaby: true }, - { title: 'Trigger & Discussion', type: 'next', isBaby: true }, - { title: 'Specific parameters', type: 'next' }, - ]) - }) - - describe('Proposal type', () => { - beforeEach(async () => { - await finishWarning() - }) - - it('Not selected', async () => { - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Selected', async () => { - const type = (await screen.findByText('Funding Request')).parentElement?.parentElement as HTMLElement - fireEvent.click(type) - - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - }) - - describe('Required stake', () => { - beforeEach(async () => { - await finishWarning() - }) - - it('Not enough funds', async () => { - const requiredStake = 9999 - stubProposalConstants(api, { requiredStake }) - await finishProposalType() - - const moveFundsModalCall: MoveFundsModalCall = { - modal: 'MoveFundsModal', - data: { - requiredStake: new BN(requiredStake), - lock: 'Proposals', - isFeeOriented: false, - }, - } - - expect(showModal).toBeCalledWith({ ...moveFundsModalCall }) - }) - - it('Enough funds', async () => { - await finishProposalType() - expect(screen.findByText('Creating new proposal')).toBeDefined() - }) - }) - - describe('General parameters', () => { - describe('Staking account', () => { - beforeEach(async () => { - await finishWarning() - await finishProposalType() - }) - - it('Not selected', async () => { - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Selected', async () => { - await selectFromDropdown('Select account for Staking', 'alice') - - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - }) - - describe('Proposal details', () => { - beforeEach(async () => { - await finishWarning() - await finishProposalType() - await finishStakingAccount() - }) - - it('Not filled', async () => { - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Filled', async () => { - await fillProposalDetails() - - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - }) - - describe('Proposal details validation', () => { - beforeEach(async () => { - stubConst(api, 'proposalsEngine.titleMaxLength', createType('u32', 5)) - stubConst(api, 'proposalsEngine.descriptionMaxLength', createType('u32', 5)) - await finishWarning() - await finishProposalType() - }) - it('Title too long', async () => { - stubConst(api, 'proposalsEngine.titleMaxLength', createType('u32', 5)) - - await finishStakingAccount() - - await fillProposalDetails() - - expect(await screen.findByText(/Title exceeds maximum length/i)).toBeDefined() - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Description too long', async () => { - await finishStakingAccount() - - await fillProposalDetails() - - expect(await screen.findByText(/Rationale exceeds maximum length/i)).toBeDefined() - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Both fields too long', async () => { - await finishStakingAccount() - - await fillProposalDetails() - - expect(await screen.findByText(/Title exceeds maximum length/i)).toBeDefined() - expect(await screen.findByText(/Rationale exceeds maximum length/i)).toBeDefined() - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - }) - - describe('Trigger & Discussion', () => { - beforeEach(async () => { - await finishWarning() - await finishProposalType() - await finishStakingAccount() - await finishProposalDetails() - }) - - it('Default(Trigger - No, Discussion Mode - Open)', async () => { - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - - describe('Trigger - Yes', () => { - beforeEach(async () => { - await triggerYes() - }) - - it('Not filled block number', async () => { - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Invalid block number: too low', async () => { - await triggerYes() - await fillTriggerBlock(10) - - await waitFor(async () => expect(await screen.getByText('The minimum block number is 20')).toBeDefined()) - - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Invalid block number: too high', async () => { - await act(async () => { - await triggerYes() - await fillTriggerBlock(99999999999999) - }) - expect(screen.queryAllByText(/(^The maximum block number is \d*)*/i)).toBeDefined() - const button = await getNextStepButton() - expect(button).toBeDisabled() - }) - - it('Valid block number', async () => { - await triggerYes() - await fillTriggerBlock(30) - - expect(await screen.getByText(/^ā‰ˆ.*/i)).toBeDefined() - - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - }) - - describe('Discussion Mode - Closed', () => { - beforeEach(async () => { - await discussionClosed() - }) - - it('Add member to whitelist', async () => { - await selectFromDropdown('Add member to whitelist', 'alice') - - expect(await screen.getByTestId('removeMember')).toBeDefined() - - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - - it('Remove member from whitelist', async () => { - await selectFromDropdown('Add member to whitelist', 'alice') - - expect(await screen.getByTestId('removeMember')).toBeDefined() - - fireEvent.click(await screen.getByTestId('removeMember')) - expect(screen.queryByTestId('removeMember')).toBeNull() - - const button = await getNextStepButton() - expect(button).not.toBeDisabled() - }) - }) - }) - }) - - describe('Specific parameters', () => { - const getTxParameters = async () => { - const [, getTransaction] = findLast(mockedTransactionFee.mock.calls, (params) => params.length > 0) ?? [] - if (!getTransaction) return - - createProposalTxMock.mockReset() - await getTransaction() - return createProposalTxMock.mock.calls[0] - } - - beforeEach(async () => { - await finishWarning() - }) - - describe('Type - Signal', () => { - beforeEach(async () => { - await finishProposalType('signal') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Invalid - signal field not filled ', async () => { - expect(screen.queryByLabelText(/^signal/i)).toHaveValue('') - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Valid - signal field filled', async () => { - const signal = 'Foo' - await SpecificParameters.Signal.fillSignal(signal) - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSignal.toHuman() - expect(parameters).toEqual(signal) - const button = await getCreateButton() - expect(button).toBeEnabled() - }) - }) - - describe('Type - Funding Request', () => { - beforeEach(async () => { - await finishProposalType('fundingRequest') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Invalid - not filled amount, no selected recipient', async () => { - expect(screen.queryByText('Recipient account')).not.toBeNull() - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Invalid - no selected recipient', async () => { - await SpecificParameters.fillAmount(100) - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Invalid - not filled amount', async () => { - await SpecificParameters.FundingRequest.selectRecipient('bob') - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Invalid - amount exceeds max value of 10k', async () => { - await SpecificParameters.FundingRequest.selectRecipient('bob') - await SpecificParameters.fillAmount(100_000) - - const button = await getCreateButton() - expect(screen.queryByText(/^Maximal amount allowed is*/)).toBeInTheDocument() - expect(button).toBeDisabled() - }) - - it('Valid - everything filled', async () => { - const amount = 100 - await SpecificParameters.fillAmount(amount) - await SpecificParameters.FundingRequest.selectRecipient('bob') - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asFundingRequest.toJSON() - expect(parameters).toEqual([ - { - account: getMember('bob').controllerAccount, - amount: amount, - }, - ]) - const button = await getCreateButton() - expect(button).not.toBeDisabled() - }) - }) - - describe('Type - Set Referral Cut', () => { - beforeEach(async () => { - await finishProposalType('setReferralCut') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Default - Invalid', async () => { - expect(await screen.getByTestId('amount-input')).toHaveValue('0') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Invalid - over 100 percent', async () => { - await SpecificParameters.fillAmount(200) - expect(await screen.getByTestId('amount-input')).toHaveValue('200') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid', async () => { - const amount = 40 - await SpecificParameters.fillAmount(amount) - expect(await screen.getByTestId('amount-input')).toHaveValue(String(amount)) - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetReferralCut.toJSON() - - expect(parameters).toEqual(amount) - expect(await getCreateButton()).toBeEnabled() - }) - - it('Valid with execution warning', async () => { - const amount = 100 - const button = await getCreateButton() - - await SpecificParameters.fillAmount(amount) - expect(await screen.getByTestId('amount-input')).toHaveValue(String(amount)) - expect(button).toBeDisabled() - - const checkbox = screen.getByTestId('execution-requirement') - fireEvent.click(checkbox) - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetReferralCut.toJSON() - - expect(parameters).toEqual(amount) - expect(button).toBeEnabled() - }) - }) - - describe('Type - Decrease Working Group Lead Stake', () => { - beforeEach(async () => { - await finishProposalType('decreaseWorkingGroupLeadStake') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Default - not filled amount, no selected group', async () => { - expect(screen.queryByText('Working Group Lead')).toBeNull() - expect(await getButton(/By half/i)).toBeDisabled() - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Group selected, amount filled with half stake', async () => { - await SpecificParameters.DecreaseWorkingGroupLeadStake.selectGroup('Forum') - await waitFor(async () => expect(await getButton(/By half/i)).not.toBeDisabled()) - - expect(screen.queryByText(/The actual stake for Forum Working Group Lead is /i)).not.toBeNull() - - const button = await getCreateButton() - expect(button).not.toBeDisabled() - }) - - it('Zero amount entered', async () => { - await SpecificParameters.DecreaseWorkingGroupLeadStake.selectGroup('Forum') - await waitFor(async () => expect(await getButton(/By half/i)).not.toBeDisabled()) - await SpecificParameters.fillAmount(0) - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Valid - group selected, amount filled', async () => { - const amount = 100 - const group = 'Forum' - await SpecificParameters.DecreaseWorkingGroupLeadStake.selectGroup(group) - await waitFor(() => - expect(screen.queryByText(/The actual stake for Forum Working Group Lead is /i)).not.toBeNull() - ) - await SpecificParameters.fillAmount(amount) - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asDecreaseWorkingGroupLeadStake.toJSON() - expect(parameters).toEqual([Number(forumLeadId?.split('-')[1]), amount, group]) - - const button = await getCreateButton() - expect(button).not.toBeDisabled() - }) - }) - - describe('Type - Terminate Working Group Lead', () => { - beforeEach(async () => { - await finishProposalType('terminateWorkingGroupLead') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Default - not filled amount, no selected group', async () => { - expect(await screen.findByLabelText('Working Group', { selector: 'input' })).toHaveValue('') - - const button = await getCreateButton() - expect(button).toBeDisabled() - }) - - it('Valid - group selected, amount: not filled/filled', async () => { - const group = 'Forum' - const slashingAmount = 100 - await SpecificParameters.TerminateWorkingGroupLead.selectGroup(group) - const workingGroup = server?.server?.schema.find('WorkingGroup', 'forumWorkingGroup') as any - const leader = workingGroup?.leader.membership - expect(await screen.findByText(leader?.handle)).toBeDefined() - - const button = await getCreateButton() - expect(button).not.toBeDisabled() - - await triggerYes() - await SpecificParameters.fillAmount(slashingAmount) - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asTerminateWorkingGroupLead.toJSON() - expect(parameters).toEqual({ - slashingAmount, - workerId: Number(forumLeadId?.split('-')[1]), - group, - }) - - expect(button).not.toBeDisabled() - }) - }) - - describe('Type - Create Working Group Lead Opening', () => { - beforeEach(async () => { - await finishProposalType('createWorkingGroupLeadOpening') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Create Working Group Lead Opening$/i)).toBeDefined() - }) - - it('Step 1: Invalid to Valid', async () => { - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.selectGroup('Forum') - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillTitle('Foo') - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillDescription('Bar') - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillShortDescription('Baz') - expect(await getNextStepButton()).toBeEnabled() - }) - - it('Step 2: Invalid to Valid', async () => { - await SpecificParameters.CreateWorkingGroupLeadOpening.flow({ - group: 'Forum', - title: 'Foo', - description: 'Bar', - shortDesc: 'Baz', - }) - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillDetails('Lorem ipsum') - expect(await getNextStepButton()).toBeEnabled() - - await toggleCheckBox(true) - await fillField('field-period-length', '0') - expect(await getNextStepButton()).toBeDisabled() - - await toggleCheckBox(false) - expect(await getNextStepButton()).toBeEnabled() - }) - - it('Step 3: Invalid to Valid', async () => { - await SpecificParameters.CreateWorkingGroupLeadOpening.flow( - { group: 'Forum', title: 'Foo', description: 'Bar', shortDesc: 'Baz' }, - { duration: 100, details: 'Lorem ipsum' } - ) - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillQuestionField('šŸ?', 0) - expect(await getNextStepButton()).toBeEnabled() - - const addQuestionBtn = await screen.findByText('Add new question') - act(() => { - fireEvent.click(addQuestionBtn) - }) - expect(await getNextStepButton()).toBeDisabled() - - await toggleCheckBox(false, 1) - expect(await getNextStepButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillQuestionField('šŸ˜?', 1) - expect(await getNextStepButton()).toBeEnabled() - }) - - it('Step 4: Invalid to valid', async () => { - const step1 = { group: 'Storage', title: 'Foo', description: 'Bar', shortDesc: 'Baz' } - const step2 = { duration: 100, details: 'Lorem ipsum' } - const step3 = { - questions: [ - { question: 'Short?', type: QUESTION_INPUT.TEXT }, - { question: 'Long?', type: QUESTION_INPUT.TEXTAREA }, - ], - } - const step4 = { stake: 100, unstakingPeriod: 101, rewardPerBlock: 102 } - - await SpecificParameters.CreateWorkingGroupLeadOpening.flow(step1, step2, step3) - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillStakingAmount(step4.stake) - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillUnstakingPeriod(step4.unstakingPeriod) - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.CreateWorkingGroupLeadOpening.fillRewardPerBlock(step4.rewardPerBlock) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - - const { description: metadata, ...data } = txSpecificParameters.asCreateWorkingGroupLeadOpening.toJSON() - expect(data).toEqual({ - rewardPerBlock: step4.rewardPerBlock, - stakePolicy: { - stakeAmount: step4.stake, - leavingUnstakingPeriod: step4.unstakingPeriod, - }, - group: step1.group, - }) - - expect(metadataFromBytes(OpeningMetadata, metadata)).toEqual({ - title: step1.title, - shortDescription: step1.shortDesc, - description: step1.description, - hiringLimit: 1, - expectedEndingTimestamp: step2.duration, - applicationDetails: step2.details, - applicationFormQuestions: step3.questions, - }) - }) - }) - - describe('Type - Set Working Group Lead Reward', () => { - beforeEach(async () => { - await finishProposalType('setWorkingGroupLeadReward') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Set Working Group Lead Reward$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByLabelText(/^Working Group$/i, { selector: 'input' })).toHaveValue('') - expect(await screen.queryByTestId('amount-input')).toHaveValue('') - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.SetWorkingGroupLeadReward.fillRewardAmount(0) - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - const group = 'Forum' - const amount = 100 - await SpecificParameters.SetWorkingGroupLeadReward.selectGroup(group) - await SpecificParameters.SetWorkingGroupLeadReward.fillRewardAmount(amount) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetWorkingGroupLeadReward.toJSON() - expect(parameters).toEqual([Number(forumLeadId?.split('-')[1]), amount, group]) - }) - }) - - describe('Type - Set Max Validator Count', () => { - beforeEach(async () => { - await finishProposalType('setMaxValidatorCount') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Set max validator count$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByTestId('amount-input')).toHaveValue('0') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Validate max and min value', async () => { - await SpecificParameters.fillAmount(400) - expect(await screen.queryByText('Maximal amount allowed is')) - - await SpecificParameters.fillAmount(0) - expect(await screen.queryByText('Minimal amount allowed is')) - }) - - it('Valid form', async () => { - const amount = 100 - await SpecificParameters.fillAmount(amount) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetMaxValidatorCount.toJSON() - await waitFor(() => expect(parameters).toEqual(amount)) - }) - }) - - describe('Type - Cancel Working Group Lead Opening', () => { - beforeEach(async () => { - await finishProposalType('cancelWorkingGroupLeadOpening') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Cancel Working Group Lead Opening$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByLabelText(/^Opening/i, { selector: 'input' })).toHaveValue('') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - await SpecificParameters.CancelWorkingGroupLeadOpening.selectedOpening('forumWorkingGroup-1337') - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asCancelWorkingGroupLeadOpening.toJSON() - expect(parameters).toEqual([1337, 'Forum']) - expect(await getCreateButton()).toBeEnabled() - }) - }) - - describe('Type - Set Council Budget Increment', () => { - beforeEach(async () => { - await finishProposalType('setCouncilBudgetIncrement') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Tokens added to council budget every day$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByTestId('amount-input')).toHaveValue('') - expect(await screen.queryByTestId('amount-input')).toBeEnabled() - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.fillAmount(0) - expect(await getCreateButton()).toBeDisabled() - }) - - it('Validate max value', async () => { - await SpecificParameters.fillAmount(powerOf2(128)) - expect(await screen.queryByTestId('amount-input')).toHaveValue('') - expect(await screen.queryByTestId('amount-input')).toBeEnabled() - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - const amount = 100 - await SpecificParameters.fillAmount(amount) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetCouncilBudgetIncrement.toJSON() - expect(parameters).toEqual(amount) - }) - }) - - describe('Type - Set Councilor Reward', () => { - beforeEach(async () => { - await finishProposalType('setCouncilorReward') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Set Councilor Reward$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByTestId('amount-input')).toHaveValue('') - expect(await screen.queryByTestId('amount-input')).toBeEnabled() - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.fillAmount(0) - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - const amount = 100 - await SpecificParameters.fillAmount(amount) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetCouncilorReward.toJSON() - expect(parameters).toEqual(amount) - }) - }) - - describe('Type - Set Membership lead invitation quota proposal', () => { - beforeEach(async () => { - await finishProposalType('setMembershipLeadInvitationQuota') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Set Membership Lead Invitation Quota$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - await waitFor(async () => expect(await screen.queryByTestId('amount-input')).toBeEnabled()) - expect(await screen.queryByTestId('amount-input')).toHaveValue('0') - expect(await screen.queryByTestId('amount-input')).toBeEnabled() - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.fillAmount(0) - expect(await getCreateButton()).toBeDisabled() - }) - - it('Validate max value', async () => { - await waitFor(async () => expect(await screen.queryByTestId('amount-input')).toBeEnabled()) - await SpecificParameters.fillAmount(powerOf2(32)) - expect(screen.queryByTestId('amount-input')).toHaveValue('0') - expect(screen.queryByTestId('amount-input')).toBeEnabled() - }) - - it('Valid form', async () => { - const amount = 100 - await waitFor(async () => expect(await screen.queryByTestId('amount-input')).toBeEnabled()) - await SpecificParameters.fillAmount(amount) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetMembershipLeadInvitationQuota.toJSON() - expect(parameters).toEqual(amount) - }) - }) - describe('Type - Fill Working Group Lead Opening', () => { - beforeEach(async () => { - await finishProposalType('fillWorkingGroupLeadOpening') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Fill Working Group Lead Opening$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByLabelText(/^Opening/i, { selector: 'input' })).toHaveValue('') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - await SpecificParameters.FillWorkingGroupLeadOpening.selectedOpening('forumWorkingGroup-1337') - await SpecificParameters.FillWorkingGroupLeadOpening.selectApplication( - `Member ID: ${APPLICATION_DATA.applicantId}` - ) - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asFillWorkingGroupLeadOpening.toJSON() - expect(parameters).toEqual({ - openingId: 1337, - applicationId: 1337, - workingGroup: 'Forum', - }) - expect(await getCreateButton()).toBeEnabled() - }) - }) - describe('Type - Set Initial Invitation Count', () => { - beforeAll(() => { - stubQuery(api, 'members.initialInvitationCount', createType('u32', 13)) - }) - - beforeEach(async () => { - await finishProposalType('setInitialInvitationCount') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Set Initial Invitation Count$/i)).toBeDefined() - }) - - it('Displays current invitations count', async () => { - expect(await screen.findByText('The current initial invitation count is 13.')).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.findByLabelText(/^New Count$/i, { selector: 'input' })).toHaveValue('0') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - const count = 1 - await SpecificParameters.SetInitialInvitationCount.fillCount(1) - expect(await getCreateButton()).toBeEnabled() - await SpecificParameters.SetInitialInvitationCount.fillCount(count) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetInitialInvitationCount.toJSON() - expect(parameters).toEqual(count) - }) - }) - describe('Type - Set Initial Invitation Balance', () => { - beforeAll(() => { - stubQuery(api, 'members.initialInvitationBalance', createType('Balance', 2137)) - }) - - beforeEach(async () => { - await finishProposalType('setInitialInvitationBalance') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - - expect(screen.getByText(/^Set Initial Invitation Balance$/i)).toBeDefined() - }) - - it('Invalid form', async () => { - expect(await screen.queryByTestId('amount-input')).toHaveValue('') - expect(await getCreateButton()).toBeDisabled() - - await SpecificParameters.fillAmount(0) - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid form', async () => { - const amount = 1000 - await SpecificParameters.fillAmount(amount) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetInitialInvitationBalance.toJSON() - expect(parameters).toEqual(amount) - }) - - it('Displays current balance', async () => { - expect(await screen.queryByText(`The current balance is 2137 ${CurrencyName.integerValue}.`)).toBeDefined() - }) - }) - describe('Type - Set Membership price', () => { - beforeEach(async () => { - await finishProposalType('setMembershipPrice') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Default - Invalid', async () => { - expect(await screen.getByTestId('amount-input')).toHaveValue('') - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid', async () => { - const price = 100 - await SpecificParameters.fillAmount(price) - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asSetMembershipPrice.toJSON() - expect(parameters).toEqual(price) - }) - }) - - describe('Type - Update Working Group Budget', () => { - beforeEach(async () => { - stubQuery(api, 'council.budget', new BN(2500)) - await finishProposalType('updateWorkingGroupBudget') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Default - no selected group, amount not filled', async () => { - expect(await screen.findByLabelText('Working Group', { selector: 'input' })).toHaveValue('') - - expect(await getCreateButton()).toBeDisabled() - }) - - it('Invalid - group selected, positive amount bigger than current council budget', async () => { - await SpecificParameters.UpdateWorkingGroupBudget.selectGroup('Forum') - await waitFor(() => expect(screen.queryByText(/Current budget for Forum Working Group is /i)).not.toBeNull()) - await SpecificParameters.fillAmount(3000) - - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid - group selected, amount automatically filled', async () => { - await SpecificParameters.UpdateWorkingGroupBudget.selectGroup('Forum') - await waitFor(() => expect(screen.queryByText(/Current budget for Forum Working Group is /i)).not.toBeNull()) - - expect(await getCreateButton()).not.toBeDisabled() - }) - - it('Valid - group selected, amount bigger than current stake filled', async () => { - await SpecificParameters.UpdateWorkingGroupBudget.selectGroup('Forum') - await waitFor(() => expect(screen.queryByText(/Current budget for Forum Working Group is /i)).not.toBeNull()) - await SpecificParameters.fillAmount(1000) - - expect(await getCreateButton()).toBeEnabled() - }) - - it('Invaild - group selected, negative amount bigger than current WG budget', async () => { - await SpecificParameters.UpdateWorkingGroupBudget.selectGroup('Forum') - await waitFor(() => expect(screen.queryByText(/Current budget for Forum Working Group is /i)).not.toBeNull()) - - // Switch to 'Decrease budget', input will be handled as negative - await triggerYes() - await SpecificParameters.fillAmount(999999) - - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid - group selected, negative amount filled', async () => { - const amount = 100 - const group = 'Forum' - await SpecificParameters.UpdateWorkingGroupBudget.selectGroup('Forum') - await waitFor(() => expect(screen.queryByText(/Current budget for Forum Working Group is /i)).not.toBeNull()) - - // Switch to 'Decrease budget', input will be handled as negative - await triggerYes() - await SpecificParameters.fillAmount(amount) - - expect(await getCreateButton()).toBeEnabled() - - const [, txSpecificParameters] = await getTxParameters() - const parameters = txSpecificParameters.asUpdateWorkingGroupBudget.toJSON() - expect(parameters).toEqual([amount, group, 'Negative']) - }) - }) - - describe('Type - Runtime Upgrade', () => { - beforeEach(async () => { - await finishProposalType('runtimeUpgrade') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - }) - - it('Default - Invalid', async () => { - expect(await getCreateButton()).toBeDisabled() - }) - - it('Valid', async () => { - const file = Object.defineProperties(new File([], 'runtime.wasm', { type: 'application/wasm' }), { - isValidWASM: { value: true }, - arrayBuffer: { value: () => Promise.resolve(new ArrayBuffer(1)) }, - size: { value: 1 }, - }) - - await act(async () => { - fireEvent.change(screen.getByTestId('runtime-upgrade-input'), { target: { files: [file] } }) - }) - - expect(await getCreateButton()).toBeEnabled() - }) - }) - }) - - describe('Authorize', () => { - it('Fee fail before transaction', async () => { - await finishWarning() - await finishProposalType('fundingRequest') - const requiredStake = 10 - stubProposalConstants(api, { requiredStake }) - stubTransaction(api, 'api.tx.utility.batch', 10000) - mockTransactionFee({ feeInfo: { transactionFee: new BN(10000), canAfford: false } }) - - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - await SpecificParameters.FundingRequest.finish(100, 'bob') - - const moveFundsModalCall: MoveFundsModalCall = { - modal: 'MoveFundsModal', - data: { - requiredStake: new BN(requiredStake), - lock: 'Proposals', - isFeeOriented: true, - }, - } - - expect(showModal).toBeCalledWith({ ...moveFundsModalCall }) - }) - - describe('Staking account not bound nor staking candidate', () => { - beforeEach(async () => { - mockTransactionFee({ transaction: batchTx }) - await finishWarning() - await finishProposalType('fundingRequest') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - await SpecificParameters.FundingRequest.finish(100, 'bob') - }) - - it('Bind account step', async () => { - expect(await screen.findByText('You intend to bind account for staking')).toBeDefined() - expect((await screen.findByText(/^modals.transactionFee.label/i))?.nextSibling?.textContent).toBe('42') - }) - - it('Bind account failure', async () => { - stubTransactionFailure(bindAccountTx) - - await act(async () => { - fireEvent.click(screen.getByText(/^Sign transaction/i)) - }) - - expect(await screen.findByText('Failure')).toBeDefined() - }) - - it('Create proposal step', async () => { - stubTransactionSuccess(bindAccountTx, 'members', 'StakingAccountAdded') - - await act(async () => { - fireEvent.click(screen.getByText(/^Sign transaction/i)) - }) - - expect(await screen.findByText(/You intend to create a proposal/i)).toBeDefined() - expect(screen.getByText(/modals\.transactionFee\.label/i)?.nextSibling?.textContent).toBe('25') - }) - - it('Create proposal success', async () => { - stubTransactionSuccess(bindAccountTx, 'members', 'StakingAccountAdded') - await act(async () => { - fireEvent.click(screen.getByText(/^Sign transaction/i)) - }) - stubTransactionSuccess(batchTx, 'proposalsCodex', 'ProposalCreated', [createType('ProposalId', 1337)]) - - await act(async () => { - fireEvent.click(await screen.findByText(/^Sign transaction and Create$/i)) - }) - - expect(await screen.findByText('See my Proposal')).toBeDefined() - }) - - it('Create proposal failure', async () => { - stubTransactionSuccess(bindAccountTx, 'members', 'StakingAccountAdded') - await act(async () => { - fireEvent.click(screen.getByText(/^Sign transaction/i)) - }) - stubTransactionFailure(batchTx) - - await act(async () => { - fireEvent.click(await screen.findByText(/^Sign transaction and Create$/i)) - }) - - expect(await screen.findByText('Failure')).toBeDefined() - }) - }) - - describe('Staking account is a candidate', () => { - beforeEach(async () => { - mockTransactionFee({ transaction: batchTx }) - stubQuery( - api, - 'members.stakingAccountIdMemberStatus', - createType('PalletMembershipStakingAccountMemberBinding', { - memberId: createType('MemberId', 0), - confirmed: createType('bool', false), - }) - ) - stubQuery(api, 'members.stakingAccountIdMemberStatus.size', createType('u64', 8)) - - await finishWarning() - await finishProposalType('fundingRequest') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - await SpecificParameters.FundingRequest.finish(100, 'bob') - }) - - it('Create proposal step', async () => { - expect(await screen.findByText(/You intend to create a proposa/i)).not.toBeNull() - expect((await screen.findByText(/^modals.transactionFee.label/i))?.nextSibling?.textContent).toBe('25') - }) - - it('Create proposal success', async () => { - stubTransactionSuccess(batchTx, 'proposalsCodex', 'ProposalCreated', [createType('ProposalId', 1337)]) - - await act(async () => { - fireEvent.click(await screen.getByText(/^Sign transaction and Create$/i)) - }) - - expect(await screen.findByText('See my Proposal')).toBeDefined() - }) - - it('Create proposal failure', async () => { - stubTransactionFailure(batchTx) - - await act(async () => { - fireEvent.click(await screen.getByText(/^Sign transaction and Create$/i)) - }) - - expect(await screen.findByText('Failure')).toBeDefined() - }) - }) - - describe('Staking account is confirmed', () => { - beforeEach(async () => { - mockTransactionFee({ transaction: createProposalTx }) - stubQuery( - api, - 'members.stakingAccountIdMemberStatus', - createType('PalletMembershipStakingAccountMemberBinding', { - memberId: createType('MemberId', 0), - confirmed: createType('bool', true), - }) - ) - stubQuery(api, 'members.stakingAccountIdMemberStatus.size', createType('u64', 8)) - - await finishWarning() - await finishProposalType('fundingRequest') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion() - await SpecificParameters.FundingRequest.finish(100, 'bob') - }) - - it('Create proposal step', async () => { - expect(await screen.findByText(/You intend to create a proposa/i)).not.toBeNull() - expect((await screen.findByText(/^modals.transactionFee.label/i))?.nextSibling?.textContent).toBe('25') - }) - - it('Create proposal success', async () => { - stubTransactionSuccess(createProposalTx, 'proposalsCodex', 'ProposalCreated', [ - createType('ProposalId', 1337), - ]) - - await act(async () => { - fireEvent.click(await screen.getByText(/^Sign transaction and Create$/i)) - }) - - expect(await screen.findByText('See my Proposal')).toBeDefined() - }) - - it('Create proposal failure', async () => { - stubTransactionFailure(createProposalTx) - - await act(async () => { - fireEvent.click(await screen.getByText(/^Sign transaction and Create$/i)) - }) - - expect(await screen.findByText('Failure')).toBeDefined() - }) - }) - }) - - it('Previous step', async () => { - await finishWarning() - await finishProposalType() - await finishStakingAccount() - await finishProposalDetails() - - expect(screen.queryByText('Discussion mode:')).not.toBeNull() - await clickPreviousButton() - - expect(screen.queryByDisplayValue('Some title')).not.toBeNull() - await clickPreviousButton() - - expect(screen.queryByText('Select account for Staking')).not.toBeNull() - expect(await getNextStepButton()).not.toBeDisabled() - await clickPreviousButton() - - expect(screen.queryByText('Please choose proposal type')).not.toBeNull() - expect(await getNextStepButton()).not.toBeDisabled() - }) - - describe('Discussion mode transaction', () => { - beforeEach(async () => { - mockTransactionFee({ transaction: createProposalTx }) - stubQuery( - api, - 'members.stakingAccountIdMemberStatus', - createType('PalletMembershipStakingAccountMemberBinding', { - memberId: createType('MemberId', 0), - confirmed: createType('bool', true), - }) - ) - stubQuery(api, 'members.stakingAccountIdMemberStatus.size', createType('u64', 8)) - stubTransactionSuccess(createProposalTx, 'proposalsCodex', 'ProposalCreated', [createType('ProposalId', 1337)]) - await finishWarning() - await finishProposalType('fundingRequest') - await finishStakingAccount() - await finishProposalDetails() - await finishTriggerAndDiscussion(true) - await SpecificParameters.FundingRequest.finish(100, 'bob') - - await act(async () => { - fireEvent.click(await screen.getByText(/^Sign transaction and Create$/i)) - }) - }) - - it('Arrives at the transaction modal', async () => { - expect(await screen.findByText(/You intend to change the proposal discussion thread mode./i)).toBeDefined() - expect(await screen.findByText(/Sign transaction and change mode/i)).toBeDefined() - }) - - it('Success', async () => { - stubTransactionSuccess(changeModeTx, 'proposalsDiscussion', 'ThreadModeChanged') - const button = await getButton(/sign transaction and change mode/i) - await act(async () => { - fireEvent.click(button) - }) - expect(await screen.findByText('See my Proposal')).toBeDefined() - }) - - it('Failure', async () => { - stubTransactionFailure(changeModeTx) - const button = await getButton(/sign transaction and change mode/i) - - fireEvent.click(button) - - expect(await screen.findByText('Failure')).toBeDefined() - }) - }) - }) - - const getCheckbox = async () => - await screen.queryByText("I'm aware of the possible risks associated with creating a proposal.") - - async function finishWarning() { - await renderModal() - - const button = await getWarningNextButton() - - const checkbox = await getCheckbox() - fireEvent.click(checkbox as HTMLElement) - fireEvent.click(button as HTMLElement) - } - - async function finishProposalType(type?: ProposalType) { - const typeElement = (await screen.findByText(camelCaseToText(type || 'fundingRequest'))).parentElement - ?.parentElement as HTMLElement - fireEvent.click(typeElement) - - await clickNextButton() - } - - async function finishStakingAccount() { - await selectFromDropdown('Select account for Staking', 'alice') - - await clickNextButton() - } - - async function finishProposalDetails() { - await fillProposalDetails() - - await clickNextButton() - } - - async function finishTriggerAndDiscussion(closeDiscussion = false) { - if (closeDiscussion) { - await discussionClosed() - } - await clickNextButton() - } - - async function fillProposalDetails() { - const titleInput = await screen.findByLabelText(/Proposal title/i) - fireEvent.change(titleInput, { target: { value: 'Some title' } }) - - const rationaleInput = await screen.findByLabelText(/Rationale/i) - fireEvent.change(rationaleInput, { target: { value: 'Some rationale' } }) - } - - async function triggerYes() { - const triggerToggle = await screen.findByText('Yes') - fireEvent.click(triggerToggle) - } - - async function fillTriggerBlock(value: number) { - const blockInput = await screen.getByTestId('triggerBlock') - fireEvent.change(blockInput, { target: { value } }) - } - - async function discussionClosed() { - const discussionToggle = (await screen.findAllByRole('checkbox'))[1] - fireEvent.click(discussionToggle) - } - - async function getWarningNextButton() { - return await getButton('Create A Proposal') - } - - async function getPreviousStepButton() { - return await getButton(/Previous step/i) - } - - async function clickPreviousButton() { - const button = await getPreviousStepButton() - fireEvent.click(button as HTMLElement) - } - - async function getNextStepButton() { - return getButton(/Next step/i) - } - - async function getCreateButton() { - return getButton(/Create proposal/i) - } - - async function clickNextButton() { - const button = await getNextStepButton() - fireEvent.click(button as HTMLElement) - } - - const selectGroup = async (name: string) => { - await selectFromDropdown('^Working Group$', name) - } - - const selectedOpening = async (name: string) => { - await selectFromDropdown('^Opening$', name) - } - - const selectApplication = async (name: string) => { - await selectFromDropdown('^Application$', name) - } - - async function fillField(id: string, value: number | BN | string) { - const amountInput = screen.getByTestId(id) - act(() => { - fireEvent.change(amountInput, { target: { value: String(value) } }) - }) - } - - const SpecificParameters = { - fillAmount: async (value: number | BN) => await fillField('amount-input', String(value)), - Signal: { - fillSignal: async (value: string) => await fillField('signal', value), - }, - FundingRequest: { - selectRecipient: async (name: string) => { - await selectFromDropdown('Recipient account', name) - }, - finish: async (amount: number, recipient: string) => { - await SpecificParameters.fillAmount(amount) - await SpecificParameters.FundingRequest.selectRecipient(recipient) - - const button = await getCreateButton() - act(() => { - fireEvent.click(button as HTMLElement) - }) - }, - }, - DecreaseWorkingGroupLeadStake: { - selectGroup, - }, - TerminateWorkingGroupLead: { - selectGroup, - }, - CreateWorkingGroupLeadOpening: { - selectGroup, - fillTitle: async (value: string) => await fillField('opening-title', value), - fillShortDescription: async (value: string) => await fillField('short-description', value), - fillDescription: async (value: string) => await fillField('field-description', value), - fillDuration: async (value: number | undefined) => { - await toggleCheckBox(!!value) - if (value) await fillField('field-period-length', value) - }, - fillDetails: async (value: string) => await fillField('field-details', value), - fillQuestionField: async (value: string, index: number) => { - const field = (await screen.findAllByRole('textbox'))[index] - act(() => { - fireEvent.change(field, { target: { value } }) - }) - }, - fillQuestions: async (value: OpeningMetadata.IApplicationFormQuestion[]) => { - const addQuestionBtn = await screen.findByText('Add new question') - - for (let index = 0; index < value.length; index++) { - if (index > 0) - act(() => { - fireEvent.click(addQuestionBtn) - }) - - const question = value[index].question ?? '' - await SpecificParameters.CreateWorkingGroupLeadOpening.fillQuestionField(question, index) - - await toggleCheckBox(value[index].type === QUESTION_INPUT.TEXT, index) - } - }, - fillUnstakingPeriod: async (value: number) => await fillField('leaving-unstaking-period', value), - fillStakingAmount: async (value: number) => await fillField('staking-amount', value), - fillRewardPerBlock: async (value: number) => await fillField('reward-per-block', value), - flow: async ( - step1?: { group: string; title: string; description: string; shortDesc: string }, - step2?: { duration: number | undefined; details: string }, - step3?: { questions: OpeningMetadata.IApplicationFormQuestion[] }, - step4?: { stake: number; unstakingPeriod: number; rewardPerBlock: number } - ) => { - if (!step1) return - await SpecificParameters.CreateWorkingGroupLeadOpening.selectGroup(step1.group) - await SpecificParameters.CreateWorkingGroupLeadOpening.fillTitle(step1.title) - await SpecificParameters.CreateWorkingGroupLeadOpening.fillDescription(step1.description) - await SpecificParameters.CreateWorkingGroupLeadOpening.fillShortDescription(step1.shortDesc) - await clickNextButton() - - if (!step2) return - await SpecificParameters.CreateWorkingGroupLeadOpening.fillDuration(step2.duration) - await SpecificParameters.CreateWorkingGroupLeadOpening.fillDetails(step2.details) - await clickNextButton() - - if (!step3) return - await SpecificParameters.CreateWorkingGroupLeadOpening.fillQuestions(step3.questions) - await clickNextButton() - - if (!step4) return - await SpecificParameters.CreateWorkingGroupLeadOpening.fillStakingAmount(step4.stake) - await SpecificParameters.CreateWorkingGroupLeadOpening.fillUnstakingPeriod(step4.unstakingPeriod) - await SpecificParameters.CreateWorkingGroupLeadOpening.fillRewardPerBlock(step4.rewardPerBlock) - - const createButton = await getCreateButton() - await act(async () => { - fireEvent.click(createButton as HTMLElement) - }) - }, - }, - CancelWorkingGroupLeadOpening: { - selectedOpening, - }, - SetWorkingGroupLeadReward: { - selectGroup, - fillRewardAmount: async (value: number) => await fillField('amount-input', value), - }, - FillWorkingGroupLeadOpening: { - selectedOpening, - selectApplication, - }, - UpdateWorkingGroupBudget: { - selectGroup, - }, - SetInitialInvitationCount: { - fillCount: async (value: number) => await fillField('count-input', value), - }, - } - - async function renderModal() { - return await render( - - - - - - - - - - - - - - ) - } -}) From e7d863715a21ab5ce26316125e2521e5b54bda08 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 13 Jul 2023 10:33:04 +0200 Subject: [PATCH 30/32] Fix tests failing in Playwright --- .../Proposals/CurrentProposals.stories.tsx | 56 +++---------------- packages/ui/src/mocks/data/proposals.ts | 1 + 2 files changed, 10 insertions(+), 47 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 14ba6d5be9..e34ee620b8 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -430,7 +430,7 @@ export const AddNewProposalHappy: Story = { await userEvent.type(blockInput, '9999') await waitFor(() => expect(modal.queryByText(/The maximum block number is \d+/)).toBeNull()) expect(await modal.findByText(/^ā‰ˆ.*/)) - expect(nextButton).toBeEnabled() + await waitFor(() => expect(nextButton).toBeEnabled()) }) await step('Discussion Mode', async () => { @@ -710,7 +710,14 @@ const specificParametersTest = await step(`Specific parameters: ${proposalType}`, create) await step('Sign transaction and Create', async () => { - await userEvent.click(modal.getByText('Sign transaction and Create')) + await waitFor(async () => { + const createButton = modal.queryByText('Create proposal') + if (createButton) { + await waitFor(() => expect(createButton).toBeEnabled()) + await userEvent.click(createButton) + } + await userEvent.click(modal.getByText('Sign transaction and Create')) + }) expect(await waitForModal(modal, 'Success')) }) } @@ -735,8 +742,6 @@ export const SpecificParametersSignal: Story = { editor.setData('Lorem ipsum...') await waitForElementToBeRemoved(validation) expect(nextButton).toBeEnabled() - - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -765,9 +770,6 @@ export const SpecificParametersFundingRequest: Story = { await userEvent.clear(amountField) await userEvent.type(amountField, '100') await waitFor(() => expect(modal.queryByText(/Maximal amount allowed is \d+/)).toBeNull()) - await expect(nextButton).toBeEnabled() - - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -802,9 +804,6 @@ export const SpecificParametersSetReferralCut: Story = { expect(await modal.findByText('Input must be equal or less than 50% for proposal to execute')) expect(nextButton).toBeDisabled() userEvent.click(modal.getByText(EXECUTION_WARNING_BOX)) - await waitFor(() => expect(nextButton).toBeEnabled()) - - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -852,9 +851,6 @@ export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { // Valid 1/2 userEvent.click(modal.getByText('By half')) expect(amountField).toHaveValue('500') - await waitFor(() => expect(nextButton).toBeEnabled()) - - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -893,10 +889,6 @@ export const SpecificParametersTerminateWorkingGroupLead: Story = { const amountField = modal.getByTestId('amount-input') expect(amountField).toHaveValue('') userEvent.type(amountField, '2000') - await waitFor(() => expect(nextButton).toBeDisabled()) - await waitFor(() => expect(nextButton).toBeEnabled()) - - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -963,8 +955,6 @@ export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { await userEvent.type(modal.getByLabelText('Staking amount *'), '100') await userEvent.type(modal.getByLabelText('Role cooldown period'), '0') await userEvent.type(modal.getByLabelText('Reward amount per Block'), '0.1') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1030,8 +1020,6 @@ export const SpecificParametersSetWorkingGroupLeadReward: Story = { // Valid again await userEvent.clear(amountField) await userEvent.type(amountField, '10') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1069,8 +1057,6 @@ export const SpecificParametersSetMaxValidatorCount: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '10') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1091,8 +1077,6 @@ export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { // Valid await userEvent.click(modal.getByPlaceholderText('Choose opening to cancel')) userEvent.click(body.getByText('Hire Storage Working Group Lead')) - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1126,8 +1110,6 @@ export const SpecificParametersSetCouncilBudgetIncrement: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '500') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1155,8 +1137,6 @@ export const SpecificParametersSetCouncilorReward: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '10') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1194,8 +1174,6 @@ export const SpecificParametersSetMembershipLeadInvitationQuota: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '3') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1234,9 +1212,6 @@ export const SpecificParametersFillWorkingGroupLeadOpening: Story = { expect(modal.getByText('Foo')) expect(modal.getByText('šŸ˜?')) expect(modal.getByText('Bar')) - - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1278,8 +1253,6 @@ export const SpecificParametersSetInitialInvitationCount: Story = { // Valid await userEvent.clear(countField) await userEvent.type(countField, '7') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1310,8 +1283,6 @@ export const SpecificParametersSetInitialInvitationBalance: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '7') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1337,8 +1308,6 @@ export const SpecificParametersSetMembershipPrice: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '8') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1398,8 +1367,6 @@ export const SpecificParametersUpdateWorkingGroupBudget: Story = { // Valid await userEvent.clear(amountField) await userEvent.type(amountField, '99') - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { @@ -1435,11 +1402,6 @@ export const SpecificParametersRuntimeUpgrade: Story = { await waitFor(() => expect(setIsValidWASM).toHaveBeenCalledWith(false)) const confirmation = await modal.findByText(/was loaded successfully!/) expect(within(confirmation).getByText('valid.wasm')) - - await waitFor(() => expect(nextButton).toBeEnabled()) - await waitFor(() => expect(nextButton).toBeDisabled()) // The button gets enabled 1 rendering frame due to isLoading lagging behind - await waitFor(() => expect(nextButton).toBeEnabled()) - await userEvent.click(nextButton) }) step('Transaction parameters', () => { diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 1f649861e4..cee1a07020 100644 --- a/packages/ui/src/mocks/data/proposals.ts +++ b/packages/ui/src/mocks/data/proposals.ts @@ -189,6 +189,7 @@ export const proposalsPagesChain = ( }, council: { councilSize, idlePeriodDuration: 1, announcingPeriodDuration: 1 }, + referendum: { voteStageDuration: 1, revealStageDuration: 1 }, members: { referralCutMaximumPercent: 50, From 78f5118344928edd70ef2f1c085bb7affddad262 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 13 Jul 2023 11:16:31 +0200 Subject: [PATCH 31/32] Simplify expected tx parameters amounts --- .../Proposals/CurrentProposals.stories.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index e34ee620b8..20eac6baec 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -857,7 +857,7 @@ export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { const leaderId = 10 // Set on the mock QN query const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) expect(specificParameters.toJSON()).toEqual({ - decreaseWorkingGroupLeadStake: [leaderId, Number(joy(500)), 'Forum'], + decreaseWorkingGroupLeadStake: [leaderId, 500_0000000000, 'Forum'], }) }) }), @@ -897,7 +897,7 @@ export const SpecificParametersTerminateWorkingGroupLead: Story = { expect(specificParameters.toJSON()).toEqual({ terminateWorkingGroupLead: { workerId: leaderId, - slashingAmount: Number(joy(2000)), + slashingAmount: 2000_0000000000, group: 'Forum', }, }) @@ -962,9 +962,9 @@ export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { const { description, ...data } = specificParameters.asCreateWorkingGroupLeadOpening.toJSON() expect(data).toEqual({ - rewardPerBlock: Number(joy(0.1)), + rewardPerBlock: 1000000000, stakePolicy: { - stakeAmount: Number(joy(100)), + stakeAmount: 100_0000000000, leavingUnstakingPeriod: 0, }, group: 'Forum', @@ -1026,7 +1026,7 @@ export const SpecificParametersSetWorkingGroupLeadReward: Story = { const leaderId = 10 // Set on the mock QN query const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) expect(specificParameters.toJSON()).toEqual({ - setWorkingGroupLeadReward: [leaderId, Number(joy(10)), 'Forum'], + setWorkingGroupLeadReward: [leaderId, 10_0000000000, 'Forum'], }) }) }), @@ -1114,7 +1114,7 @@ export const SpecificParametersSetCouncilBudgetIncrement: Story = { step('Transaction parameters', () => { const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) - expect(specificParameters.toJSON()).toEqual({ setCouncilBudgetIncrement: Number(joy(500)) }) + expect(specificParameters.toJSON()).toEqual({ setCouncilBudgetIncrement: 500_0000000000 }) }) }), } @@ -1141,7 +1141,7 @@ export const SpecificParametersSetCouncilorReward: Story = { step('Transaction parameters', () => { const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) - expect(specificParameters.toJSON()).toEqual({ setCouncilorReward: Number(joy(10)) }) + expect(specificParameters.toJSON()).toEqual({ setCouncilorReward: 10_0000000000 }) }) }), } @@ -1287,7 +1287,7 @@ export const SpecificParametersSetInitialInvitationBalance: Story = { step('Transaction parameters', () => { const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) - expect(specificParameters.toJSON()).toEqual({ setInitialInvitationBalance: Number(joy(7)) }) + expect(specificParameters.toJSON()).toEqual({ setInitialInvitationBalance: 7_0000000000 }) }) }), } @@ -1312,7 +1312,7 @@ export const SpecificParametersSetMembershipPrice: Story = { step('Transaction parameters', () => { const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) - expect(specificParameters.toJSON()).toEqual({ setMembershipPrice: Number(joy(8)) }) + expect(specificParameters.toJSON()).toEqual({ setMembershipPrice: 8_0000000000 }) }) }), } @@ -1372,7 +1372,7 @@ export const SpecificParametersUpdateWorkingGroupBudget: Story = { step('Transaction parameters', () => { const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) expect(specificParameters.toJSON()).toEqual({ - updateWorkingGroupBudget: [Number(joy(99)), 'Forum', 'Negative'], + updateWorkingGroupBudget: [99_0000000000, 'Forum', 'Negative'], }) }) }), From 559d470e9dd59ccbeae499133f97bfc3e505a8ce Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 13 Jul 2023 11:24:52 +0200 Subject: [PATCH 32/32] Fix failing "unit" tests --- .../ui/test/council/modals/AnnounceCandidacyModal.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx index cfe2c2a1ed..d990ea1c85 100644 --- a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx +++ b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx @@ -262,7 +262,7 @@ describe('UI: Announce Candidacy Modal', () => { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' ) - expect(screen.queryByText(/^maximum length is \d+ symbols/i)).not.toBeNull() + await waitFor(() => expect(screen.getByText(/^maximum length is \d+ symbols/i))) expect(await getNextStepButton()).toBeDisabled() }) @@ -271,7 +271,7 @@ describe('UI: Announce Candidacy Modal', () => { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua!Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' ) - expect(screen.queryByText(/^maximum length is \d+ symbols/i)).not.toBeNull() + await waitFor(() => expect(screen.getByText(/^maximum length is \d+ symbols/i))) expect(await getNextStepButton()).toBeDisabled() })