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 e082e09d08..20eac6baec 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -1,28 +1,77 @@ +import { OpeningMetadata } from '@joystream/metadata-protobuf' 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, waitForElementToBeRemoved, within } from '@storybook/testing-library' +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' -import { getButtonByText } 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 { + GetProposalsEventsDocument, + GetProposalVotesDocument, + GetProposalsCountDocument, + GetProposalsDocument, +} from '@/proposals/queries' +import { + GetWorkingGroupApplicationsDocument, + GetWorkingGroupDocument, + GetWorkingGroupOpeningsDocument, + GetWorkingGroupsDocument, +} from '@/working-groups/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.', +} + +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 - onVote: CallableFunction + onAddStakingAccountCandidate: jest.Mock + onConfirmStakingAccount: jest.Mock + onCreateProposal: jest.Mock + onChangeThreadMode: jest.Mock + onVote: jest.Mock } type Story = StoryObj> @@ -32,7 +81,11 @@ export default { argTypes: { proposalCount: { control: { type: 'range', max: MAX_ACTIVE_PROPOSAL } }, - onVote: { action: 'Voted' }, + onAddStakingAccountCandidate: { action: 'Members.StakingAccountAdded' }, + onConfirmStakingAccount: { action: 'Members.StakingAccountConfirmed' }, + onCreateProposal: { action: 'ProposalsCodex.ProposalCreated' }, + onChangeThreadMode: { action: 'proposalsDiscussion.ThreadModeChanged' }, + onVote: { action: 'ProposalsEngine.Voted' }, }, args: { @@ -48,19 +101,77 @@ export default { }, }, - mocks: ({ args }: StoryContext): MocksParameters => { + isLoggedIn: true, + balance: 100, + + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: { isTrue: true }, + size: 1, + }, + + 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 } }, + accounts: parameters.isLoggedIn + ? { active: { member: alice, balances: parameters.balance } } + : { list: [{ member: alice }] }, - chain: proposalsPagesChain(args.proposalCount, { - tx: { - proposalsEngine: { - vote: { event: 'Voted', onSend: args.onVote }, - }, + chain: proposalsPagesChain( + { + activeProposalCount: args.proposalCount, + minimumValidatorCount: parameters.minimumValidatorCount, + setMaxValidatorCountProposalMaxValidators: parameters.setMaxValidatorCountProposalMaxValidators, + initialInvitationCount: parameters.initialInvitationCount, + initialInvitationBalance: parameters.initialInvitationBalance, + + councilSize: parameters.councilSize, + councilBudget: parameters.councilBudget, + councilorReward: parameters.councilorReward, + nextRewardPayments: parameters.nextRewardPayments, + + 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: { + members: { + stakingAccountIdMemberStatus: parameters.stakingAccountIdMemberStatus, + }, + }, + tx: { + proposalsEngine: { + vote: { event: 'Voted', onSend: args.onVote }, + }, + }, + } + ), queryNode: [ { @@ -94,6 +205,62 @@ export default { proposalVotedEvents: [], }, }, + + { + query: GetProposalsEventsDocument, + data: { events: [] }, + }, + + { + query: SearchMembersDocument, + data: { + memberships: [alice], + }, + }, + { + query: GetMemberDocument, + data: { + membershipByUniqueInput: alice, + }, + }, + + { + query: GetWorkingGroupsDocument, + data: { + workingGroups: [forumWG, storageWG], + }, + }, + { + query: GetWorkingGroupDocument, + data: { + workingGroupByUniqueInput: forumWG, + }, + }, + { + query: GetWorkingGroupOpeningsDocument, + data: { + workingGroupOpenings: [OPENING_DATA], + }, + }, + { + query: GetWorkingGroupApplicationsDocument, + data: { + workingGroupApplications: [ + { + 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') }, + }, + ], + }, + }, ], } }, @@ -102,9 +269,1144 @@ export default { export const Default: Story = {} -export const AddNewProposal: Story = { +// ---------------------------------------------------------------------------- +// 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: { isTrue: false }, + size: 0, + }, + }, + + play: async ({ args, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + 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')) + } + + 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 waitForModal(modal, '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) + 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() + 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(await waitForModal(modal, 'Creating new proposal')) + + expect(localStorage.getItem('proposalCaution')).toBe('true') + }) + }) + + await step('General parameters', async () => { + 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') + + expect(nextButton).toBeDisabled() + await userEvent.click(modal.getByText('Set Referral Cut')) + await waitFor(() => expect(nextButton).not.toBeDisabled()) + await userEvent.click(nextButton) + }) + + 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) + }) + + await step('Proposal details', async () => { + const titleField = modal.getByLabelText('Proposal title') + const rationaleEditor = await getEditorByLabel(modal, 'Rationale') + + expect(nextButton).toBeDisabled() + + // 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(nextButton).toBeDisabled() + + // Invalid rational + await userEvent.clear(titleField) + await userEvent.type(titleField, PROPOSAL_DATA.title) + rationaleEditor.setData(PROPOSAL_DATA.description.padEnd(3002, ' baz')) + const rationaleValidation = await modal.findByText('Rationale exceeds maximum length') + expect(titleValidation).not.toBeInTheDocument() + expect(nextButton).toBeDisabled() + + // Valid + rationaleEditor.setData(PROPOSAL_DATA.description) + await waitForElementToBeRemoved(rationaleValidation) + expect(nextButton).toBeEnabled() + + await userEvent.click(nextButton) + }) + + 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') + + // Invalid: too low + await userEvent.type(blockInput, '10') + expect(await modal.findByText(/The minimum block number is \d+/)) + expect(nextButton).toBeDisabled() + + // 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(/^ā‰ˆ.*/)) + await waitFor(() => 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' })) + }) + + await fillSetReferralCutStep(modal, step) + }) + + 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(await modal.findByText('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.onAddStakingAccountCandidate).toHaveBeenCalledWith(alice.id) + + 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, + }) + + const changeModeTxParams = args.onChangeThreadMode.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: [] }) + }) + }) + }, +} + +// ---------------------------------------------------------------------------- +// Create proposal: Failure cases +// ---------------------------------------------------------------------------- + +export const NotEnoughFunds: Story = { + parameters: { balance: 1 }, + play: async ({ canvasElement }) => { const screen = within(canvasElement) - await userEvent.click(getButtonByText(screen, 'Add new proposal')) + 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')) + }, +} + +const fillGeneralParameters = async ( + modal: Container, + step: StepFunction, + proposalType: string, + closeDiscussion = false +) => { + 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 () => { + 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 + 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 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')) + }) + } + + await specificStep({ args, parameters, createProposal, modal, step }) + } + +export const SpecificParametersSignal: Story = { + play: specificParametersTest('Signal', async ({ args, createProposal, modal, step }) => { + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + 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() + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toHuman()).toEqual({ Signal: 'Lorem ipsum...' }) + }) + }), +} + +export const SpecificParametersFundingRequest: Story = { + 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()) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + fundingRequest: [{ account: alice.controllerAccount, amount: 100_0000000000 }], + }) + }) + }), +} + +export const SpecificParametersSetReferralCut: Story = { + 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)) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setReferralCut: 100 }) + }) + }), +} + +export const SpecificParametersDecreaseWorkingGroupLeadStake: Story = { + parameters: { 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') + }) + + 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, 500_0000000000, 'Forum'], + }) + }) + }), +} + +export const SpecificParametersTerminateWorkingGroupLead: Story = { + parameters: { 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') + }) + + 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: 2000_0000000000, + group: 'Forum', + }, + }) + }) + }), +} + +export const SpecificParametersCreateWorkingGroupLeadOpening: Story = { + parameters: { 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + const { description, ...data } = specificParameters.asCreateWorkingGroupLeadOpening.toJSON() + + expect(data).toEqual({ + rewardPerBlock: 1000000000, + stakePolicy: { + stakeAmount: 100_0000000000, + 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 }, + ], + }) + }) + }), +} + +export const SpecificParametersSetWorkingGroupLeadReward: Story = { + parameters: { 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') + }) + + 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, 10_0000000000, 'Forum'], + }) + }) + }), +} + +export const SpecificParametersSetMaxValidatorCount: Story = { + parameters: { 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setMaxValidatorCount: 10 }) + }) + }), +} + +export const SpecificParametersCancelWorkingGroupLeadOpening: Story = { + 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')) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ cancelWorkingGroupLeadOpening: [12, 'Storage'] }) + }) + }), +} + +export const SpecificParametersSetCouncilBudgetIncrement: Story = { + 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setCouncilBudgetIncrement: 500_0000000000 }) + }) + }), +} + +export const SpecificParametersSetCouncilorReward: Story = { + 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setCouncilorReward: 10_0000000000 }) + }) + }), +} + +export const SpecificParametersSetMembershipLeadInvitationQuota: Story = { + parameters: { 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setMembershipLeadInvitationQuota: 3 }) + }) + } + ), +} + +export const SpecificParametersFillWorkingGroupLeadOpening: Story = { + 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')) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + fillWorkingGroupLeadOpening: { + applicationId: 15, + openingId: 12, + workingGroup: 'Storage', + }, + }) + }) + }), +} + +export const SpecificParametersSetInitialInvitationCount: Story = { + parameters: { 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 invitations + await userEvent.type(countField, '0') + expect(await modal.findByText('Amount must be greater than zero')) + 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setInitialInvitationCount: 7 }) + }) + }), +} + +export const SpecificParametersSetInitialInvitationBalance: Story = { + parameters: { 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setInitialInvitationBalance: 7_0000000000 }) + }) + }), +} + +export const SpecificParametersSetMembershipPrice: Story = { + 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ setMembershipPrice: 8_0000000000 }) + }) + }), +} + +export const SpecificParametersUpdateWorkingGroupBudget: Story = { + parameters: { + 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') + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + updateWorkingGroupBudget: [99_0000000000, 'Forum', 'Negative'], + }) + }) + }), +} + +export const SpecificParametersRuntimeUpgrade: Story = { + 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')) + }) + + step('Transaction parameters', () => { + const [, specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ runtimeUpgrade: '0x' }) + }) + }), } 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/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} + /> + )} /> ) }) 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/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) diff --git a/packages/ui/src/mocks/data/proposals.ts b/packages/ui/src/mocks/data/proposals.ts index 196e3739ef..cee1a07020 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,54 @@ export const generateProposals = ( }) }, Math.min(limit, max - offset)) +type ProposalChainProps = { + activeProposalCount: number + minimumValidatorCount?: number + setMaxValidatorCountProposalMaxValidators?: number + initialInvitationCount?: number + initialInvitationBalance?: string + + councilSize?: number + councilBudget?: string + councilorReward?: string + nextRewardPayments?: number + + 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 = (activeProposalCount: number, extra?: Chain): Chain => +export const proposalsPagesChain = ( + { + activeProposalCount, + minimumValidatorCount = 4, + setMaxValidatorCountProposalMaxValidators = 100, + initialInvitationCount = 5, + initialInvitationBalance = joy(5), + + councilSize = 3, + councilBudget = joy(2000), + councilorReward = joy(200), + nextRewardPayments = 12345, + + onAddStakingAccountCandidate, + onConfirmStakingAccount, + onCreateProposal, + onChangeThreadMode, + + addStakingAccountCandidateFailure, + confirmStakingAccountFailure, + createProposalFailure, + changeThreadModeFailure, + }: ProposalChainProps, + extra?: Chain +): Chain => merge( { consts: { @@ -141,6 +188,9 @@ export const proposalsPagesChain = (activeProposalCount: number, extra?: Chain): maximumCashoutAllowedLimit: joy(1_666_666), }, + council: { councilSize, idlePeriodDuration: 1, announcingPeriodDuration: 1 }, + referendum: { voteStageDuration: 1, revealStageDuration: 1 }, + members: { referralCutMaximumPercent: 50, }, @@ -153,7 +203,7 @@ export const proposalsPagesChain = (activeProposalCount: number, extra?: Chain): proposalsCodex: { fundingRequestProposalMaxTotalAmount: joy(166_666), - setMaxValidatorCountProposalMaxValidators: 100, + setMaxValidatorCountProposalMaxValidators, ...Object.fromEntries( proposalTypes.map((type) => [ @@ -174,17 +224,61 @@ export const proposalsPagesChain = (activeProposalCount: number, extra?: Chain): }, query: { - members: { membershipPrice: joy(20) }, + council: { + budget: councilBudget, + councilorReward, + nextRewardPayments, + stage: { stage: { isIdle: true }, changedAt: 123 }, + }, + referendum: { stage: {} }, + + members: { + initialInvitationCount, + initialInvitationBalance, + membershipPrice: joy(20), + stakingAccountIdMemberStatus: { + memberId: 0, + confirmed: false, + size: 0, + }, + }, + proposalsEngine: { activeProposalCount }, - staking: { minimumValidatorCount: 4 }, + staking: { minimumValidatorCount }, }, tx: { proposalsCodex: { - createProposal: { event: 'Create' }, + createProposal: { event: 'ProposalCreated', onSend: onCreateProposal, failure: createProposalFailure }, + }, + proposalsDiscussion: { + changeThreadMode: { + event: 'ThreadModeChanged', + onSend: onChangeThreadMode, + failure: changeThreadModeFailure, + }, + }, + + members: { + 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('')), + }, }, - members: { confirmStakingAccount: {} }, - utility: { batch: {} }, }, } satisfies Chain, extra 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) 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 } 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 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. 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({ 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' 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() }) diff --git a/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx b/packages/ui/test/proposals/modals/AddNewProposalModal.test.tsx index 6ebccd8577..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( - - - - - - - - - - - - - - ) - } -})